Serenity Operating System
at portability 484 lines 19 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/BoxLayout.h> 36#include <LibGUI/Button.h> 37#include <LibGUI/CppSyntaxHighlighter.h> 38#include <LibGUI/FilePicker.h> 39#include <LibGUI/FontDatabase.h> 40#include <LibGUI/Menu.h> 41#include <LibGUI/MenuBar.h> 42#include <LibGUI/MessageBox.h> 43#include <LibGUI/StatusBar.h> 44#include <LibGUI/TextBox.h> 45#include <LibGUI/TextEditor.h> 46#include <LibGUI/ToolBar.h> 47#include <LibGfx/Font.h> 48 49TextEditorWidget::TextEditorWidget() 50{ 51 set_layout(make<GUI::VerticalBoxLayout>()); 52 layout()->set_spacing(0); 53 54 auto toolbar = add<GUI::ToolBar>(); 55 m_editor = add<GUI::TextEditor>(); 56 m_editor->set_ruler_visible(true); 57 m_editor->set_automatic_indentation_enabled(true); 58 m_editor->set_line_wrapping_enabled(true); 59 60 m_editor->on_change = [this] { 61 // Do not mark as diry on the first change (When document is first opened.) 62 if (m_document_opening) { 63 m_document_opening = false; 64 return; 65 } 66 67 bool was_dirty = m_document_dirty; 68 m_document_dirty = true; 69 if (!was_dirty) 70 update_title(); 71 }; 72 73 m_find_replace_widget = add<GUI::Widget>(); 74 m_find_replace_widget->set_fill_with_background_color(true); 75 m_find_replace_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 76 m_find_replace_widget->set_preferred_size(0, 48); 77 m_find_replace_widget->set_layout(make<GUI::VerticalBoxLayout>()); 78 m_find_replace_widget->layout()->set_margins({ 2, 2, 2, 4 }); 79 m_find_replace_widget->set_visible(false); 80 81 m_find_widget = m_find_replace_widget->add<GUI::Widget>(); 82 m_find_widget->set_fill_with_background_color(true); 83 m_find_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 84 m_find_widget->set_preferred_size(0, 22); 85 m_find_widget->set_layout(make<GUI::HorizontalBoxLayout>()); 86 m_find_widget->set_visible(false); 87 88 m_replace_widget = m_find_replace_widget->add<GUI::Widget>(); 89 m_replace_widget->set_fill_with_background_color(true); 90 m_replace_widget->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); 91 m_replace_widget->set_preferred_size(0, 22); 92 m_replace_widget->set_layout(make<GUI::HorizontalBoxLayout>()); 93 m_replace_widget->set_visible(false); 94 95 m_find_textbox = m_find_widget->add<GUI::TextBox>(); 96 m_replace_textbox = m_replace_widget->add<GUI::TextBox>(); 97 98 m_find_next_action = GUI::Action::create("Find next", { Mod_Ctrl, Key_G }, [&](auto&) { 99 auto needle = m_find_textbox->text(); 100 if (needle.is_empty()) { 101 dbg() << "find_next(\"\")"; 102 return; 103 } 104 auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().end()); 105 dbg() << "find_next(\"" << needle << "\") returned " << found_range; 106 if (found_range.is_valid()) { 107 m_editor->set_selection(found_range); 108 } else { 109 GUI::MessageBox::show( 110 String::format("Not found: \"%s\"", needle.characters()), 111 "Not found", 112 GUI::MessageBox::Type::Information, 113 GUI::MessageBox::InputType::OK, window()); 114 } 115 }); 116 117 m_find_previous_action = GUI::Action::create("Find previous", { Mod_Ctrl | Mod_Shift, Key_G }, [&](auto&) { 118 auto needle = m_find_textbox->text(); 119 if (needle.is_empty()) { 120 dbg() << "find_prev(\"\")"; 121 return; 122 } 123 124 auto selection_start = m_editor->normalized_selection().start(); 125 if (!selection_start.is_valid()) 126 selection_start = m_editor->normalized_selection().end(); 127 128 auto found_range = m_editor->document().find_previous(needle, selection_start); 129 130 dbg() << "find_prev(\"" << needle << "\") returned " << found_range; 131 if (found_range.is_valid()) { 132 m_editor->set_selection(found_range); 133 } else { 134 GUI::MessageBox::show( 135 String::format("Not found: \"%s\"", needle.characters()), 136 "Not found", 137 GUI::MessageBox::Type::Information, 138 GUI::MessageBox::InputType::OK, window()); 139 } 140 }); 141 142 m_replace_next_action = GUI::Action::create("Replace next", { Mod_Ctrl, Key_F1 }, [&](auto&) { 143 auto needle = m_find_textbox->text(); 144 auto substitute = m_replace_textbox->text(); 145 146 if (needle.is_empty()) 147 return; 148 149 auto selection_start = m_editor->normalized_selection().start(); 150 if (!selection_start.is_valid()) 151 selection_start = m_editor->normalized_selection().start(); 152 153 auto found_range = m_editor->document().find_next(needle, selection_start); 154 155 if (found_range.is_valid()) { 156 m_editor->set_selection(found_range); 157 m_editor->insert_at_cursor_or_replace_selection(substitute); 158 } else { 159 GUI::MessageBox::show( 160 String::format("Not found: \"%s\"", needle.characters()), 161 "Not found", 162 GUI::MessageBox::Type::Information, 163 GUI::MessageBox::InputType::OK, window()); 164 } 165 }); 166 167 m_replace_previous_action = GUI::Action::create("Replace previous", { Mod_Ctrl | Mod_Shift, Key_F1 }, [&](auto&) { 168 auto needle = m_find_textbox->text(); 169 auto substitute = m_replace_textbox->text(); 170 if (needle.is_empty()) 171 return; 172 173 auto selection_start = m_editor->normalized_selection().start(); 174 if (!selection_start.is_valid()) 175 selection_start = m_editor->normalized_selection().start(); 176 177 auto found_range = m_editor->document().find_previous(needle, selection_start); 178 179 if (found_range.is_valid()) { 180 m_editor->set_selection(found_range); 181 m_editor->insert_at_cursor_or_replace_selection(substitute); 182 } else { 183 GUI::MessageBox::show( 184 String::format("Not found: \"%s\"", needle.characters()), 185 "Not found", 186 GUI::MessageBox::Type::Information, 187 GUI::MessageBox::InputType::OK, window()); 188 } 189 }); 190 191 m_replace_all_action = GUI::Action::create("Replace all", { Mod_Ctrl, Key_F2 }, [&](auto&) { 192 auto needle = m_find_textbox->text(); 193 auto substitute = m_replace_textbox->text(); 194 if (needle.is_empty()) 195 return; 196 197 auto found_range = m_editor->document().find_next(needle); 198 while (found_range.is_valid()) { 199 m_editor->set_selection(found_range); 200 m_editor->insert_at_cursor_or_replace_selection(substitute); 201 found_range = m_editor->document().find_next(needle); 202 } 203 }); 204 205 m_find_previous_button = m_find_widget->add<GUI::Button>("Find previous"); 206 m_find_previous_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 207 m_find_previous_button->set_preferred_size(150, 0); 208 m_find_previous_button->set_action(*m_find_previous_action); 209 210 m_find_next_button = m_find_widget->add<GUI::Button>("Find next"); 211 m_find_next_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 212 m_find_next_button->set_preferred_size(150, 0); 213 m_find_next_button->set_action(*m_find_next_action); 214 215 m_find_textbox->on_return_pressed = [this] { 216 m_find_next_button->click(); 217 }; 218 219 m_find_textbox->on_escape_pressed = [this] { 220 m_find_replace_widget->set_visible(false); 221 m_editor->set_focus(true); 222 }; 223 224 m_replace_previous_button = m_replace_widget->add<GUI::Button>("Replace previous"); 225 m_replace_previous_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 226 m_replace_previous_button->set_preferred_size(100, 0); 227 m_replace_previous_button->set_action(*m_replace_previous_action); 228 229 m_replace_next_button = m_replace_widget->add<GUI::Button>("Replace next"); 230 m_replace_next_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 231 m_replace_next_button->set_preferred_size(100, 0); 232 m_replace_next_button->set_action(*m_replace_next_action); 233 234 m_replace_all_button = m_replace_widget->add<GUI::Button>("Replace all"); 235 m_replace_all_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); 236 m_replace_all_button->set_preferred_size(100, 0); 237 m_replace_all_button->set_action(*m_replace_all_action); 238 239 m_replace_textbox->on_return_pressed = [this] { 240 m_replace_next_button->click(); 241 }; 242 243 m_replace_textbox->on_escape_pressed = [this] { 244 m_find_replace_widget->set_visible(false); 245 m_editor->set_focus(true); 246 }; 247 248 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&) { 249 m_find_replace_widget->set_visible(true); 250 m_find_widget->set_visible(true); 251 m_replace_widget->set_visible(true); 252 m_find_textbox->set_focus(true); 253 254 if (m_editor->has_selection()) { 255 auto selected_text = m_editor->document().text_in_range(m_editor->normalized_selection()); 256 m_find_textbox->set_text(selected_text); 257 } 258 m_find_textbox->select_all(); 259 }); 260 261 m_editor->add_custom_context_menu_action(*m_find_replace_action); 262 m_editor->add_custom_context_menu_action(*m_find_next_action); 263 m_editor->add_custom_context_menu_action(*m_find_previous_action); 264 265 m_statusbar = add<GUI::StatusBar>(); 266 267 m_editor->on_cursor_change = [this] { 268 StringBuilder builder; 269 builder.appendf("Line: %d, Column: %d", m_editor->cursor().line() + 1, m_editor->cursor().column()); 270 m_statusbar->set_text(builder.to_string()); 271 }; 272 273 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&) { 274 if (m_document_dirty) { 275 auto save_document_first_result = GUI::MessageBox::show("Save Document First?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel); 276 if (save_document_first_result == GUI::Dialog::ExecResult::ExecYes) 277 m_save_action->activate(); 278 if (save_document_first_result == GUI::Dialog::ExecResult::ExecCancel) 279 return; 280 } 281 282 m_document_dirty = false; 283 m_editor->set_text(StringView()); 284 set_path(FileSystemPath()); 285 update_title(); 286 }); 287 288 m_open_action = GUI::CommonActions::make_open_action([this](auto&) { 289 Optional<String> open_path = GUI::FilePicker::get_open_filepath(); 290 291 if (!open_path.has_value()) 292 return; 293 294 if (m_document_dirty) { 295 auto save_document_first_result = GUI::MessageBox::show("Save Document First?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel, window()); 296 if (save_document_first_result == GUI::Dialog::ExecResult::ExecYes) 297 m_save_action->activate(); 298 if (save_document_first_result == GUI::Dialog::ExecResult::ExecCancel) 299 return; 300 } 301 302 open_sesame(open_path.value()); 303 }); 304 305 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&) { 306 Optional<String> save_path = GUI::FilePicker::get_save_filepath(m_name.is_null() ? "Untitled" : m_name, m_extension.is_null() ? "txt" : m_extension); 307 if (!save_path.has_value()) 308 return; 309 310 if (!m_editor->write_to_file(save_path.value())) { 311 GUI::MessageBox::show("Unable to save file.\n", "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); 312 return; 313 } 314 315 m_document_dirty = false; 316 set_path(FileSystemPath(save_path.value())); 317 dbg() << "Wrote document to " << save_path.value(); 318 }); 319 320 m_save_action = GUI::Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [&](const GUI::Action&) { 321 if (!m_path.is_empty()) { 322 if (!m_editor->write_to_file(m_path)) { 323 GUI::MessageBox::show("Unable to save file.\n", "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); 324 } else { 325 m_document_dirty = false; 326 update_title(); 327 } 328 return; 329 } 330 331 m_save_as_action->activate(); 332 }); 333 334 m_line_wrapping_setting_action = GUI::Action::create("Line wrapping", [&](GUI::Action& action) { 335 action.set_checked(!action.is_checked()); 336 m_editor->set_line_wrapping_enabled(action.is_checked()); 337 }); 338 m_line_wrapping_setting_action->set_checkable(true); 339 m_line_wrapping_setting_action->set_checked(m_editor->is_line_wrapping_enabled()); 340 341 auto menubar = make<GUI::MenuBar>(); 342 auto app_menu = GUI::Menu::construct("Text Editor"); 343 app_menu->add_action(*m_new_action); 344 app_menu->add_action(*m_open_action); 345 app_menu->add_action(*m_save_action); 346 app_menu->add_action(*m_save_as_action); 347 app_menu->add_separator(); 348 app_menu->add_action(GUI::CommonActions::make_quit_action([this](auto&) { 349 if (!request_close()) 350 return; 351 GUI::Application::the().quit(0); 352 })); 353 menubar->add_menu(move(app_menu)); 354 355 auto edit_menu = GUI::Menu::construct("Edit"); 356 edit_menu->add_action(m_editor->undo_action()); 357 edit_menu->add_action(m_editor->redo_action()); 358 edit_menu->add_separator(); 359 edit_menu->add_action(m_editor->cut_action()); 360 edit_menu->add_action(m_editor->copy_action()); 361 edit_menu->add_action(m_editor->paste_action()); 362 edit_menu->add_action(m_editor->delete_action()); 363 edit_menu->add_separator(); 364 edit_menu->add_action(*m_find_replace_action); 365 edit_menu->add_action(*m_find_next_action); 366 edit_menu->add_action(*m_find_previous_action); 367 edit_menu->add_action(*m_replace_next_action); 368 edit_menu->add_action(*m_replace_previous_action); 369 edit_menu->add_action(*m_replace_all_action); 370 menubar->add_menu(move(edit_menu)); 371 372 auto font_menu = GUI::Menu::construct("Font"); 373 GFontDatabase::the().for_each_fixed_width_font([&](const StringView& font_name) { 374 font_menu->add_action(GUI::Action::create(font_name, [this](const GUI::Action& action) { 375 m_editor->set_font(GFontDatabase::the().get_by_name(action.text())); 376 m_editor->update(); 377 })); 378 }); 379 380 auto view_menu = GUI::Menu::construct("View"); 381 view_menu->add_action(*m_line_wrapping_setting_action); 382 view_menu->add_separator(); 383 view_menu->add_submenu(move(font_menu)); 384 menubar->add_menu(move(view_menu)); 385 386 auto help_menu = GUI::Menu::construct("Help"); 387 help_menu->add_action(GUI::Action::create("About", [&](const GUI::Action&) { 388 GUI::AboutDialog::show("Text Editor", Gfx::Bitmap::load_from_file("/res/icons/32x32/app-texteditor.png"), window()); 389 })); 390 menubar->add_menu(move(help_menu)); 391 392 GUI::Application::the().set_menubar(move(menubar)); 393 394 toolbar->add_action(*m_new_action); 395 toolbar->add_action(*m_open_action); 396 toolbar->add_action(*m_save_action); 397 398 toolbar->add_separator(); 399 400 toolbar->add_action(m_editor->cut_action()); 401 toolbar->add_action(m_editor->copy_action()); 402 toolbar->add_action(m_editor->paste_action()); 403 toolbar->add_action(m_editor->delete_action()); 404 405 toolbar->add_separator(); 406 407 toolbar->add_action(m_editor->undo_action()); 408 toolbar->add_action(m_editor->redo_action()); 409} 410 411TextEditorWidget::~TextEditorWidget() 412{ 413} 414 415void TextEditorWidget::set_path(const FileSystemPath& file) 416{ 417 m_path = file.string(); 418 m_name = file.title(); 419 m_extension = file.extension(); 420 421 if (m_extension == "cpp" || m_extension == "h") 422 m_editor->set_syntax_highlighter(make<GUI::CppSyntaxHighlighter>()); 423 424 update_title(); 425} 426 427void TextEditorWidget::update_title() 428{ 429 StringBuilder builder; 430 builder.append("Text Editor: "); 431 builder.append(m_path); 432 if (m_document_dirty) 433 builder.append(" (*)"); 434 window()->set_title(builder.to_string()); 435} 436 437void TextEditorWidget::open_sesame(const String& path) 438{ 439 auto file = Core::File::construct(path); 440 if (!file->open(Core::IODevice::ReadOnly)) { 441 GUI::MessageBox::show(String::format("Opening \"%s\" failed: %s", path.characters(), strerror(errno)), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); 442 return; 443 } 444 445 m_editor->set_text(file->read_all()); 446 m_document_dirty = false; 447 m_document_opening = true; 448 449 set_path(FileSystemPath(path)); 450 451 m_editor->set_focus(true); 452} 453 454bool TextEditorWidget::request_close() 455{ 456 if (!m_document_dirty) 457 return true; 458 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()); 459 460 if (result == GUI::MessageBox::ExecYes) 461 m_save_action->activate(); 462 463 if (result == GUI::MessageBox::ExecNo) 464 return true; 465 466 return false; 467} 468 469void TextEditorWidget::drop_event(GUI::DropEvent& event) 470{ 471 event.accept(); 472 window()->move_to_front(); 473 474 if (event.mime_data().has_urls()) { 475 auto urls = event.mime_data().urls(); 476 if (urls.is_empty()) 477 return; 478 if (urls.size() > 1) { 479 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()); 480 return; 481 } 482 open_sesame(urls.first().path()); 483 } 484}