Serenity Operating System
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}