Serenity Operating System
at master 1400 lines 64 kB view raw
1/* 2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021-2022, Mustafa Quraish <mustafa@serenityos.org> 4 * Copyright (c) 2021-2022, Tobias Christiansen <tobyase@serenityos.org> 5 * Copyright (c) 2022, Timothy Slater <tslater2006@gmail.com> 6 * 7 * SPDX-License-Identifier: BSD-2-Clause 8 */ 9 10#include "MainWidget.h" 11#include "CreateNewImageDialog.h" 12#include "CreateNewLayerDialog.h" 13#include "EditGuideDialog.h" 14#include "FilterGallery.h" 15#include "FilterParams.h" 16#include "LevelsDialog.h" 17#include "ResizeImageDialog.h" 18#include <AK/String.h> 19#include <Applications/PixelPaint/PixelPaintWindowGML.h> 20#include <LibConfig/Client.h> 21#include <LibCore/Debounce.h> 22#include <LibCore/MimeData.h> 23#include <LibGUI/Application.h> 24#include <LibGUI/Clipboard.h> 25#include <LibGUI/Icon.h> 26#include <LibGUI/ItemListModel.h> 27#include <LibGUI/Menubar.h> 28#include <LibGUI/MessageBox.h> 29#include <LibGUI/Toolbar.h> 30#include <LibGUI/Window.h> 31#include <LibGfx/Rect.h> 32 33namespace PixelPaint { 34 35IconBag g_icon_bag; 36 37MainWidget::MainWidget() 38 : Widget() 39{ 40 load_from_gml(pixel_paint_window_gml).release_value_but_fixme_should_propagate_errors(); 41 42 m_toolbox = find_descendant_of_type_named<PixelPaint::ToolboxWidget>("toolbox"); 43 m_statusbar = *find_descendant_of_type_named<GUI::Statusbar>("statusbar"); 44 45 m_tab_widget = find_descendant_of_type_named<GUI::TabWidget>("tab_widget"); 46 47 m_palette_widget = *find_descendant_of_type_named<PixelPaint::PaletteWidget>("palette_widget"); 48 49 m_histogram_widget = *find_descendant_of_type_named<PixelPaint::HistogramWidget>("histogram_widget"); 50 m_vectorscope_widget = *find_descendant_of_type_named<PixelPaint::VectorscopeWidget>("vectorscope_widget"); 51 m_layer_list_widget = *find_descendant_of_type_named<PixelPaint::LayerListWidget>("layer_list_widget"); 52 m_layer_list_widget->on_layer_select = [&](auto* layer) { 53 auto* editor = current_image_editor(); 54 VERIFY(editor); 55 editor->set_active_layer(layer); 56 }; 57 58 m_layer_properties_widget = *find_descendant_of_type_named<PixelPaint::LayerPropertiesWidget>("layer_properties_widget"); 59 m_tool_properties_widget = *find_descendant_of_type_named<PixelPaint::ToolPropertiesWidget>("tool_properties_widget"); 60 61 m_toolbox->on_tool_selection = [&](auto* tool) { 62 auto* editor = current_image_editor(); 63 VERIFY(editor); 64 editor->set_active_tool(tool); 65 m_tool_properties_widget->set_active_tool(tool); 66 }; 67 68 m_tab_widget->on_middle_click = [&](auto& widget) { 69 m_tab_widget->on_tab_close_click(widget); 70 }; 71 72 m_tab_widget->on_tab_close_click = [&](auto& widget) { 73 auto& image_editor = verify_cast<PixelPaint::ImageEditor>(widget); 74 if (image_editor.request_close()) { 75 m_tab_widget->deferred_invoke([&] { 76 m_tab_widget->remove_tab(image_editor); 77 if (m_tab_widget->children().size() == 0) { 78 m_histogram_widget->set_image(nullptr); 79 m_vectorscope_widget->set_image(nullptr); 80 m_layer_list_widget->set_image(nullptr); 81 m_layer_properties_widget->set_layer(nullptr); 82 m_palette_widget->set_image_editor(nullptr); 83 m_tool_properties_widget->set_enabled(false); 84 set_actions_enabled(false); 85 set_mask_actions_for_layer(nullptr); 86 } 87 update_window_modified(); 88 }); 89 } 90 }; 91 92 m_tab_widget->on_change = [&](auto& widget) { 93 auto& image_editor = verify_cast<PixelPaint::ImageEditor>(widget); 94 m_palette_widget->set_image_editor(&image_editor); 95 m_histogram_widget->set_image(&image_editor.image()); 96 m_vectorscope_widget->set_image(&image_editor.image()); 97 m_layer_list_widget->set_image(&image_editor.image()); 98 m_layer_properties_widget->set_layer(image_editor.active_layer()); 99 update_window_modified(); 100 if (auto* active_tool = m_toolbox->active_tool()) 101 image_editor.set_active_tool(active_tool); 102 m_show_guides_action->set_checked(image_editor.guide_visibility()); 103 m_show_rulers_action->set_checked(image_editor.ruler_visibility()); 104 image_editor.on_scale_change(image_editor.scale()); 105 image_editor.undo_stack().on_state_change = [this] { 106 image_editor_did_update_undo_stack(); 107 }; 108 // Ensure that our undo/redo actions are in sync with the current editor. 109 image_editor_did_update_undo_stack(); 110 set_mask_actions_for_layer(image_editor.active_layer()); 111 }; 112} 113 114void MainWidget::image_editor_did_update_undo_stack() 115{ 116 auto* image_editor = current_image_editor(); 117 if (!image_editor) { 118 m_undo_action->set_enabled(false); 119 m_redo_action->set_enabled(false); 120 return; 121 } 122 image_editor->update_modified(); 123 124 auto make_action_text = [](auto prefix, auto suffix) { 125 StringBuilder builder; 126 builder.append(prefix); 127 if (suffix.has_value()) { 128 builder.append(' '); 129 builder.append(suffix.value()); 130 } 131 return builder.to_deprecated_string(); 132 }; 133 134 auto& undo_stack = image_editor->undo_stack(); 135 m_undo_action->set_enabled(undo_stack.can_undo()); 136 m_redo_action->set_enabled(undo_stack.can_redo()); 137 138 m_undo_action->set_text(make_action_text("&Undo"sv, undo_stack.undo_action_text())); 139 m_redo_action->set_text(make_action_text("&Redo"sv, undo_stack.redo_action_text())); 140} 141 142// Note: Update these together! v 143static Vector<DeprecatedString> const s_suggested_zoom_levels { "25%", "50%", "100%", "200%", "300%", "400%", "800%", "1600%", "Fit to width", "Fit to height", "Fit entire image" }; 144static constexpr int s_zoom_level_fit_width = 8; 145static constexpr int s_zoom_level_fit_height = 9; 146static constexpr int s_zoom_level_fit_image = 10; 147// Note: Update these together! ^ 148 149ErrorOr<void> MainWidget::initialize_menubar(GUI::Window& window) 150{ 151 auto file_menu = TRY(window.try_add_menu("&File")); 152 153 m_new_image_action = GUI::Action::create( 154 "&New Image...", { Mod_Ctrl, Key_N }, g_icon_bag.filetype_pixelpaint, [&](auto&) { 155 auto dialog = PixelPaint::CreateNewImageDialog::construct(&window); 156 if (dialog->exec() == GUI::Dialog::ExecResult::OK) { 157 auto image_result = PixelPaint::Image::create_with_size(dialog->image_size()); 158 if (image_result.is_error()) { 159 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to create image with size {}, error: {}", dialog->image_size(), image_result.error())); 160 return; 161 } 162 auto image = image_result.release_value(); 163 auto bg_layer_result = PixelPaint::Layer::create_with_size(*image, image->size(), "Background"); 164 if (bg_layer_result.is_error()) { 165 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to create layer with size {}, error: {}", image->size(), bg_layer_result.error())); 166 return; 167 } 168 auto bg_layer = bg_layer_result.release_value(); 169 image->add_layer(*bg_layer); 170 auto background_color = dialog->background_color(); 171 if (background_color != Gfx::Color::Transparent) 172 bg_layer->content_bitmap().fill(background_color); 173 174 auto& editor = create_new_editor(*image); 175 auto image_title = dialog->image_name().trim_whitespace(); 176 editor.set_title(image_title.is_empty() ? "Untitled" : image_title); 177 editor.set_unmodified(); 178 179 m_histogram_widget->set_image(image); 180 m_vectorscope_widget->set_image(image); 181 m_layer_list_widget->set_image(image); 182 m_layer_list_widget->set_selected_layer(bg_layer); 183 } 184 }); 185 186 m_new_image_from_clipboard_action = GUI::Action::create( 187 "&New Image from Clipboard", { Mod_Ctrl | Mod_Shift, Key_V }, g_icon_bag.new_clipboard, [&](auto&) { 188 auto result = create_image_from_clipboard(); 189 if (result.is_error()) { 190 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to create image from clipboard: {}", result.error())); 191 } 192 }); 193 194 m_open_image_action = GUI::CommonActions::make_open_action([&](auto&) { 195 auto response = FileSystemAccessClient::Client::the().open_file(&window); 196 if (response.is_error()) 197 return; 198 open_image(response.release_value()); 199 }); 200 201 m_save_image_as_action = GUI::CommonActions::make_save_as_action([&](auto&) { 202 auto* editor = current_image_editor(); 203 VERIFY(editor); 204 editor->save_project_as(); 205 }); 206 207 m_save_image_action = GUI::CommonActions::make_save_action([&](auto&) { 208 auto* editor = current_image_editor(); 209 VERIFY(editor); 210 editor->save_project(); 211 }); 212 213 TRY(file_menu->try_add_action(*m_new_image_action)); 214 TRY(file_menu->try_add_action(*m_new_image_from_clipboard_action)); 215 TRY(file_menu->try_add_action(*m_open_image_action)); 216 TRY(file_menu->try_add_action(*m_save_image_action)); 217 TRY(file_menu->try_add_action(*m_save_image_as_action)); 218 219 m_export_submenu = TRY(file_menu->try_add_submenu("&Export")); 220 221 TRY(m_export_submenu->try_add_action( 222 GUI::Action::create( 223 "As &BMP", [&](auto&) { 224 auto* editor = current_image_editor(); 225 VERIFY(editor); 226 auto response = FileSystemAccessClient::Client::the().save_file(&window, editor->title(), "bmp"); 227 if (response.is_error()) 228 return; 229 auto preserve_alpha_channel = GUI::MessageBox::show(&window, "Do you wish to preserve transparency?"sv, "Preserve transparency?"sv, GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo); 230 auto result = editor->image().export_bmp_to_file(response.value().release_stream(), preserve_alpha_channel == GUI::MessageBox::ExecResult::Yes); 231 if (result.is_error()) 232 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Export to BMP failed: {}", result.error())); 233 }))); 234 235 TRY(m_export_submenu->try_add_action( 236 GUI::Action::create( 237 "As &PNG", [&](auto&) { 238 auto* editor = current_image_editor(); 239 VERIFY(editor); 240 // TODO: fix bmp on line below? 241 auto response = FileSystemAccessClient::Client::the().save_file(&window, editor->title(), "png"); 242 if (response.is_error()) 243 return; 244 auto preserve_alpha_channel = GUI::MessageBox::show(&window, "Do you wish to preserve transparency?"sv, "Preserve transparency?"sv, GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo); 245 auto result = editor->image().export_png_to_file(response.value().release_stream(), preserve_alpha_channel == GUI::MessageBox::ExecResult::Yes); 246 if (result.is_error()) 247 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Export to PNG failed: {}", result.error())); 248 }))); 249 250 TRY(m_export_submenu->try_add_action( 251 GUI::Action::create( 252 "As &QOI", [&](auto&) { 253 auto* editor = current_image_editor(); 254 VERIFY(editor); 255 auto response = FileSystemAccessClient::Client::the().save_file(&window, editor->title(), "qoi"); 256 if (response.is_error()) 257 return; 258 auto result = editor->image().export_qoi_to_file(response.value().release_stream()); 259 if (result.is_error()) 260 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Export to QOI failed: {}", result.error())); 261 }))); 262 263 m_export_submenu->set_icon(g_icon_bag.file_export); 264 265 TRY(file_menu->try_add_separator()); 266 267 TRY(file_menu->add_recent_files_list([&](auto& action) { 268 auto path = action.text(); 269 auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(&window, path); 270 if (response.is_error()) 271 return; 272 open_image(response.release_value()); 273 })); 274 275 m_close_image_action = GUI::Action::create("&Close Image", { Mod_Ctrl, Key_W }, g_icon_bag.close_image, [&](auto&) { 276 auto* active_widget = m_tab_widget->active_widget(); 277 VERIFY(active_widget); 278 m_tab_widget->on_tab_close_click(*active_widget); 279 }); 280 281 TRY(file_menu->try_add_action(*m_close_image_action)); 282 283 TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([this](auto&) { 284 if (request_close()) 285 GUI::Application::the()->quit(); 286 }))); 287 288 m_edit_menu = TRY(window.try_add_menu("&Edit")); 289 290 m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) { 291 auto* editor = current_image_editor(); 292 VERIFY(editor); 293 294 if (!editor->active_layer()) { 295 dbgln("Cannot cut with no active layer selected"); 296 return; 297 } 298 auto bitmap = editor->active_layer()->copy_bitmap(editor->image().selection()); 299 if (!bitmap) { 300 dbgln("copy_bitmap() from Layer failed"); 301 return; 302 } 303 GUI::Clipboard::the().set_bitmap(*bitmap); 304 editor->active_layer()->erase_selection(editor->image().selection()); 305 }); 306 307 m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) { 308 auto* editor = current_image_editor(); 309 VERIFY(editor); 310 311 if (!editor->active_layer()) { 312 dbgln("Cannot copy with no active layer selected"); 313 return; 314 } 315 auto bitmap = editor->active_layer()->copy_bitmap(editor->image().selection()); 316 if (!bitmap) { 317 dbgln("copy_bitmap() from Layer failed"); 318 return; 319 } 320 auto layer_rect = editor->active_layer()->relative_rect(); 321 HashMap<DeprecatedString, DeprecatedString> layer_metadata; 322 layer_metadata.set("pixelpaint-layer-x", DeprecatedString::number(layer_rect.x())); 323 layer_metadata.set("pixelpaint-layer-y", DeprecatedString::number(layer_rect.y())); 324 325 GUI::Clipboard::the().set_bitmap(*bitmap, layer_metadata); 326 }); 327 328 m_copy_merged_action = GUI::Action::create( 329 "Copy &Merged", { Mod_Ctrl | Mod_Shift, Key_C }, g_icon_bag.edit_copy, [&](auto&) { 330 auto* editor = current_image_editor(); 331 VERIFY(editor); 332 333 auto bitmap = editor->image().copy_bitmap(editor->image().selection()); 334 if (!bitmap) { 335 dbgln("copy_bitmap() from Image failed"); 336 return; 337 } 338 GUI::Clipboard::the().set_bitmap(*bitmap); 339 }); 340 341 m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) { 342 auto* editor = current_image_editor(); 343 if (!editor) { 344 auto result = create_image_from_clipboard(); 345 if (result.is_error()) { 346 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to create image from clipboard: {}", result.error())); 347 } 348 return; 349 } 350 351 auto data_and_type = GUI::Clipboard::the().fetch_data_and_type(); 352 auto bitmap = data_and_type.as_bitmap(); 353 if (!bitmap) 354 return; 355 356 auto layer_result = PixelPaint::Layer::create_with_bitmap(editor->image(), *bitmap, "Pasted layer"); 357 if (layer_result.is_error()) { 358 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Could not create bitmap when pasting: {}", layer_result.error())); 359 return; 360 } 361 auto layer = layer_result.release_value(); 362 363 auto layer_x_position = data_and_type.metadata.get("pixelpaint-layer-x"); 364 auto layer_y_position = data_and_type.metadata.get("pixelpaint-layer-y"); 365 if (layer_x_position.has_value() && layer_y_position.has_value()) { 366 auto x = layer_x_position.value().to_int(); 367 auto y = layer_y_position.value().to_int(); 368 if (x.has_value() && x.value()) { 369 auto pasted_layer_location = Gfx::IntPoint { x.value(), y.value() }; 370 371 auto pasted_layer_frame_rect = editor->content_to_frame_rect({ pasted_layer_location, layer->size() }).to_type<int>(); 372 // If the pasted layer is entirely outside the canvas bounds, default to the top left. 373 if (!editor->content_rect().intersects(pasted_layer_frame_rect)) 374 pasted_layer_location = {}; 375 376 layer->set_location(pasted_layer_location); 377 // Ensure the pasted layer is visible to the user. 378 if (!editor->frame_inner_rect().intersects(pasted_layer_frame_rect)) 379 editor->fit_content_to_view(); 380 } 381 } 382 383 editor->image().add_layer(*layer); 384 editor->set_active_layer(layer); 385 editor->image().selection().clear(); 386 }); 387 GUI::Clipboard::the().on_change = [&](auto& mime_type) { 388 m_paste_action->set_enabled(mime_type == "image/x-serenityos"); 389 }; 390 m_paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "image/x-serenityos"); 391 392 m_undo_action = GUI::CommonActions::make_undo_action([&](auto&) { 393 if (auto* editor = current_image_editor()) 394 editor->undo(); 395 }); 396 397 m_redo_action = GUI::CommonActions::make_redo_action([&](auto&) { 398 auto* editor = current_image_editor(); 399 VERIFY(editor); 400 editor->redo(); 401 }); 402 403 TRY(m_edit_menu->try_add_action(*m_undo_action)); 404 TRY(m_edit_menu->try_add_action(*m_redo_action)); 405 TRY(m_edit_menu->try_add_separator()); 406 TRY(m_edit_menu->try_add_action(*m_cut_action)); 407 TRY(m_edit_menu->try_add_action(*m_copy_action)); 408 TRY(m_edit_menu->try_add_action(*m_copy_merged_action)); 409 TRY(m_edit_menu->try_add_action(*m_paste_action)); 410 TRY(m_edit_menu->try_add_separator()); 411 412 TRY(m_edit_menu->try_add_action(GUI::CommonActions::make_select_all_action([&](auto&) { 413 auto* editor = current_image_editor(); 414 VERIFY(editor); 415 if (!editor->active_layer()) 416 return; 417 editor->image().selection().merge(editor->active_layer()->relative_rect(), PixelPaint::Selection::MergeMode::Set); 418 editor->did_complete_action("Select All"sv); 419 }))); 420 TRY(m_edit_menu->try_add_action(GUI::Action::create( 421 "Clear &Selection", g_icon_bag.clear_selection, [&](auto&) { 422 auto* editor = current_image_editor(); 423 VERIFY(editor); 424 editor->image().selection().clear(); 425 editor->did_complete_action("Clear Selection"sv); 426 }))); 427 TRY(m_edit_menu->try_add_action(GUI::Action::create( 428 "&Invert Selection", g_icon_bag.invert_selection, [&](auto&) { 429 auto* editor = current_image_editor(); 430 VERIFY(editor); 431 editor->image().selection().invert(); 432 editor->did_complete_action("Invert Selection"sv); 433 }))); 434 435 TRY(m_edit_menu->try_add_separator()); 436 TRY(m_edit_menu->try_add_action(GUI::Action::create( 437 "S&wap Colors", { Mod_None, Key_X }, g_icon_bag.swap_colors, [&](auto&) { 438 auto* editor = current_image_editor(); 439 VERIFY(editor); 440 auto old_primary_color = editor->primary_color(); 441 editor->set_primary_color(editor->secondary_color()); 442 editor->set_secondary_color(old_primary_color); 443 }))); 444 TRY(m_edit_menu->try_add_action(GUI::Action::create( 445 "&Default Colors", { Mod_None, Key_D }, g_icon_bag.default_colors, [&](auto&) { 446 auto* editor = current_image_editor(); 447 VERIFY(editor); 448 editor->set_primary_color(Color::Black); 449 editor->set_secondary_color(Color::White); 450 }))); 451 TRY(m_edit_menu->try_add_action(GUI::Action::create( 452 "&Load Color Palette", g_icon_bag.load_color_palette, [&](auto&) { 453 auto response = FileSystemAccessClient::Client::the().open_file(&window, "Load Color Palette"); 454 if (response.is_error()) 455 return; 456 457 auto result = PixelPaint::PaletteWidget::load_palette_file(response.release_value().release_stream()); 458 if (result.is_error()) { 459 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Loading color palette failed: {}", result.error())); 460 return; 461 } 462 463 m_palette_widget->display_color_list(result.value()); 464 }))); 465 TRY(m_edit_menu->try_add_action(GUI::Action::create( 466 "Sa&ve Color Palette", g_icon_bag.save_color_palette, [&](auto&) { 467 auto response = FileSystemAccessClient::Client::the().save_file(&window, "untitled", "palette"); 468 if (response.is_error()) 469 return; 470 471 auto result = PixelPaint::PaletteWidget::save_palette_file(m_palette_widget->colors(), response.release_value().release_stream()); 472 if (result.is_error()) 473 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Writing color palette failed: {}", result.error())); 474 }))); 475 476 m_view_menu = TRY(window.try_add_menu("&View")); 477 478 m_zoom_in_action = GUI::CommonActions::make_zoom_in_action( 479 [&](auto&) { 480 auto* editor = current_image_editor(); 481 VERIFY(editor); 482 editor->scale_by(0.1f); 483 }); 484 485 m_zoom_out_action = GUI::CommonActions::make_zoom_out_action( 486 [&](auto&) { 487 auto* editor = current_image_editor(); 488 VERIFY(editor); 489 editor->scale_by(-0.1f); 490 }); 491 492 m_reset_zoom_action = GUI::CommonActions::make_reset_zoom_action( 493 [&](auto&) { 494 if (auto* editor = current_image_editor()) 495 editor->reset_view(); 496 }); 497 498 m_add_guide_action = GUI::Action::create( 499 "&Add Guide", g_icon_bag.add_guide, [&](auto&) { 500 auto dialog = PixelPaint::EditGuideDialog::construct(&window); 501 if (dialog->exec() != GUI::Dialog::ExecResult::OK) 502 return; 503 auto* editor = current_image_editor(); 504 VERIFY(editor); 505 auto offset = dialog->offset_as_pixel(*editor); 506 if (!offset.has_value()) 507 return; 508 editor->add_guide(PixelPaint::Guide::construct(dialog->orientation(), offset.value())); 509 }); 510 511 // Save this so other methods can use it 512 m_show_guides_action = GUI::Action::create_checkable( 513 "Show &Guides", [&](auto& action) { 514 Config::write_bool("PixelPaint"sv, "Guides"sv, "Show"sv, action.is_checked()); 515 auto* editor = current_image_editor(); 516 VERIFY(editor); 517 editor->set_guide_visibility(action.is_checked()); 518 }); 519 m_show_guides_action->set_checked(Config::read_bool("PixelPaint"sv, "Guides"sv, "Show"sv, true)); 520 521 TRY(m_view_menu->try_add_action(*m_zoom_in_action)); 522 TRY(m_view_menu->try_add_action(*m_zoom_out_action)); 523 TRY(m_view_menu->try_add_action(*m_reset_zoom_action)); 524 TRY(m_view_menu->try_add_action(GUI::Action::create( 525 "Fit Image To &View", g_icon_bag.fit_image_to_view, [&](auto&) { 526 auto* editor = current_image_editor(); 527 VERIFY(editor); 528 editor->fit_image_to_view(); 529 }))); 530 TRY(m_view_menu->try_add_separator()); 531 TRY(m_view_menu->try_add_action(*m_add_guide_action)); 532 TRY(m_view_menu->try_add_action(*m_show_guides_action)); 533 534 TRY(m_view_menu->try_add_action(GUI::Action::create( 535 "&Clear Guides", g_icon_bag.clear_guides, [&](auto&) { 536 auto* editor = current_image_editor(); 537 VERIFY(editor); 538 editor->clear_guides(); 539 }))); 540 TRY(m_view_menu->try_add_separator()); 541 542 auto show_pixel_grid_action = GUI::Action::create_checkable( 543 "Show &Pixel Grid", [&](auto& action) { 544 Config::write_bool("PixelPaint"sv, "PixelGrid"sv, "Show"sv, action.is_checked()); 545 auto* editor = current_image_editor(); 546 VERIFY(editor); 547 editor->set_pixel_grid_visibility(action.is_checked()); 548 }); 549 show_pixel_grid_action->set_checked(Config::read_bool("PixelPaint"sv, "PixelGrid"sv, "Show"sv, true)); 550 TRY(m_view_menu->try_add_action(*show_pixel_grid_action)); 551 552 m_show_rulers_action = TRY(GUI::Action::try_create_checkable( 553 "Show R&ulers", { Mod_Ctrl, Key_R }, [&](auto& action) { 554 Config::write_bool("PixelPaint"sv, "Rulers"sv, "Show"sv, action.is_checked()); 555 auto* editor = current_image_editor(); 556 VERIFY(editor); 557 editor->set_ruler_visibility(action.is_checked()); 558 })); 559 m_show_rulers_action->set_checked(Config::read_bool("PixelPaint"sv, "Rulers"sv, "Show"sv, true)); 560 TRY(m_view_menu->try_add_action(*m_show_rulers_action)); 561 562 m_show_active_layer_boundary_action = GUI::Action::create_checkable( 563 "Show Active Layer &Boundary", [&](auto& action) { 564 Config::write_bool("PixelPaint"sv, "ImageEditor"sv, "ShowActiveLayerBoundary"sv, action.is_checked()); 565 auto* editor = current_image_editor(); 566 VERIFY(editor); 567 editor->set_show_active_layer_boundary(action.is_checked()); 568 }); 569 m_show_active_layer_boundary_action->set_checked(Config::read_bool("PixelPaint"sv, "ImageEditor"sv, "ShowActiveLayerBoundary"sv, true)); 570 TRY(m_view_menu->try_add_action(*m_show_active_layer_boundary_action)); 571 572 TRY(m_view_menu->try_add_separator()); 573 574 auto histogram_action = GUI::Action::create_checkable("&Histogram", [&](auto& action) { 575 Config::write_bool("PixelPaint"sv, "Scopes"sv, "ShowHistogram"sv, action.is_checked()); 576 m_histogram_widget->parent_widget()->set_visible(action.is_checked()); 577 }); 578 histogram_action->set_checked(Config::read_bool("PixelPaint"sv, "Scopes"sv, "ShowHistogram"sv, false)); 579 m_histogram_widget->parent_widget()->set_visible(histogram_action->is_checked()); 580 581 auto vectorscope_action = GUI::Action::create_checkable("&Vectorscope", [&](auto& action) { 582 Config::write_bool("PixelPaint"sv, "Scopes"sv, "ShowVectorscope"sv, action.is_checked()); 583 m_vectorscope_widget->parent_widget()->set_visible(action.is_checked()); 584 }); 585 vectorscope_action->set_checked(Config::read_bool("PixelPaint"sv, "Scopes"sv, "ShowVectorscope"sv, false)); 586 m_vectorscope_widget->parent_widget()->set_visible(vectorscope_action->is_checked()); 587 588 auto scopes_menu = TRY(m_view_menu->try_add_submenu("&Scopes")); 589 TRY(scopes_menu->try_add_action(histogram_action)); 590 TRY(scopes_menu->try_add_action(vectorscope_action)); 591 592 m_tool_menu = TRY(window.try_add_menu("&Tool")); 593 m_toolbox->for_each_tool([&](auto& tool) { 594 if (tool.action()) 595 m_tool_menu->add_action(*tool.action()); 596 return IterationDecision::Continue; 597 }); 598 599 m_image_menu = TRY(window.try_add_menu("&Image")); 600 TRY(m_image_menu->try_add_action(GUI::Action::create( 601 "Flip Image &Vertically", g_icon_bag.edit_flip_vertical, [&](auto&) { 602 auto* editor = current_image_editor(); 603 VERIFY(editor); 604 auto image_flip_or_error = editor->image().flip(Gfx::Orientation::Vertical); 605 if (image_flip_or_error.is_error()) { 606 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to flip image: {}", image_flip_or_error.error().string_literal()))); 607 return; 608 } 609 editor->did_complete_action("Flip Image Vertically"sv); 610 }))); 611 TRY(m_image_menu->try_add_action(GUI::Action::create( 612 "Flip Image &Horizontally", g_icon_bag.edit_flip_horizontal, [&](auto&) { 613 auto* editor = current_image_editor(); 614 VERIFY(editor); 615 auto image_flip_or_error = editor->image().flip(Gfx::Orientation::Horizontal); 616 if (image_flip_or_error.is_error()) { 617 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to flip image: {}", image_flip_or_error.error().string_literal()))); 618 return; 619 } 620 editor->did_complete_action("Flip Image Horizontally"sv); 621 }))); 622 TRY(m_image_menu->try_add_separator()); 623 624 TRY(m_image_menu->try_add_action(GUI::Action::create("Rotate Image &Counterclockwise", { Mod_Ctrl | Mod_Shift, Key_LessThan }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-rotate-ccw.png"sv)), 625 [&](auto&) { 626 auto* editor = current_image_editor(); 627 VERIFY(editor); 628 auto image_rotate_or_error = editor->image().rotate(Gfx::RotationDirection::CounterClockwise); 629 if (image_rotate_or_error.is_error()) { 630 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to rotate image: {}", image_rotate_or_error.error().string_literal()))); 631 return; 632 } 633 editor->did_complete_action("Rotate Image Counterclockwise"sv); 634 }))); 635 636 TRY(m_image_menu->try_add_action(GUI::Action::create("Rotate Image Clock&wise", { Mod_Ctrl | Mod_Shift, Key_GreaterThan }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-rotate-cw.png"sv)), 637 [&](auto&) { 638 auto* editor = current_image_editor(); 639 VERIFY(editor); 640 auto image_rotate_or_error = editor->image().rotate(Gfx::RotationDirection::Clockwise); 641 if (image_rotate_or_error.is_error()) { 642 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to rotate image: {}", image_rotate_or_error.error().string_literal()))); 643 return; 644 } 645 editor->did_complete_action("Rotate Image Clockwise"sv); 646 }))); 647 TRY(m_image_menu->try_add_separator()); 648 TRY(m_image_menu->try_add_action(GUI::Action::create( 649 "&Resize Image...", { Mod_Ctrl | Mod_Shift, Key_R }, g_icon_bag.resize_image, [&](auto&) { 650 auto* editor = current_image_editor(); 651 VERIFY(editor); 652 auto dialog = PixelPaint::ResizeImageDialog::construct(editor->image().size(), &window); 653 if (dialog->exec() == GUI::Dialog::ExecResult::OK) { 654 auto image_resize_or_error = editor->image().resize(dialog->desired_size(), dialog->scaling_mode()); 655 if (image_resize_or_error.is_error()) { 656 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to resize image: {}", image_resize_or_error.error().string_literal()))); 657 return; 658 } 659 editor->did_complete_action("Resize Image"sv); 660 } 661 }))); 662 TRY(m_image_menu->try_add_action(GUI::Action::create( 663 "&Crop Image to Selection", g_icon_bag.crop, [&](auto&) { 664 auto* editor = current_image_editor(); 665 VERIFY(editor); 666 // FIXME: disable this action if there is no selection 667 if (editor->image().selection().is_empty()) 668 return; 669 auto crop_rect = editor->image().rect().intersected(editor->image().selection().bounding_rect()); 670 auto image_crop_or_error = editor->image().crop(crop_rect); 671 if (image_crop_or_error.is_error()) { 672 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to crop image: {}", image_crop_or_error.error().string_literal()))); 673 return; 674 } 675 editor->image().selection().clear(); 676 editor->did_complete_action("Crop Image to Selection"sv); 677 }))); 678 679 TRY(m_image_menu->try_add_action(GUI::Action::create( 680 "&Crop Image to Content", g_icon_bag.crop, [&](auto&) { 681 auto* editor = current_image_editor(); 682 VERIFY(editor); 683 684 auto content_bounding_rect = editor->image().nonempty_content_bounding_rect(); 685 if (!content_bounding_rect.has_value()) 686 return; 687 688 auto image_crop_or_error = editor->image().crop(content_bounding_rect.value()); 689 if (image_crop_or_error.is_error()) { 690 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to crop image: {}", image_crop_or_error.error().string_literal()))); 691 return; 692 } 693 editor->did_complete_action("Crop Image to Content"sv); 694 }))); 695 696 m_layer_menu = TRY(window.try_add_menu("&Layer")); 697 698 m_layer_menu->on_visibility_change = [this](bool visible) { 699 if (!visible) 700 return; 701 702 auto* editor = current_image_editor(); 703 bool image_has_selection = editor && editor->active_layer() && !editor->active_layer()->image().selection().is_empty(); 704 705 m_layer_via_copy->set_enabled(image_has_selection); 706 m_layer_via_cut->set_enabled(image_has_selection); 707 }; 708 709 TRY(m_layer_menu->try_add_action(GUI::Action::create( 710 "New &Layer...", { Mod_Ctrl | Mod_Shift, Key_N }, g_icon_bag.new_layer, [&](auto&) { 711 auto* editor = current_image_editor(); 712 VERIFY(editor); 713 auto dialog = PixelPaint::CreateNewLayerDialog::construct(editor->image().size(), &window); 714 if (dialog->exec() == GUI::Dialog::ExecResult::OK) { 715 auto layer_or_error = PixelPaint::Layer::create_with_size(editor->image(), dialog->layer_size(), dialog->layer_name()); 716 if (layer_or_error.is_error()) { 717 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Unable to create layer with size {}", dialog->size())); 718 return; 719 } 720 editor->image().add_layer(layer_or_error.release_value()); 721 editor->layers_did_change(); 722 editor->did_complete_action("New Layer"sv); 723 m_layer_list_widget->select_top_layer(); 724 } 725 }))); 726 727 m_layer_via_copy = GUI::Action::create( 728 "Layer via Copy", { Mod_Ctrl | Mod_Shift, Key_C }, g_icon_bag.new_layer, [&](auto&) { 729 auto add_layer_success = current_image_editor()->add_new_layer_from_selection(); 730 if (add_layer_success.is_error()) { 731 GUI::MessageBox::show_error(&window, add_layer_success.release_error().string_literal()); 732 return; 733 } 734 current_image_editor()->did_complete_action("New Layer via Copy"sv); 735 m_layer_list_widget->select_top_layer(); 736 }); 737 TRY(m_layer_menu->try_add_action(*m_layer_via_copy)); 738 739 m_layer_via_cut = GUI::Action::create( 740 "Layer via Cut", { Mod_Ctrl | Mod_Shift, Key_X }, g_icon_bag.new_layer, [&](auto&) { 741 auto add_layer_success = current_image_editor()->add_new_layer_from_selection(); 742 if (add_layer_success.is_error()) { 743 GUI::MessageBox::show_error(&window, add_layer_success.release_error().string_literal()); 744 return; 745 } 746 current_image_editor()->active_layer()->erase_selection(current_image_editor()->image().selection()); 747 current_image_editor()->did_complete_action("New Layer via Cut"sv); 748 m_layer_list_widget->select_top_layer(); 749 }); 750 TRY(m_layer_menu->try_add_action(*m_layer_via_cut)); 751 752 TRY(m_layer_menu->try_add_separator()); 753 754 auto create_layer_mask_callback = [&](auto const& action_name, Function<void(Layer*)> mask_function) { 755 return [&, mask_function = move(mask_function)](GUI::Action&) { 756 auto* editor = current_image_editor(); 757 VERIFY(editor); 758 auto* active_layer = editor->active_layer(); 759 if (!active_layer) 760 return; 761 762 mask_function(active_layer); 763 764 editor->did_complete_action(action_name); 765 editor->update(); 766 m_layer_list_widget->repaint(); 767 set_mask_actions_for_layer(active_layer); 768 }; 769 }; 770 771 m_add_mask_action = GUI::Action::create( 772 "Add M&ask", { Mod_Ctrl | Mod_Shift, Key_M }, g_icon_bag.add_mask, create_layer_mask_callback("Add Mask", [&](Layer* active_layer) { 773 VERIFY(!active_layer->is_masked()); 774 if (auto maybe_error = active_layer->create_mask(); maybe_error.is_error()) 775 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to create layer mask: {}", maybe_error.release_error())); 776 })); 777 TRY(m_layer_menu->try_add_action(*m_add_mask_action)); 778 779 m_delete_mask_action = GUI::Action::create( 780 "Delete Mask", create_layer_mask_callback("Delete Mask", [&](Layer* active_layer) { 781 VERIFY(active_layer->is_masked()); 782 active_layer->delete_mask(); 783 })); 784 TRY(m_layer_menu->try_add_action(*m_delete_mask_action)); 785 786 m_apply_mask_action = GUI::Action::create( 787 "Apply Mask", create_layer_mask_callback("Apply Mask", [&](Layer* active_layer) { 788 VERIFY(active_layer->is_masked()); 789 active_layer->apply_mask(); 790 })); 791 TRY(m_layer_menu->try_add_action(*m_apply_mask_action)); 792 793 TRY(m_layer_menu->try_add_separator()); 794 795 TRY(m_layer_menu->try_add_action(GUI::Action::create( 796 "Select &Previous Layer", { 0, Key_PageUp }, g_icon_bag.previous_layer, [&](auto&) { 797 m_layer_list_widget->cycle_through_selection(1); 798 }))); 799 TRY(m_layer_menu->try_add_action(GUI::Action::create( 800 "Select &Next Layer", { 0, Key_PageDown }, g_icon_bag.next_layer, [&](auto&) { 801 m_layer_list_widget->cycle_through_selection(-1); 802 }))); 803 TRY(m_layer_menu->try_add_action(GUI::Action::create( 804 "Select &Top Layer", { 0, Key_Home }, g_icon_bag.top_layer, [&](auto&) { 805 m_layer_list_widget->select_top_layer(); 806 }))); 807 TRY(m_layer_menu->try_add_action(GUI::Action::create( 808 "Select B&ottom Layer", { 0, Key_End }, g_icon_bag.bottom_layer, [&](auto&) { 809 m_layer_list_widget->select_bottom_layer(); 810 }))); 811 TRY(m_layer_menu->try_add_separator()); 812 TRY(m_layer_menu->try_add_action(GUI::CommonActions::make_move_to_front_action( 813 [&](auto&) { 814 auto* editor = current_image_editor(); 815 VERIFY(editor); 816 auto active_layer = editor->active_layer(); 817 if (!active_layer) 818 return; 819 editor->image().move_layer_to_front(*active_layer); 820 editor->layers_did_change(); 821 }))); 822 TRY(m_layer_menu->try_add_action(GUI::CommonActions::make_move_to_back_action( 823 [&](auto&) { 824 auto* editor = current_image_editor(); 825 VERIFY(editor); 826 auto active_layer = editor->active_layer(); 827 if (!active_layer) 828 return; 829 editor->image().move_layer_to_back(*active_layer); 830 editor->layers_did_change(); 831 }))); 832 TRY(m_layer_menu->try_add_separator()); 833 TRY(m_layer_menu->try_add_action(GUI::Action::create( 834 "Move Active Layer &Up", { Mod_Ctrl, Key_PageUp }, g_icon_bag.active_layer_up, [&](auto&) { 835 auto* editor = current_image_editor(); 836 VERIFY(editor); 837 auto active_layer = editor->active_layer(); 838 if (!active_layer) 839 return; 840 editor->image().move_layer_up(*active_layer); 841 }))); 842 TRY(m_layer_menu->try_add_action(GUI::Action::create( 843 "Move Active Layer &Down", { Mod_Ctrl, Key_PageDown }, g_icon_bag.active_layer_down, [&](auto&) { 844 auto* editor = current_image_editor(); 845 VERIFY(editor); 846 auto active_layer = editor->active_layer(); 847 if (!active_layer) 848 return; 849 editor->image().move_layer_down(*active_layer); 850 }))); 851 TRY(m_layer_menu->try_add_separator()); 852 TRY(m_layer_menu->try_add_action(GUI::Action::create( 853 "&Remove Active Layer", { Mod_Ctrl, Key_D }, g_icon_bag.delete_layer, [&](auto&) { 854 auto* editor = current_image_editor(); 855 VERIFY(editor); 856 auto active_layer = editor->active_layer(); 857 if (!active_layer) 858 return; 859 860 auto active_layer_index = editor->image().index_of(*active_layer); 861 editor->image().remove_layer(*active_layer); 862 863 if (editor->image().layer_count()) { 864 auto& next_active_layer = editor->image().layer(active_layer_index > 0 ? active_layer_index - 1 : 0); 865 editor->set_active_layer(&next_active_layer); 866 } else { 867 auto layer_result = PixelPaint::Layer::create_with_size(editor->image(), editor->image().size(), "Background"); 868 if (layer_result.is_error()) { 869 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to create layer with size {}, error: {}", editor->image().size(), layer_result.error())); 870 return; 871 } 872 auto layer = layer_result.release_value(); 873 editor->image().add_layer(move(layer)); 874 editor->layers_did_change(); 875 m_layer_list_widget->select_top_layer(); 876 } 877 }))); 878 879 m_layer_list_widget->on_context_menu_request = [&](auto& event) { 880 m_layer_menu->popup(event.screen_position()); 881 }; 882 TRY(m_layer_menu->try_add_separator()); 883 TRY(m_layer_menu->try_add_action(GUI::Action::create( 884 "Fl&atten Image", { Mod_Ctrl, Key_F }, g_icon_bag.flatten_image, [&](auto&) { 885 auto* editor = current_image_editor(); 886 VERIFY(editor); 887 if (auto maybe_error = editor->image().flatten_all_layers(); maybe_error.is_error()) { 888 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to flatten all layers: {}", maybe_error.release_error())); 889 return; 890 } 891 editor->did_complete_action("Flatten Image"sv); 892 }))); 893 894 TRY(m_layer_menu->try_add_action(GUI::Action::create( 895 "&Merge Visible", { Mod_Ctrl, Key_M }, g_icon_bag.merge_visible, [&](auto&) { 896 auto* editor = current_image_editor(); 897 VERIFY(editor); 898 if (auto maybe_error = editor->image().merge_visible_layers(); maybe_error.is_error()) { 899 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to merge visible layers: {}", maybe_error.release_error())); 900 return; 901 } 902 editor->did_complete_action("Merge Visible"sv); 903 }))); 904 905 TRY(m_layer_menu->try_add_action(GUI::Action::create( 906 "Merge &Active Layer Up", g_icon_bag.merge_active_layer_up, [&](auto&) { 907 auto* editor = current_image_editor(); 908 VERIFY(editor); 909 auto active_layer = editor->active_layer(); 910 if (!active_layer) 911 return; 912 913 if (auto maybe_error = editor->image().merge_active_layer_up(*active_layer); maybe_error.is_error()) { 914 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to merge active layer up: {}", maybe_error.release_error())); 915 return; 916 } 917 editor->did_complete_action("Merge Active Layer Up"sv); 918 }))); 919 920 TRY(m_layer_menu->try_add_action(GUI::Action::create( 921 "M&erge Active Layer Down", { Mod_Ctrl, Key_E }, g_icon_bag.merge_active_layer_down, [&](auto&) { 922 auto* editor = current_image_editor(); 923 VERIFY(editor); 924 auto active_layer = editor->active_layer(); 925 if (!active_layer) 926 return; 927 928 if (auto maybe_error = editor->image().merge_active_layer_down(*active_layer); maybe_error.is_error()) { 929 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Failed to merge active layer down: {}", maybe_error.release_error())); 930 return; 931 } 932 editor->did_complete_action("Merge Active Layer Down"sv); 933 }))); 934 935 TRY(m_layer_menu->try_add_separator()); 936 TRY(m_layer_menu->try_add_action(GUI::Action::create( 937 "Flip Layer &Vertically", g_icon_bag.edit_flip_vertical, [&](auto&) { 938 auto* editor = current_image_editor(); 939 VERIFY(editor); 940 auto active_layer = editor->active_layer(); 941 if (!active_layer) 942 return; 943 auto layer_flip_or_error = active_layer->flip(Gfx::Orientation::Vertical); 944 if (layer_flip_or_error.is_error()) { 945 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to flip layer: {}", layer_flip_or_error.error().string_literal()))); 946 return; 947 } 948 editor->did_complete_action("Flip Layer Vertically"sv); 949 }))); 950 TRY(m_layer_menu->try_add_action(GUI::Action::create( 951 "Flip Layer &Horizontally", g_icon_bag.edit_flip_horizontal, [&](auto&) { 952 auto* editor = current_image_editor(); 953 VERIFY(editor); 954 auto active_layer = editor->active_layer(); 955 if (!active_layer) 956 return; 957 auto layer_flip_or_error = active_layer->flip(Gfx::Orientation::Horizontal); 958 if (layer_flip_or_error.is_error()) { 959 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to flip layer: {}", layer_flip_or_error.error().string_literal()))); 960 return; 961 } 962 editor->did_complete_action("Flip Layer Horizontally"sv); 963 }))); 964 TRY(m_layer_menu->try_add_separator()); 965 966 TRY(m_layer_menu->try_add_action(GUI::Action::create("Rotate Layer &Counterclockwise", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-rotate-ccw.png"sv)), 967 [&](auto&) { 968 auto* editor = current_image_editor(); 969 VERIFY(editor); 970 auto active_layer = editor->active_layer(); 971 if (!active_layer) 972 return; 973 auto layer_rotate_or_error = active_layer->rotate(Gfx::RotationDirection::CounterClockwise); 974 if (layer_rotate_or_error.is_error()) { 975 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to rotate layer: {}", layer_rotate_or_error.error().string_literal()))); 976 return; 977 } 978 editor->did_complete_action("Rotate Layer Counterclockwise"sv); 979 }))); 980 981 TRY(m_layer_menu->try_add_action(GUI::Action::create("Rotate Layer Clock&wise", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-rotate-cw.png"sv)), 982 [&](auto&) { 983 auto* editor = current_image_editor(); 984 VERIFY(editor); 985 auto active_layer = editor->active_layer(); 986 if (!active_layer) 987 return; 988 auto layer_rotate_or_error = active_layer->rotate(Gfx::RotationDirection::Clockwise); 989 if (layer_rotate_or_error.is_error()) { 990 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to rotate layer: {}", layer_rotate_or_error.error().string_literal()))); 991 return; 992 } 993 editor->did_complete_action("Rotate Layer Clockwise"sv); 994 }))); 995 996 TRY(m_layer_menu->try_add_separator()); 997 TRY(m_layer_menu->try_add_action(GUI::Action::create( 998 "&Crop Layer to Selection", g_icon_bag.crop, [&](auto&) { 999 auto* editor = current_image_editor(); 1000 VERIFY(editor); 1001 // FIXME: disable this action if there is no selection 1002 auto active_layer = editor->active_layer(); 1003 if (!active_layer || editor->image().selection().is_empty()) 1004 return; 1005 auto intersection = editor->image().rect().intersected(editor->image().selection().bounding_rect()); 1006 auto crop_rect = intersection.translated(-active_layer->location()); 1007 auto layer_crop_or_error = active_layer->crop(crop_rect); 1008 if (layer_crop_or_error.is_error()) { 1009 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to crop layer: {}", layer_crop_or_error.error().string_literal()))); 1010 return; 1011 } 1012 active_layer->set_location(intersection.location()); 1013 editor->image().selection().clear(); 1014 editor->did_complete_action("Crop Layer to Selection"sv); 1015 }))); 1016 TRY(m_layer_menu->try_add_action(GUI::Action::create( 1017 "&Crop Layer to Content", g_icon_bag.crop, [&](auto&) { 1018 auto* editor = current_image_editor(); 1019 VERIFY(editor); 1020 auto active_layer = editor->active_layer(); 1021 if (!active_layer) 1022 return; 1023 auto content_bounding_rect = active_layer->nonempty_content_bounding_rect(); 1024 if (!content_bounding_rect.has_value()) 1025 return; 1026 auto layer_crop_or_error = active_layer->crop(content_bounding_rect.value()); 1027 if (layer_crop_or_error.is_error()) { 1028 GUI::MessageBox::show_error(&window, MUST(String::formatted("Failed to crop layer: {}", layer_crop_or_error.error().string_literal()))); 1029 return; 1030 } 1031 active_layer->set_location(content_bounding_rect->location()); 1032 editor->did_complete_action("Crop Layer to Content"sv); 1033 }))); 1034 1035 m_filter_menu = TRY(window.try_add_menu("&Filter")); 1036 1037 TRY(m_filter_menu->try_add_action(GUI::Action::create("Filter &Gallery", g_icon_bag.filter, [&](auto&) { 1038 auto* editor = current_image_editor(); 1039 VERIFY(editor); 1040 auto dialog = PixelPaint::FilterGallery::construct(&window, editor); 1041 if (dialog->exec() != GUI::Dialog::ExecResult::OK) 1042 return; 1043 }))); 1044 1045 TRY(m_filter_menu->try_add_separator()); 1046 TRY(m_filter_menu->try_add_action(GUI::Action::create("Generic 5x5 &Convolution", g_icon_bag.generic_5x5_convolution, [&](auto&) { 1047 auto* editor = current_image_editor(); 1048 VERIFY(editor); 1049 if (auto* layer = editor->active_layer()) { 1050 Gfx::GenericConvolutionFilter<5> filter; 1051 if (auto parameters = PixelPaint::FilterParameters<Gfx::GenericConvolutionFilter<5>>::get(&window)) { 1052 filter.apply(layer->content_bitmap(), layer->rect(), layer->content_bitmap(), layer->rect(), *parameters); 1053 layer->did_modify_bitmap(layer->rect()); 1054 editor->did_complete_action("Generic 5x5 Convolution"sv); 1055 } 1056 } 1057 }))); 1058 1059 auto help_menu = TRY(window.try_add_menu("&Help")); 1060 TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(&window))); 1061 TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Pixel Paint", GUI::Icon::default_icon("app-pixel-paint"sv), &window))); 1062 1063 m_levels_dialog_action = GUI::Action::create( 1064 "Change &Levels...", { Mod_Ctrl, Key_L }, g_icon_bag.levels, [&](auto&) { 1065 auto* editor = current_image_editor(); 1066 VERIFY(editor); 1067 auto dialog = PixelPaint::LevelsDialog::construct(&window, editor); 1068 if (dialog->exec() != GUI::Dialog::ExecResult::OK) 1069 dialog->revert_possible_changes(); 1070 }); 1071 1072 auto& toolbar = *find_descendant_of_type_named<GUI::Toolbar>("toolbar"); 1073 (void)TRY(toolbar.try_add_action(*m_new_image_action)); 1074 (void)TRY(toolbar.try_add_action(*m_open_image_action)); 1075 (void)TRY(toolbar.try_add_action(*m_save_image_action)); 1076 TRY(toolbar.try_add_separator()); 1077 (void)TRY(toolbar.try_add_action(*m_cut_action)); 1078 (void)TRY(toolbar.try_add_action(*m_copy_action)); 1079 (void)TRY(toolbar.try_add_action(*m_paste_action)); 1080 (void)TRY(toolbar.try_add_action(*m_undo_action)); 1081 (void)TRY(toolbar.try_add_action(*m_redo_action)); 1082 TRY(toolbar.try_add_separator()); 1083 (void)TRY(toolbar.try_add_action(*m_zoom_in_action)); 1084 (void)TRY(toolbar.try_add_action(*m_zoom_out_action)); 1085 (void)TRY(toolbar.try_add_action(*m_reset_zoom_action)); 1086 1087 m_zoom_combobox = TRY(toolbar.try_add<GUI::ComboBox>()); 1088 m_zoom_combobox->set_max_width(75); 1089 m_zoom_combobox->set_model(*GUI::ItemListModel<DeprecatedString>::create(s_suggested_zoom_levels)); 1090 m_zoom_combobox->on_change = [this](DeprecatedString const& value, GUI::ModelIndex const& index) { 1091 auto* editor = current_image_editor(); 1092 VERIFY(editor); 1093 1094 if (index.is_valid()) { 1095 switch (index.row()) { 1096 case s_zoom_level_fit_width: 1097 editor->fit_image_to_view(ImageEditor::FitType::Width); 1098 return; 1099 case s_zoom_level_fit_height: 1100 editor->fit_image_to_view(ImageEditor::FitType::Height); 1101 return; 1102 case s_zoom_level_fit_image: 1103 editor->fit_image_to_view(ImageEditor::FitType::Both); 1104 return; 1105 } 1106 } 1107 1108 auto zoom_level_optional = value.view().trim("%"sv, TrimMode::Right).to_int(); 1109 if (!zoom_level_optional.has_value()) { 1110 // Indicate that a parse-error occurred by resetting the text to the current state. 1111 editor->on_scale_change(editor->scale()); 1112 return; 1113 } 1114 1115 editor->set_scale(zoom_level_optional.value() * 1.0f / 100); 1116 // If the selected zoom level got clamped, or a "fit to …" level was selected, 1117 // there is a chance that the new scale is identical to the old scale. 1118 // In these cases, we need to manually reset the text: 1119 editor->on_scale_change(editor->scale()); 1120 }; 1121 m_zoom_combobox->on_return_pressed = [this]() { 1122 m_zoom_combobox->on_change(m_zoom_combobox->text(), GUI::ModelIndex()); 1123 }; 1124 1125 TRY(toolbar.try_add_separator()); 1126 (void)TRY(toolbar.try_add_action(*m_levels_dialog_action)); 1127 1128 return {}; 1129} 1130 1131void MainWidget::set_actions_enabled(bool enabled) 1132{ 1133 m_save_image_action->set_enabled(enabled); 1134 m_save_image_as_action->set_enabled(enabled); 1135 m_close_image_action->set_enabled(enabled); 1136 1137 m_export_submenu->set_children_actions_enabled(enabled); 1138 1139 m_edit_menu->set_children_actions_enabled(enabled); 1140 m_paste_action->set_enabled(true); 1141 1142 m_view_menu->set_children_actions_enabled(enabled); 1143 m_layer_menu->set_children_actions_enabled(enabled); 1144 m_image_menu->set_children_actions_enabled(enabled); 1145 m_tool_menu->set_children_actions_enabled(enabled); 1146 m_filter_menu->set_children_actions_enabled(enabled); 1147 1148 m_zoom_combobox->set_enabled(enabled); 1149 1150 m_levels_dialog_action->set_enabled(enabled); 1151} 1152 1153void MainWidget::set_mask_actions_for_layer(Layer* layer) 1154{ 1155 if (!layer) { 1156 m_add_mask_action->set_visible(true); 1157 m_delete_mask_action->set_visible(false); 1158 m_apply_mask_action->set_visible(false); 1159 m_add_mask_action->set_enabled(false); 1160 return; 1161 } 1162 1163 m_add_mask_action->set_enabled(true); 1164 1165 auto masked = layer->is_masked(); 1166 m_add_mask_action->set_visible(!masked); 1167 m_delete_mask_action->set_visible(masked); 1168 m_apply_mask_action->set_visible(masked); 1169} 1170 1171void MainWidget::open_image(FileSystemAccessClient::File file) 1172{ 1173 auto try_load = m_loader.load_from_file(file.release_stream()); 1174 if (try_load.is_error()) { 1175 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Unable to open file: {}, {}", file.filename(), try_load.error())); 1176 return; 1177 } 1178 1179 auto& image = *m_loader.release_image(); 1180 auto& editor = create_new_editor(image); 1181 editor.set_loaded_from_image(m_loader.is_raw_image()); 1182 editor.set_path(file.filename().to_deprecated_string()); 1183 editor.set_unmodified(); 1184 m_layer_list_widget->set_image(&image); 1185 GUI::Application::the()->set_most_recently_open_file(file.filename()); 1186} 1187 1188ErrorOr<void> MainWidget::create_default_image() 1189{ 1190 auto image = TRY(Image::create_with_size({ 510, 356 })); 1191 1192 auto bg_layer = TRY(Layer::create_with_size(*image, image->size(), "Background")); 1193 image->add_layer(*bg_layer); 1194 bg_layer->content_bitmap().fill(Color::Transparent); 1195 1196 m_layer_list_widget->set_image(image); 1197 1198 auto& editor = create_new_editor(*image); 1199 editor.set_title("Untitled"); 1200 editor.set_active_layer(bg_layer); 1201 editor.set_unmodified(); 1202 1203 return {}; 1204} 1205 1206ErrorOr<void> MainWidget::create_image_from_clipboard() 1207{ 1208 auto bitmap = GUI::Clipboard::the().fetch_data_and_type().as_bitmap(); 1209 if (!bitmap) { 1210 return Error::from_string_view("There is no image in a clipboard to paste."sv); 1211 } 1212 1213 auto image = TRY(PixelPaint::Image::create_with_size(bitmap->size())); 1214 auto layer = TRY(PixelPaint::Layer::create_with_bitmap(image, *bitmap, "Pasted layer")); 1215 image->add_layer(*layer); 1216 1217 auto& editor = create_new_editor(*image); 1218 editor.set_title("Untitled"); 1219 1220 m_layer_list_widget->set_image(image); 1221 m_layer_list_widget->set_selected_layer(layer); 1222 set_mask_actions_for_layer(layer); 1223 return {}; 1224} 1225 1226bool MainWidget::request_close() 1227{ 1228 while (!m_tab_widget->children().is_empty()) { 1229 auto* editor = current_image_editor(); 1230 VERIFY(editor); 1231 if (!editor->request_close()) 1232 return false; 1233 m_tab_widget->remove_tab(*m_tab_widget->active_widget()); 1234 } 1235 return true; 1236} 1237 1238ImageEditor* MainWidget::current_image_editor() 1239{ 1240 if (!m_tab_widget->active_widget()) 1241 return nullptr; 1242 return verify_cast<PixelPaint::ImageEditor>(m_tab_widget->active_widget()); 1243} 1244 1245ImageEditor& MainWidget::create_new_editor(NonnullRefPtr<Image> image) 1246{ 1247 auto& image_editor = m_tab_widget->add_tab<PixelPaint::ImageEditor>("Untitled", image); 1248 1249 image_editor.on_active_layer_change = [&](auto* layer) { 1250 if (current_image_editor() != &image_editor) 1251 return; 1252 m_layer_list_widget->set_selected_layer(layer); 1253 m_layer_properties_widget->set_layer(layer); 1254 set_mask_actions_for_layer(layer); 1255 }; 1256 1257 image_editor.on_title_change = [&](auto const& title) { 1258 m_tab_widget->set_tab_title(image_editor, title); 1259 }; 1260 1261 image_editor.on_modified_change = Core::debounce([&](auto const modified) { 1262 m_tab_widget->set_tab_modified(image_editor, modified); 1263 update_window_modified(); 1264 m_histogram_widget->image_changed(); 1265 m_vectorscope_widget->image_changed(); 1266 }, 1267 100); 1268 1269 image_editor.on_image_mouse_position_change = [&](auto const& mouse_position) { 1270 auto const& image_size = current_image_editor()->image().size(); 1271 auto image_rectangle = Gfx::IntRect { 0, 0, image_size.width(), image_size.height() }; 1272 if (image_rectangle.contains(mouse_position)) { 1273 update_status_bar(current_image_editor()->appended_status_info()); 1274 m_histogram_widget->set_color_at_mouseposition(current_image_editor()->image().color_at(mouse_position)); 1275 m_vectorscope_widget->set_color_at_mouseposition(current_image_editor()->image().color_at(mouse_position)); 1276 } else { 1277 m_statusbar->set_override_text({}); 1278 m_histogram_widget->set_color_at_mouseposition(Color::Transparent); 1279 m_vectorscope_widget->set_color_at_mouseposition(Color::Transparent); 1280 } 1281 m_last_image_editor_mouse_position = mouse_position; 1282 }; 1283 1284 image_editor.on_appended_status_info_change = [&](auto const& appended_status_info) { 1285 update_status_bar(appended_status_info); 1286 }; 1287 1288 image_editor.on_leave = [&]() { 1289 m_statusbar->set_override_text({}); 1290 m_histogram_widget->set_color_at_mouseposition(Color::Transparent); 1291 m_vectorscope_widget->set_color_at_mouseposition(Color::Transparent); 1292 }; 1293 1294 image_editor.on_set_guide_visibility = [&](bool show_guides) { 1295 m_show_guides_action->set_checked(show_guides); 1296 }; 1297 1298 image_editor.on_set_ruler_visibility = [&](bool show_rulers) { 1299 m_show_rulers_action->set_checked(show_rulers); 1300 }; 1301 1302 image_editor.on_scale_change = Core::debounce([this](float scale) { 1303 m_zoom_combobox->set_text(DeprecatedString::formatted("{}%", roundf(scale * 100))); 1304 current_image_editor()->update_tool_cursor(); 1305 }, 1306 100); 1307 1308 image_editor.on_primary_color_change = [&](Color color) { 1309 m_palette_widget->set_primary_color(color); 1310 if (image_editor.active_tool()) 1311 image_editor.active_tool()->on_primary_color_change(color); 1312 }; 1313 image_editor.on_secondary_color_change = [&](Color color) { 1314 m_palette_widget->set_secondary_color(color); 1315 if (image_editor.active_tool()) 1316 image_editor.active_tool()->on_secondary_color_change(color); 1317 }; 1318 1319 if (image->layer_count()) 1320 image_editor.set_active_layer(&image->layer(0)); 1321 1322 if (!m_loader.is_raw_image()) { 1323 m_loader.json_metadata().for_each([&](JsonValue const& value) { 1324 if (!value.is_object()) 1325 return; 1326 auto& json_object = value.as_object(); 1327 auto orientation_value = json_object.get_deprecated_string("orientation"sv); 1328 if (!orientation_value.has_value()) 1329 return; 1330 1331 auto offset_value = json_object.get("offset"sv); 1332 if (!offset_value.has_value() || !offset_value->is_number()) 1333 return; 1334 1335 auto orientation_string = orientation_value.value(); 1336 PixelPaint::Guide::Orientation orientation; 1337 if (orientation_string == "horizontal"sv) 1338 orientation = PixelPaint::Guide::Orientation::Horizontal; 1339 else if (orientation_string == "vertical"sv) 1340 orientation = PixelPaint::Guide::Orientation::Vertical; 1341 else 1342 return; 1343 1344 image_editor.add_guide(PixelPaint::Guide::construct(orientation, offset_value->to_number<float>())); 1345 }); 1346 } 1347 1348 m_tab_widget->set_active_widget(&image_editor); 1349 image_editor.set_focus(true); 1350 image_editor.fit_image_to_view(); 1351 m_tool_properties_widget->set_enabled(true); 1352 set_actions_enabled(true); 1353 1354 return image_editor; 1355} 1356 1357void MainWidget::drag_enter_event(GUI::DragEvent& event) 1358{ 1359 auto const& mime_types = event.mime_types(); 1360 if (mime_types.contains_slow("text/uri-list")) 1361 event.accept(); 1362} 1363 1364void MainWidget::drop_event(GUI::DropEvent& event) 1365{ 1366 if (!event.mime_data().has_urls()) 1367 return; 1368 1369 event.accept(); 1370 1371 if (event.mime_data().urls().is_empty()) 1372 return; 1373 1374 for (auto& url : event.mime_data().urls()) { 1375 if (url.scheme() != "file") 1376 continue; 1377 1378 auto response = FileSystemAccessClient::Client::the().request_file(window(), url.path(), Core::File::OpenMode::Read); 1379 if (response.is_error()) 1380 return; 1381 open_image(response.release_value()); 1382 } 1383} 1384 1385void MainWidget::update_window_modified() 1386{ 1387 window()->set_modified(m_tab_widget->is_any_tab_modified()); 1388} 1389void MainWidget::update_status_bar(DeprecatedString appended_text) 1390{ 1391 StringBuilder builder = StringBuilder(); 1392 builder.append(m_last_image_editor_mouse_position.to_deprecated_string()); 1393 if (!appended_text.is_empty()) { 1394 builder.append(" "sv); 1395 builder.append(appended_text); 1396 } 1397 m_statusbar->set_override_text(builder.to_deprecated_string()); 1398} 1399 1400}