Serenity Operating System
at hosted 522 lines 21 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include "TextEditorWidget.h" 28#include <AK/Optional.h> 29#include <AK/StringBuilder.h> 30#include <AK/URL.h> 31#include <LibCore/File.h> 32#include <LibCore/MimeData.h> 33#include <LibGUI/AboutDialog.h> 34#include <LibGUI/Action.h> 35#include <LibGUI/ActionGroup.h> 36#include <LibGUI/BoxLayout.h> 37#include <LibGUI/Button.h> 38#include <LibGUI/CppSyntaxHighlighter.h> 39#include <LibGUI/FilePicker.h> 40#include <LibGUI/FontDatabase.h> 41#include <LibGUI/JSSyntaxHighlighter.h> 42#include <LibGUI/Menu.h> 43#include <LibGUI/MenuBar.h> 44#include <LibGUI/MessageBox.h> 45#include <LibGUI/StatusBar.h> 46#include <LibGUI/TextBox.h> 47#include <LibGUI/TextEditor.h> 48#include <LibGUI/ToolBar.h> 49#include <LibGfx/Font.h> 50#include <string.h> 51 52TextEditorWidget::TextEditorWidget() 53{ 54 set_layout<GUI::VerticalBoxLayout>(); 55 layout()->set_spacing(0); 56 57 auto& toolbar = add<GUI::ToolBar>(); 58 m_editor = add<GUI::TextEditor>(); 59 m_editor->set_ruler_visible(true); 60 m_editor->set_automatic_indentation_enabled(true); 61 m_editor->set_line_wrapping_enabled(true); 62 63 m_editor->on_change = [this] { 64 // Do not mark as diry on the first change (When document is first opened.) 65 if (m_document_opening) { 66 m_document_opening = false; 67 return; 68 } 69 70 bool was_dirty = m_document_dirty; 71 m_document_dirty = true; 72 if (!was_dirty) 73 update_title(); 74 }; 75 76 m_find_replace_widget = add<GUI::Widget>(); 77 m_find_replace_widget->set_fill_with_background_color(true); 78 m_find_replace_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 79 m_find_replace_widget->set_preferred_size(0, 48); 80 m_find_replace_widget->set_layout<GUI::VerticalBoxLayout>(); 81 m_find_replace_widget->layout()->set_margins({ 2, 2, 2, 4 }); 82 m_find_replace_widget->set_visible(false); 83 84 m_find_widget = m_find_replace_widget->add<GUI::Widget>(); 85 m_find_widget->set_fill_with_background_color(true); 86 m_find_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 87 m_find_widget->set_preferred_size(0, 22); 88 m_find_widget->set_layout<GUI::HorizontalBoxLayout>(); 89 m_find_widget->set_visible(false); 90 91 m_replace_widget = m_find_replace_widget->add<GUI::Widget>(); 92 m_replace_widget->set_fill_with_background_color(true); 93 m_replace_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 94 m_replace_widget->set_preferred_size(0, 22); 95 m_replace_widget->set_layout<GUI::HorizontalBoxLayout>(); 96 m_replace_widget->set_visible(false); 97 98 m_find_textbox = m_find_widget->add<GUI::TextBox>(); 99 m_replace_textbox = m_replace_widget->add<GUI::TextBox>(); 100 101 m_find_next_action = GUI::Action::create("Find next", { Mod_Ctrl, Key_G }, [&](auto&) { 102 auto needle = m_find_textbox->text(); 103 if (needle.is_empty()) { 104 dbg() << "find_next(\"\")"; 105 return; 106 } 107 auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().end()); 108 dbg() << "find_next(\"" << needle << "\") returned " << found_range; 109 if (found_range.is_valid()) { 110 m_editor->set_selection(found_range); 111 } else { 112 GUI::MessageBox::show( 113 String::format("Not found: \"%s\"", needle.characters()), 114 "Not found", 115 GUI::MessageBox::Type::Information, 116 GUI::MessageBox::InputType::OK, window()); 117 } 118 }); 119 120 m_find_previous_action = GUI::Action::create("Find previous", { Mod_Ctrl | Mod_Shift, Key_G }, [&](auto&) { 121 auto needle = m_find_textbox->text(); 122 if (needle.is_empty()) { 123 dbg() << "find_prev(\"\")"; 124 return; 125 } 126 127 auto selection_start = m_editor->normalized_selection().start(); 128 if (!selection_start.is_valid()) 129 selection_start = m_editor->normalized_selection().end(); 130 131 auto found_range = m_editor->document().find_previous(needle, selection_start); 132 133 dbg() << "find_prev(\"" << needle << "\") returned " << found_range; 134 if (found_range.is_valid()) { 135 m_editor->set_selection(found_range); 136 } else { 137 GUI::MessageBox::show( 138 String::format("Not found: \"%s\"", needle.characters()), 139 "Not found", 140 GUI::MessageBox::Type::Information, 141 GUI::MessageBox::InputType::OK, window()); 142 } 143 }); 144 145 m_replace_next_action = GUI::Action::create("Replace next", { Mod_Ctrl, Key_F1 }, [&](auto&) { 146 auto needle = m_find_textbox->text(); 147 auto substitute = m_replace_textbox->text(); 148 149 if (needle.is_empty()) 150 return; 151 152 auto selection_start = m_editor->normalized_selection().start(); 153 if (!selection_start.is_valid()) 154 selection_start = m_editor->normalized_selection().start(); 155 156 auto found_range = m_editor->document().find_next(needle, selection_start); 157 158 if (found_range.is_valid()) { 159 m_editor->set_selection(found_range); 160 m_editor->insert_at_cursor_or_replace_selection(substitute); 161 } else { 162 GUI::MessageBox::show( 163 String::format("Not found: \"%s\"", needle.characters()), 164 "Not found", 165 GUI::MessageBox::Type::Information, 166 GUI::MessageBox::InputType::OK, window()); 167 } 168 }); 169 170 m_replace_previous_action = GUI::Action::create("Replace previous", { Mod_Ctrl | Mod_Shift, Key_F1 }, [&](auto&) { 171 auto needle = m_find_textbox->text(); 172 auto substitute = m_replace_textbox->text(); 173 if (needle.is_empty()) 174 return; 175 176 auto selection_start = m_editor->normalized_selection().start(); 177 if (!selection_start.is_valid()) 178 selection_start = m_editor->normalized_selection().start(); 179 180 auto found_range = m_editor->document().find_previous(needle, selection_start); 181 182 if (found_range.is_valid()) { 183 m_editor->set_selection(found_range); 184 m_editor->insert_at_cursor_or_replace_selection(substitute); 185 } else { 186 GUI::MessageBox::show( 187 String::format("Not found: \"%s\"", needle.characters()), 188 "Not found", 189 GUI::MessageBox::Type::Information, 190 GUI::MessageBox::InputType::OK, window()); 191 } 192 }); 193 194 m_replace_all_action = GUI::Action::create("Replace all", { Mod_Ctrl, Key_F2 }, [&](auto&) { 195 auto needle = m_find_textbox->text(); 196 auto substitute = m_replace_textbox->text(); 197 if (needle.is_empty()) 198 return; 199 200 auto found_range = m_editor->document().find_next(needle); 201 while (found_range.is_valid()) { 202 m_editor->set_selection(found_range); 203 m_editor->insert_at_cursor_or_replace_selection(substitute); 204 found_range = m_editor->document().find_next(needle); 205 } 206 }); 207 208 m_find_previous_button = m_find_widget->add<GUI::Button>("Find previous"); 209 m_find_previous_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 210 m_find_previous_button->set_preferred_size(150, 0); 211 m_find_previous_button->set_action(*m_find_previous_action); 212 213 m_find_next_button = m_find_widget->add<GUI::Button>("Find next"); 214 m_find_next_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 215 m_find_next_button->set_preferred_size(150, 0); 216 m_find_next_button->set_action(*m_find_next_action); 217 218 m_find_textbox->on_return_pressed = [this] { 219 m_find_next_button->click(); 220 }; 221 222 m_find_textbox->on_escape_pressed = [this] { 223 m_find_replace_widget->set_visible(false); 224 m_editor->set_focus(true); 225 }; 226 227 m_replace_previous_button = m_replace_widget->add<GUI::Button>("Replace previous"); 228 m_replace_previous_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 229 m_replace_previous_button->set_preferred_size(100, 0); 230 m_replace_previous_button->set_action(*m_replace_previous_action); 231 232 m_replace_next_button = m_replace_widget->add<GUI::Button>("Replace next"); 233 m_replace_next_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 234 m_replace_next_button->set_preferred_size(100, 0); 235 m_replace_next_button->set_action(*m_replace_next_action); 236 237 m_replace_all_button = m_replace_widget->add<GUI::Button>("Replace all"); 238 m_replace_all_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 239 m_replace_all_button->set_preferred_size(100, 0); 240 m_replace_all_button->set_action(*m_replace_all_action); 241 242 m_replace_textbox->on_return_pressed = [this] { 243 m_replace_next_button->click(); 244 }; 245 246 m_replace_textbox->on_escape_pressed = [this] { 247 m_find_replace_widget->set_visible(false); 248 m_editor->set_focus(true); 249 }; 250 251 m_find_replace_action = GUI::Action::create("Find/Replace...", { Mod_Ctrl, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [this](auto&) { 252 m_find_replace_widget->set_visible(true); 253 m_find_widget->set_visible(true); 254 m_replace_widget->set_visible(true); 255 m_find_textbox->set_focus(true); 256 257 if (m_editor->has_selection()) { 258 auto selected_text = m_editor->document().text_in_range(m_editor->normalized_selection()); 259 m_find_textbox->set_text(selected_text); 260 } 261 m_find_textbox->select_all(); 262 }); 263 264 m_editor->add_custom_context_menu_action(*m_find_replace_action); 265 m_editor->add_custom_context_menu_action(*m_find_next_action); 266 m_editor->add_custom_context_menu_action(*m_find_previous_action); 267 268 m_statusbar = add<GUI::StatusBar>(); 269 270 m_editor->on_cursor_change = [this] { 271 StringBuilder builder; 272 builder.appendf("Line: %d, Column: %d", m_editor->cursor().line() + 1, m_editor->cursor().column()); 273 m_statusbar->set_text(builder.to_string()); 274 }; 275 276 m_new_action = GUI::Action::create("New", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) { 277 if (m_document_dirty) { 278 auto save_document_first_result = GUI::MessageBox::show("Save Document First?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel); 279 if (save_document_first_result == GUI::Dialog::ExecResult::ExecYes) 280 m_save_action->activate(); 281 if (save_document_first_result == GUI::Dialog::ExecResult::ExecCancel) 282 return; 283 } 284 285 m_document_dirty = false; 286 m_editor->set_text(StringView()); 287 set_path(FileSystemPath()); 288 update_title(); 289 }); 290 291 m_open_action = GUI::CommonActions::make_open_action([this](auto&) { 292 Optional<String> open_path = GUI::FilePicker::get_open_filepath(); 293 294 if (!open_path.has_value()) 295 return; 296 297 if (m_document_dirty) { 298 auto save_document_first_result = GUI::MessageBox::show("Save Document First?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel, window()); 299 if (save_document_first_result == GUI::Dialog::ExecResult::ExecYes) 300 m_save_action->activate(); 301 if (save_document_first_result == GUI::Dialog::ExecResult::ExecCancel) 302 return; 303 } 304 305 open_sesame(open_path.value()); 306 }); 307 308 m_save_as_action = GUI::Action::create("Save as...", { Mod_Ctrl | Mod_Shift, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [this](const GUI::Action&) { 309 Optional<String> save_path = GUI::FilePicker::get_save_filepath(m_name.is_null() ? "Untitled" : m_name, m_extension.is_null() ? "txt" : m_extension); 310 if (!save_path.has_value()) 311 return; 312 313 if (!m_editor->write_to_file(save_path.value())) { 314 GUI::MessageBox::show("Unable to save file.\n", "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); 315 return; 316 } 317 318 m_document_dirty = false; 319 set_path(FileSystemPath(save_path.value())); 320 dbg() << "Wrote document to " << save_path.value(); 321 }); 322 323 m_save_action = GUI::Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [&](const GUI::Action&) { 324 if (!m_path.is_empty()) { 325 if (!m_editor->write_to_file(m_path)) { 326 GUI::MessageBox::show("Unable to save file.\n", "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); 327 } else { 328 m_document_dirty = false; 329 update_title(); 330 } 331 return; 332 } 333 334 m_save_as_action->activate(); 335 }); 336 337 m_line_wrapping_setting_action = GUI::Action::create("Line wrapping", [&](GUI::Action& action) { 338 action.set_checked(!action.is_checked()); 339 m_editor->set_line_wrapping_enabled(action.is_checked()); 340 }); 341 m_line_wrapping_setting_action->set_checkable(true); 342 m_line_wrapping_setting_action->set_checked(m_editor->is_line_wrapping_enabled()); 343 344 auto menubar = make<GUI::MenuBar>(); 345 auto& app_menu = menubar->add_menu("Text Editor"); 346 app_menu.add_action(*m_new_action); 347 app_menu.add_action(*m_open_action); 348 app_menu.add_action(*m_save_action); 349 app_menu.add_action(*m_save_as_action); 350 app_menu.add_separator(); 351 app_menu.add_action(GUI::CommonActions::make_quit_action([this](auto&) { 352 if (!request_close()) 353 return; 354 GUI::Application::the().quit(0); 355 })); 356 357 auto& edit_menu = menubar->add_menu("Edit"); 358 edit_menu.add_action(m_editor->undo_action()); 359 edit_menu.add_action(m_editor->redo_action()); 360 edit_menu.add_separator(); 361 edit_menu.add_action(m_editor->cut_action()); 362 edit_menu.add_action(m_editor->copy_action()); 363 edit_menu.add_action(m_editor->paste_action()); 364 edit_menu.add_action(m_editor->delete_action()); 365 edit_menu.add_separator(); 366 edit_menu.add_action(*m_find_replace_action); 367 edit_menu.add_action(*m_find_next_action); 368 edit_menu.add_action(*m_find_previous_action); 369 edit_menu.add_action(*m_replace_next_action); 370 edit_menu.add_action(*m_replace_previous_action); 371 edit_menu.add_action(*m_replace_all_action); 372 373 auto& font_menu = menubar->add_menu("Font"); 374 GUI::FontDatabase::the().for_each_fixed_width_font([&](const StringView& font_name) { 375 font_menu.add_action(GUI::Action::create(font_name, [this](const GUI::Action& action) { 376 m_editor->set_font(GUI::FontDatabase::the().get_by_name(action.text())); 377 m_editor->update(); 378 })); 379 }); 380 381 syntax_actions = GUI::ActionGroup {}; 382 syntax_actions.set_exclusive(true); 383 384 auto& syntax_menu = menubar->add_menu("Syntax"); 385 m_plain_text_highlight = GUI::Action::create("Plain Text", [&](GUI::Action& action) { 386 action.set_checked(true); 387 m_editor->set_syntax_highlighter(nullptr); 388 m_editor->update(); 389 }); 390 m_plain_text_highlight->set_checkable(true); 391 m_plain_text_highlight->set_checked(true); 392 syntax_actions.add_action(*m_plain_text_highlight); 393 syntax_menu.add_action(*m_plain_text_highlight); 394 395 m_cpp_highlight = GUI::Action::create("C++", [&](GUI::Action& action) { 396 action.set_checked(true); 397 m_editor->set_syntax_highlighter(make<GUI::CppSyntaxHighlighter>()); 398 m_editor->update(); 399 }); 400 m_cpp_highlight->set_checkable(true); 401 syntax_actions.add_action(*m_cpp_highlight); 402 syntax_menu.add_action(*m_cpp_highlight); 403 404 m_js_highlight = GUI::Action::create("Javascript", [&](GUI::Action& action) { 405 action.set_checked(true); 406 m_editor->set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>()); 407 m_editor->update(); 408 }); 409 m_js_highlight->set_checkable(true); 410 syntax_actions.add_action(*m_js_highlight); 411 syntax_menu.add_action(*m_js_highlight); 412 413 auto& view_menu = menubar->add_menu("View"); 414 view_menu.add_action(*m_line_wrapping_setting_action); 415 view_menu.add_separator(); 416 view_menu.add_submenu(move(font_menu)); 417 view_menu.add_submenu(move(syntax_menu)); 418 419 auto& help_menu = menubar->add_menu("Help"); 420 help_menu.add_action(GUI::Action::create("About", [&](auto&) { 421 GUI::AboutDialog::show("Text Editor", Gfx::Bitmap::load_from_file("/res/icons/32x32/app-texteditor.png"), window()); 422 })); 423 424 GUI::Application::the().set_menubar(move(menubar)); 425 426 toolbar.add_action(*m_new_action); 427 toolbar.add_action(*m_open_action); 428 toolbar.add_action(*m_save_action); 429 430 toolbar.add_separator(); 431 432 toolbar.add_action(m_editor->cut_action()); 433 toolbar.add_action(m_editor->copy_action()); 434 toolbar.add_action(m_editor->paste_action()); 435 toolbar.add_action(m_editor->delete_action()); 436 437 toolbar.add_separator(); 438 439 toolbar.add_action(m_editor->undo_action()); 440 toolbar.add_action(m_editor->redo_action()); 441} 442 443TextEditorWidget::~TextEditorWidget() 444{ 445} 446 447void TextEditorWidget::set_path(const FileSystemPath& file) 448{ 449 m_path = file.string(); 450 m_name = file.title(); 451 m_extension = file.extension(); 452 453 if (m_extension == "cpp" || m_extension == "h") 454 m_cpp_highlight->activate(); 455 else if (m_extension == "js") 456 m_js_highlight->activate(); 457 else 458 m_plain_text_highlight->activate(); 459 460 update_title(); 461} 462 463void TextEditorWidget::update_title() 464{ 465 StringBuilder builder; 466 builder.append(m_path); 467 if (m_document_dirty) 468 builder.append(" (*)"); 469 builder.append(" - Text Editor"); 470 window()->set_title(builder.to_string()); 471} 472 473void TextEditorWidget::open_sesame(const String& path) 474{ 475 auto file = Core::File::construct(path); 476 if (!file->open(Core::IODevice::ReadOnly)) { 477 GUI::MessageBox::show(String::format("Opening \"%s\" failed: %s", path.characters(), strerror(errno)), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); 478 return; 479 } 480 481 m_editor->set_text(file->read_all()); 482 m_document_dirty = false; 483 m_document_opening = true; 484 485 set_path(FileSystemPath(path)); 486 487 m_editor->set_focus(true); 488} 489 490bool TextEditorWidget::request_close() 491{ 492 if (!m_document_dirty) 493 return true; 494 auto result = GUI::MessageBox::show("The document has been modified. Would you like to save?", "Unsaved changes", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel, window()); 495 496 if (result == GUI::MessageBox::ExecYes) { 497 m_save_action->activate(); 498 return true; 499 } 500 501 if (result == GUI::MessageBox::ExecNo) 502 return true; 503 504 return false; 505} 506 507void TextEditorWidget::drop_event(GUI::DropEvent& event) 508{ 509 event.accept(); 510 window()->move_to_front(); 511 512 if (event.mime_data().has_urls()) { 513 auto urls = event.mime_data().urls(); 514 if (urls.is_empty()) 515 return; 516 if (urls.size() > 1) { 517 GUI::MessageBox::show("TextEditor can only open one file at a time!", "One at a time please!", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); 518 return; 519 } 520 open_sesame(urls.first().path()); 521 } 522}