MainWidget.cpp 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165
  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. #include "MainWidget.h"
  10. #include "CreateNewImageDialog.h"
  11. #include "CreateNewLayerDialog.h"
  12. #include "EditGuideDialog.h"
  13. #include "FilterGallery.h"
  14. #include "FilterParams.h"
  15. #include "LevelsDialog.h"
  16. #include "ResizeImageDialog.h"
  17. #include <Applications/PixelPaint/PixelPaintWindowGML.h>
  18. #include <LibConfig/Client.h>
  19. #include <LibCore/File.h>
  20. #include <LibCore/MimeData.h>
  21. #include <LibFileSystemAccessClient/Client.h>
  22. #include <LibGUI/Application.h>
  23. #include <LibGUI/Clipboard.h>
  24. #include <LibGUI/Icon.h>
  25. #include <LibGUI/ItemListModel.h>
  26. #include <LibGUI/Menubar.h>
  27. #include <LibGUI/MessageBox.h>
  28. #include <LibGUI/Toolbar.h>
  29. #include <LibGUI/Window.h>
  30. #include <LibGfx/Rect.h>
  31. namespace PixelPaint {
  32. IconBag g_icon_bag;
  33. MainWidget::MainWidget()
  34. : Widget()
  35. {
  36. load_from_gml(pixel_paint_window_gml);
  37. m_toolbox = find_descendant_of_type_named<PixelPaint::ToolboxWidget>("toolbox");
  38. m_statusbar = *find_descendant_of_type_named<GUI::Statusbar>("statusbar");
  39. m_tab_widget = find_descendant_of_type_named<GUI::TabWidget>("tab_widget");
  40. m_palette_widget = *find_descendant_of_type_named<PixelPaint::PaletteWidget>("palette_widget");
  41. m_histogram_widget = *find_descendant_of_type_named<PixelPaint::HistogramWidget>("histogram_widget");
  42. m_vectorscope_widget = *find_descendant_of_type_named<PixelPaint::VectorscopeWidget>("vectorscope_widget");
  43. m_layer_list_widget = *find_descendant_of_type_named<PixelPaint::LayerListWidget>("layer_list_widget");
  44. m_layer_list_widget->on_layer_select = [&](auto* layer) {
  45. auto* editor = current_image_editor();
  46. VERIFY(editor);
  47. editor->set_active_layer(layer);
  48. };
  49. m_layer_properties_widget = *find_descendant_of_type_named<PixelPaint::LayerPropertiesWidget>("layer_properties_widget");
  50. m_tool_properties_widget = *find_descendant_of_type_named<PixelPaint::ToolPropertiesWidget>("tool_properties_widget");
  51. m_toolbox->on_tool_selection = [&](auto* tool) {
  52. auto* editor = current_image_editor();
  53. VERIFY(editor);
  54. editor->set_active_tool(tool);
  55. m_tool_properties_widget->set_active_tool(tool);
  56. };
  57. m_tab_widget->on_middle_click = [&](auto& widget) {
  58. m_tab_widget->on_tab_close_click(widget);
  59. };
  60. m_tab_widget->on_tab_close_click = [&](auto& widget) {
  61. auto& image_editor = verify_cast<PixelPaint::ImageEditor>(widget);
  62. if (image_editor.request_close()) {
  63. m_tab_widget->deferred_invoke([&] {
  64. m_tab_widget->remove_tab(image_editor);
  65. if (m_tab_widget->children().size() == 0) {
  66. m_histogram_widget->set_image(nullptr);
  67. m_vectorscope_widget->set_image(nullptr);
  68. m_layer_list_widget->set_image(nullptr);
  69. m_layer_properties_widget->set_layer(nullptr);
  70. m_palette_widget->set_image_editor(nullptr);
  71. m_tool_properties_widget->set_enabled(false);
  72. set_actions_enabled(false);
  73. }
  74. });
  75. }
  76. };
  77. m_tab_widget->on_change = [&](auto& widget) {
  78. auto& image_editor = verify_cast<PixelPaint::ImageEditor>(widget);
  79. m_palette_widget->set_image_editor(&image_editor);
  80. m_histogram_widget->set_image(&image_editor.image());
  81. m_vectorscope_widget->set_image(&image_editor.image());
  82. m_layer_list_widget->set_image(&image_editor.image());
  83. m_layer_properties_widget->set_layer(image_editor.active_layer());
  84. window()->set_modified(image_editor.is_modified());
  85. image_editor.on_modified_change = [this](bool modified) {
  86. window()->set_modified(modified);
  87. m_histogram_widget->image_changed();
  88. m_vectorscope_widget->image_changed();
  89. };
  90. if (auto* active_tool = m_toolbox->active_tool())
  91. image_editor.set_active_tool(active_tool);
  92. m_show_guides_action->set_checked(image_editor.guide_visibility());
  93. m_show_rulers_action->set_checked(image_editor.ruler_visibility());
  94. image_editor.on_scale_change(image_editor.scale());
  95. image_editor.undo_stack().on_state_change = [this] {
  96. image_editor_did_update_undo_stack();
  97. };
  98. // Ensure that our undo/redo actions are in sync with the current editor.
  99. image_editor_did_update_undo_stack();
  100. };
  101. }
  102. void MainWidget::image_editor_did_update_undo_stack()
  103. {
  104. auto* image_editor = current_image_editor();
  105. if (!image_editor) {
  106. m_undo_action->set_enabled(false);
  107. m_redo_action->set_enabled(false);
  108. return;
  109. }
  110. auto make_action_text = [](auto prefix, auto suffix) {
  111. StringBuilder builder;
  112. builder.append(prefix);
  113. if (suffix.has_value()) {
  114. builder.append(' ');
  115. builder.append(suffix.value());
  116. }
  117. return builder.to_string();
  118. };
  119. auto& undo_stack = image_editor->undo_stack();
  120. m_undo_action->set_enabled(undo_stack.can_undo());
  121. m_redo_action->set_enabled(undo_stack.can_redo());
  122. m_undo_action->set_text(make_action_text("&Undo"sv, undo_stack.undo_action_text()));
  123. m_redo_action->set_text(make_action_text("&Redo"sv, undo_stack.redo_action_text()));
  124. }
  125. // Note: Update these together! v
  126. static Vector<String> const s_suggested_zoom_levels { "25%", "50%", "100%", "200%", "300%", "400%", "800%", "1600%", "Fit to width", "Fit to height", "Fit entire image" };
  127. static constexpr int s_zoom_level_fit_width = 8;
  128. static constexpr int s_zoom_level_fit_height = 9;
  129. static constexpr int s_zoom_level_fit_image = 10;
  130. // Note: Update these together! ^
  131. void MainWidget::initialize_menubar(GUI::Window& window)
  132. {
  133. auto& file_menu = window.add_menu("&File");
  134. m_new_image_action = GUI::Action::create(
  135. "&New Image...", { Mod_Ctrl, Key_N }, g_icon_bag.filetype_pixelpaint, [&](auto&) {
  136. auto dialog = PixelPaint::CreateNewImageDialog::construct(&window);
  137. if (dialog->exec() == GUI::Dialog::ExecResult::OK) {
  138. auto image = PixelPaint::Image::try_create_with_size(dialog->image_size()).release_value_but_fixme_should_propagate_errors();
  139. auto bg_layer = PixelPaint::Layer::try_create_with_size(*image, image->size(), "Background").release_value_but_fixme_should_propagate_errors();
  140. image->add_layer(*bg_layer);
  141. bg_layer->content_bitmap().fill(Color::White);
  142. auto& editor = create_new_editor(*image);
  143. auto image_title = dialog->image_name().trim_whitespace();
  144. editor.set_title(image_title.is_empty() ? "Untitled" : image_title);
  145. editor.undo_stack().set_current_unmodified();
  146. m_histogram_widget->set_image(image);
  147. m_vectorscope_widget->set_image(image);
  148. m_layer_list_widget->set_image(image);
  149. m_layer_list_widget->set_selected_layer(bg_layer);
  150. }
  151. });
  152. m_new_image_from_clipboard_action = GUI::Action::create(
  153. "&New Image from Clipboard", { Mod_Ctrl | Mod_Shift, Key_V }, g_icon_bag.new_clipboard, [&](auto&) {
  154. create_image_from_clipboard();
  155. });
  156. m_open_image_action = GUI::CommonActions::make_open_action([&](auto&) {
  157. auto response = FileSystemAccessClient::Client::the().try_open_file(&window);
  158. if (response.is_error())
  159. return;
  160. open_image(response.value());
  161. });
  162. m_save_image_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
  163. auto* editor = current_image_editor();
  164. VERIFY(editor);
  165. editor->save_project_as();
  166. });
  167. m_save_image_action = GUI::CommonActions::make_save_action([&](auto&) {
  168. auto* editor = current_image_editor();
  169. VERIFY(editor);
  170. editor->save_project();
  171. });
  172. file_menu.add_action(*m_new_image_action);
  173. file_menu.add_action(*m_new_image_from_clipboard_action);
  174. file_menu.add_action(*m_open_image_action);
  175. file_menu.add_action(*m_save_image_action);
  176. file_menu.add_action(*m_save_image_as_action);
  177. m_export_submenu = file_menu.add_submenu("&Export");
  178. m_export_submenu->add_action(
  179. GUI::Action::create(
  180. "As &BMP", [&](auto&) {
  181. auto* editor = current_image_editor();
  182. VERIFY(editor);
  183. auto response = FileSystemAccessClient::Client::the().try_save_file(&window, editor->title(), "bmp");
  184. if (response.is_error())
  185. return;
  186. 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);
  187. auto result = editor->image().export_bmp_to_file(response.value(), preserve_alpha_channel == GUI::MessageBox::ExecResult::Yes);
  188. if (result.is_error())
  189. GUI::MessageBox::show_error(&window, String::formatted("Export to BMP failed: {}", result.error()));
  190. }));
  191. m_export_submenu->add_action(
  192. GUI::Action::create(
  193. "As &PNG", [&](auto&) {
  194. auto* editor = current_image_editor();
  195. VERIFY(editor);
  196. // TODO: fix bmp on line below?
  197. auto response = FileSystemAccessClient::Client::the().try_save_file(&window, editor->title(), "png");
  198. if (response.is_error())
  199. return;
  200. 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);
  201. auto result = editor->image().export_png_to_file(response.value(), preserve_alpha_channel == GUI::MessageBox::ExecResult::Yes);
  202. if (result.is_error())
  203. GUI::MessageBox::show_error(&window, String::formatted("Export to PNG failed: {}", result.error()));
  204. }));
  205. m_export_submenu->add_action(
  206. GUI::Action::create(
  207. "As &QOI", [&](auto&) {
  208. auto* editor = current_image_editor();
  209. VERIFY(editor);
  210. auto response = FileSystemAccessClient::Client::the().try_save_file(&window, editor->title(), "qoi");
  211. if (response.is_error())
  212. return;
  213. auto result = editor->image().export_qoi_to_file(response.value());
  214. if (result.is_error())
  215. GUI::MessageBox::show_error(&window, String::formatted("Export to QOI failed: {}", result.error()));
  216. }));
  217. m_export_submenu->set_icon(g_icon_bag.file_export);
  218. file_menu.add_separator();
  219. m_close_image_action = GUI::Action::create("&Close Image", { Mod_Ctrl, Key_W }, g_icon_bag.close_image, [&](auto&) {
  220. auto* active_widget = m_tab_widget->active_widget();
  221. VERIFY(active_widget);
  222. m_tab_widget->on_tab_close_click(*active_widget);
  223. });
  224. file_menu.add_action(*m_close_image_action);
  225. file_menu.add_action(GUI::CommonActions::make_quit_action([this](auto&) {
  226. if (request_close())
  227. GUI::Application::the()->quit();
  228. }));
  229. m_edit_menu = window.add_menu("&Edit");
  230. m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) {
  231. auto* editor = current_image_editor();
  232. VERIFY(editor);
  233. if (!editor->active_layer()) {
  234. dbgln("Cannot cut with no active layer selected");
  235. return;
  236. }
  237. auto bitmap = editor->active_layer()->try_copy_bitmap(editor->image().selection());
  238. if (!bitmap) {
  239. dbgln("try_copy_bitmap() from Layer failed");
  240. return;
  241. }
  242. GUI::Clipboard::the().set_bitmap(*bitmap);
  243. editor->active_layer()->erase_selection(editor->image().selection());
  244. });
  245. m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) {
  246. auto* editor = current_image_editor();
  247. VERIFY(editor);
  248. if (!editor->active_layer()) {
  249. dbgln("Cannot copy with no active layer selected");
  250. return;
  251. }
  252. auto bitmap = editor->active_layer()->try_copy_bitmap(editor->image().selection());
  253. if (!bitmap) {
  254. dbgln("try_copy_bitmap() from Layer failed");
  255. return;
  256. }
  257. GUI::Clipboard::the().set_bitmap(*bitmap);
  258. });
  259. m_copy_merged_action = GUI::Action::create(
  260. "Copy &Merged", { Mod_Ctrl | Mod_Shift, Key_C }, g_icon_bag.edit_copy, [&](auto&) {
  261. auto* editor = current_image_editor();
  262. VERIFY(editor);
  263. auto bitmap = editor->image().try_copy_bitmap(editor->image().selection());
  264. if (!bitmap) {
  265. dbgln("try_copy_bitmap() from Image failed");
  266. return;
  267. }
  268. GUI::Clipboard::the().set_bitmap(*bitmap);
  269. });
  270. m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) {
  271. auto* editor = current_image_editor();
  272. if (!editor) {
  273. create_image_from_clipboard();
  274. return;
  275. }
  276. auto bitmap = GUI::Clipboard::the().fetch_data_and_type().as_bitmap();
  277. if (!bitmap)
  278. return;
  279. auto layer = PixelPaint::Layer::try_create_with_bitmap(editor->image(), *bitmap, "Pasted layer").release_value_but_fixme_should_propagate_errors();
  280. editor->image().add_layer(*layer);
  281. editor->set_active_layer(layer);
  282. editor->image().selection().clear();
  283. });
  284. GUI::Clipboard::the().on_change = [&](auto& mime_type) {
  285. m_paste_action->set_enabled(mime_type == "image/x-serenityos");
  286. };
  287. m_paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "image/x-serenityos");
  288. m_undo_action = GUI::CommonActions::make_undo_action([&](auto&) {
  289. if (auto* editor = current_image_editor())
  290. editor->undo();
  291. });
  292. m_redo_action = GUI::CommonActions::make_redo_action([&](auto&) {
  293. auto* editor = current_image_editor();
  294. VERIFY(editor);
  295. editor->redo();
  296. });
  297. m_edit_menu->add_action(*m_undo_action);
  298. m_edit_menu->add_action(*m_redo_action);
  299. m_edit_menu->add_separator();
  300. m_edit_menu->add_action(*m_cut_action);
  301. m_edit_menu->add_action(*m_copy_action);
  302. m_edit_menu->add_action(*m_copy_merged_action);
  303. m_edit_menu->add_action(*m_paste_action);
  304. m_edit_menu->add_separator();
  305. m_edit_menu->add_action(GUI::CommonActions::make_select_all_action([&](auto&) {
  306. auto* editor = current_image_editor();
  307. VERIFY(editor);
  308. if (!editor->active_layer())
  309. return;
  310. editor->image().selection().merge(editor->active_layer()->relative_rect(), PixelPaint::Selection::MergeMode::Set);
  311. }));
  312. m_edit_menu->add_action(GUI::Action::create(
  313. "Clear &Selection", g_icon_bag.clear_selection, [&](auto&) {
  314. auto* editor = current_image_editor();
  315. VERIFY(editor);
  316. editor->image().selection().clear();
  317. }));
  318. m_edit_menu->add_action(GUI::Action::create(
  319. "&Invert Selection", g_icon_bag.invert_selection, [&](auto&) {
  320. auto* editor = current_image_editor();
  321. VERIFY(editor);
  322. editor->image().selection().invert();
  323. }));
  324. m_edit_menu->add_separator();
  325. m_edit_menu->add_action(GUI::Action::create(
  326. "S&wap Colors", { Mod_None, Key_X }, g_icon_bag.swap_colors, [&](auto&) {
  327. auto* editor = current_image_editor();
  328. VERIFY(editor);
  329. auto old_primary_color = editor->primary_color();
  330. editor->set_primary_color(editor->secondary_color());
  331. editor->set_secondary_color(old_primary_color);
  332. }));
  333. m_edit_menu->add_action(GUI::Action::create(
  334. "&Default Colors", { Mod_None, Key_D }, g_icon_bag.default_colors, [&](auto&) {
  335. auto* editor = current_image_editor();
  336. VERIFY(editor);
  337. editor->set_primary_color(Color::Black);
  338. editor->set_secondary_color(Color::White);
  339. }));
  340. m_edit_menu->add_action(GUI::Action::create(
  341. "&Load Color Palette", g_icon_bag.load_color_palette, [&](auto&) {
  342. auto response = FileSystemAccessClient::Client::the().try_open_file(&window, "Load Color Palette");
  343. if (response.is_error())
  344. return;
  345. auto result = PixelPaint::PaletteWidget::load_palette_file(*response.value());
  346. if (result.is_error()) {
  347. GUI::MessageBox::show_error(&window, String::formatted("Loading color palette failed: {}", result.error()));
  348. return;
  349. }
  350. m_palette_widget->display_color_list(result.value());
  351. }));
  352. m_edit_menu->add_action(GUI::Action::create(
  353. "Sa&ve Color Palette", g_icon_bag.save_color_palette, [&](auto&) {
  354. auto response = FileSystemAccessClient::Client::the().try_save_file(&window, "untitled", "palette");
  355. if (response.is_error())
  356. return;
  357. auto result = PixelPaint::PaletteWidget::save_palette_file(m_palette_widget->colors(), *response.value());
  358. if (result.is_error())
  359. GUI::MessageBox::show_error(&window, String::formatted("Writing color palette failed: {}", result.error()));
  360. }));
  361. m_view_menu = window.add_menu("&View");
  362. m_zoom_in_action = GUI::CommonActions::make_zoom_in_action(
  363. [&](auto&) {
  364. auto* editor = current_image_editor();
  365. VERIFY(editor);
  366. editor->scale_by(0.1f);
  367. });
  368. m_zoom_out_action = GUI::CommonActions::make_zoom_out_action(
  369. [&](auto&) {
  370. auto* editor = current_image_editor();
  371. VERIFY(editor);
  372. editor->scale_by(-0.1f);
  373. });
  374. m_reset_zoom_action = GUI::CommonActions::make_reset_zoom_action(
  375. [&](auto&) {
  376. if (auto* editor = current_image_editor())
  377. editor->reset_view();
  378. });
  379. m_add_guide_action = GUI::Action::create(
  380. "&Add Guide", g_icon_bag.add_guide, [&](auto&) {
  381. auto dialog = PixelPaint::EditGuideDialog::construct(&window);
  382. if (dialog->exec() != GUI::Dialog::ExecResult::OK)
  383. return;
  384. auto* editor = current_image_editor();
  385. VERIFY(editor);
  386. auto offset = dialog->offset_as_pixel(*editor);
  387. if (!offset.has_value())
  388. return;
  389. editor->add_guide(PixelPaint::Guide::construct(dialog->orientation(), offset.value()));
  390. });
  391. // Save this so other methods can use it
  392. m_show_guides_action = GUI::Action::create_checkable(
  393. "Show &Guides", [&](auto& action) {
  394. Config::write_bool("PixelPaint"sv, "Guides"sv, "Show"sv, action.is_checked());
  395. auto* editor = current_image_editor();
  396. VERIFY(editor);
  397. editor->set_guide_visibility(action.is_checked());
  398. });
  399. m_show_guides_action->set_checked(Config::read_bool("PixelPaint"sv, "Guides"sv, "Show"sv, true));
  400. m_view_menu->add_action(*m_zoom_in_action);
  401. m_view_menu->add_action(*m_zoom_out_action);
  402. m_view_menu->add_action(*m_reset_zoom_action);
  403. m_view_menu->add_action(GUI::Action::create(
  404. "Fit Image To &View", g_icon_bag.fit_image_to_view, [&](auto&) {
  405. auto* editor = current_image_editor();
  406. VERIFY(editor);
  407. editor->fit_image_to_view();
  408. }));
  409. m_view_menu->add_separator();
  410. m_view_menu->add_action(*m_add_guide_action);
  411. m_view_menu->add_action(*m_show_guides_action);
  412. m_view_menu->add_action(GUI::Action::create(
  413. "&Clear Guides", g_icon_bag.clear_guides, [&](auto&) {
  414. auto* editor = current_image_editor();
  415. VERIFY(editor);
  416. editor->clear_guides();
  417. }));
  418. m_view_menu->add_separator();
  419. auto show_pixel_grid_action = GUI::Action::create_checkable(
  420. "Show &Pixel Grid", [&](auto& action) {
  421. Config::write_bool("PixelPaint"sv, "PixelGrid"sv, "Show"sv, action.is_checked());
  422. auto* editor = current_image_editor();
  423. VERIFY(editor);
  424. editor->set_pixel_grid_visibility(action.is_checked());
  425. });
  426. show_pixel_grid_action->set_checked(Config::read_bool("PixelPaint"sv, "PixelGrid"sv, "Show"sv, true));
  427. m_view_menu->add_action(*show_pixel_grid_action);
  428. m_show_rulers_action = GUI::Action::create_checkable(
  429. "Show R&ulers", { Mod_Ctrl, Key_R }, [&](auto& action) {
  430. Config::write_bool("PixelPaint"sv, "Rulers"sv, "Show"sv, action.is_checked());
  431. auto* editor = current_image_editor();
  432. VERIFY(editor);
  433. editor->set_ruler_visibility(action.is_checked());
  434. });
  435. m_show_rulers_action->set_checked(Config::read_bool("PixelPaint"sv, "Rulers"sv, "Show"sv, true));
  436. m_view_menu->add_action(*m_show_rulers_action);
  437. m_show_active_layer_boundary_action = GUI::Action::create_checkable(
  438. "Show Active Layer &Boundary", [&](auto& action) {
  439. Config::write_bool("PixelPaint"sv, "ImageEditor"sv, "ShowActiveLayerBoundary"sv, action.is_checked());
  440. auto* editor = current_image_editor();
  441. VERIFY(editor);
  442. editor->set_show_active_layer_boundary(action.is_checked());
  443. });
  444. m_show_active_layer_boundary_action->set_checked(Config::read_bool("PixelPaint"sv, "ImageEditor"sv, "ShowActiveLayerBoundary"sv, true));
  445. m_view_menu->add_action(*m_show_active_layer_boundary_action);
  446. m_view_menu->add_separator();
  447. auto histogram_action = GUI::Action::create_checkable("&Histogram", [&](auto& action) {
  448. Config::write_bool("PixelPaint"sv, "Scopes"sv, "ShowHistogram"sv, action.is_checked());
  449. m_histogram_widget->parent_widget()->set_visible(action.is_checked());
  450. });
  451. histogram_action->set_checked(Config::read_bool("PixelPaint"sv, "Scopes"sv, "ShowHistogram"sv, false));
  452. m_histogram_widget->parent_widget()->set_visible(histogram_action->is_checked());
  453. auto vectorscope_action = GUI::Action::create_checkable("&Vectorscope", [&](auto& action) {
  454. Config::write_bool("PixelPaint"sv, "Scopes"sv, "ShowVectorscope"sv, action.is_checked());
  455. m_vectorscope_widget->parent_widget()->set_visible(action.is_checked());
  456. });
  457. vectorscope_action->set_checked(Config::read_bool("PixelPaint"sv, "Scopes"sv, "ShowVectorscope"sv, false));
  458. m_vectorscope_widget->parent_widget()->set_visible(vectorscope_action->is_checked());
  459. auto& scopes_menu = m_view_menu->add_submenu("&Scopes");
  460. scopes_menu.add_action(histogram_action);
  461. scopes_menu.add_action(vectorscope_action);
  462. m_tool_menu = window.add_menu("&Tool");
  463. m_toolbox->for_each_tool([&](auto& tool) {
  464. if (tool.action())
  465. m_tool_menu->add_action(*tool.action());
  466. return IterationDecision::Continue;
  467. });
  468. m_image_menu = window.add_menu("&Image");
  469. m_image_menu->add_action(GUI::Action::create(
  470. "Flip Image &Vertically", g_icon_bag.edit_flip_vertical, [&](auto&) {
  471. auto* editor = current_image_editor();
  472. VERIFY(editor);
  473. editor->image().flip(Gfx::Orientation::Vertical);
  474. editor->did_complete_action("Flip Image Vertically"sv);
  475. }));
  476. m_image_menu->add_action(GUI::Action::create(
  477. "Flip Image &Horizontally", g_icon_bag.edit_flip_horizontal, [&](auto&) {
  478. auto* editor = current_image_editor();
  479. VERIFY(editor);
  480. editor->image().flip(Gfx::Orientation::Horizontal);
  481. editor->did_complete_action("Flip Image Horizontally"sv);
  482. }));
  483. m_image_menu->add_separator();
  484. m_image_menu->add_action(GUI::Action::create("Rotate Image &Counterclockwise", { Mod_Ctrl | Mod_Shift, Key_LessThan }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-rotate-ccw.png"sv).release_value_but_fixme_should_propagate_errors(),
  485. [&](auto&) {
  486. auto* editor = current_image_editor();
  487. VERIFY(editor);
  488. editor->image().rotate(Gfx::RotationDirection::CounterClockwise);
  489. editor->did_complete_action("Rotate Image Counterclockwise"sv);
  490. }));
  491. m_image_menu->add_action(GUI::Action::create("Rotate Image Clock&wise", { Mod_Ctrl | Mod_Shift, Key_GreaterThan }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-rotate-cw.png"sv).release_value_but_fixme_should_propagate_errors(),
  492. [&](auto&) {
  493. auto* editor = current_image_editor();
  494. VERIFY(editor);
  495. editor->image().rotate(Gfx::RotationDirection::Clockwise);
  496. editor->did_complete_action("Rotate Image Clockwise"sv);
  497. }));
  498. m_image_menu->add_separator();
  499. m_image_menu->add_action(GUI::Action::create(
  500. "&Resize Image...", { Mod_Ctrl | Mod_Shift, Key_R }, g_icon_bag.resize_image, [&](auto&) {
  501. auto* editor = current_image_editor();
  502. VERIFY(editor);
  503. auto dialog = PixelPaint::ResizeImageDialog::construct(editor->image().size(), &window);
  504. if (dialog->exec() == GUI::Dialog::ExecResult::OK) {
  505. editor->image().resize(dialog->desired_size(), dialog->scaling_mode());
  506. editor->did_complete_action("Resize Image"sv);
  507. }
  508. }));
  509. m_image_menu->add_action(GUI::Action::create(
  510. "&Crop Image to Selection", g_icon_bag.crop, [&](auto&) {
  511. auto* editor = current_image_editor();
  512. VERIFY(editor);
  513. // FIXME: disable this action if there is no selection
  514. if (editor->image().selection().is_empty())
  515. return;
  516. auto crop_rect = editor->image().rect().intersected(editor->image().selection().bounding_rect());
  517. editor->image().crop(crop_rect);
  518. editor->image().selection().clear();
  519. editor->did_complete_action("Crop Image to Selection"sv);
  520. }));
  521. m_image_menu->add_action(GUI::Action::create(
  522. "&Crop Image to Content", g_icon_bag.crop, [&](auto&) {
  523. auto* editor = current_image_editor();
  524. VERIFY(editor);
  525. auto content_bounding_rect = editor->image().nonempty_content_bounding_rect();
  526. if (!content_bounding_rect.has_value())
  527. return;
  528. editor->image().crop(content_bounding_rect.value());
  529. editor->did_complete_action("Crop Image to Content"sv);
  530. }));
  531. m_layer_menu = window.add_menu("&Layer");
  532. m_layer_menu->on_visibility_change = [this](bool visible) {
  533. if (!visible)
  534. return;
  535. bool image_has_selection = !current_image_editor()->active_layer()->image().selection().is_empty();
  536. m_layer_via_copy->set_enabled(image_has_selection);
  537. m_layer_via_cut->set_enabled(image_has_selection);
  538. };
  539. m_layer_menu->add_action(GUI::Action::create(
  540. "New &Layer...", { Mod_Ctrl | Mod_Shift, Key_N }, g_icon_bag.new_layer, [&](auto&) {
  541. auto* editor = current_image_editor();
  542. VERIFY(editor);
  543. auto dialog = PixelPaint::CreateNewLayerDialog::construct(editor->image().size(), &window);
  544. if (dialog->exec() == GUI::Dialog::ExecResult::OK) {
  545. auto layer_or_error = PixelPaint::Layer::try_create_with_size(editor->image(), dialog->layer_size(), dialog->layer_name());
  546. if (layer_or_error.is_error()) {
  547. GUI::MessageBox::show_error(&window, String::formatted("Unable to create layer with size {}", dialog->size()));
  548. return;
  549. }
  550. editor->image().add_layer(layer_or_error.release_value());
  551. editor->layers_did_change();
  552. editor->did_complete_action("New Layer"sv);
  553. m_layer_list_widget->select_top_layer();
  554. }
  555. }));
  556. m_layer_via_copy = GUI::Action::create(
  557. "Layer via Copy", { Mod_Ctrl | Mod_Shift, Key_C }, g_icon_bag.new_layer, [&](auto&) {
  558. auto add_layer_success = current_image_editor()->add_new_layer_from_selection();
  559. if (add_layer_success.is_error()) {
  560. GUI::MessageBox::show_error(&window, add_layer_success.release_error().string_literal());
  561. return;
  562. }
  563. current_image_editor()->did_complete_action("New Layer via Copy"sv);
  564. m_layer_list_widget->select_top_layer();
  565. });
  566. m_layer_menu->add_action(*m_layer_via_copy);
  567. m_layer_via_cut = GUI::Action::create(
  568. "Layer via Cut", { Mod_Ctrl | Mod_Shift, Key_X }, g_icon_bag.new_layer, [&](auto&) {
  569. auto add_layer_success = current_image_editor()->add_new_layer_from_selection();
  570. if (add_layer_success.is_error()) {
  571. GUI::MessageBox::show_error(&window, add_layer_success.release_error().string_literal());
  572. return;
  573. }
  574. current_image_editor()->active_layer()->erase_selection(current_image_editor()->image().selection());
  575. current_image_editor()->did_complete_action("New Layer via Cut"sv);
  576. m_layer_list_widget->select_top_layer();
  577. });
  578. m_layer_menu->add_action(*m_layer_via_cut);
  579. m_layer_menu->add_separator();
  580. m_layer_menu->add_action(GUI::Action::create(
  581. "Add M&ask", { Mod_Ctrl | Mod_Shift, Key_M }, g_icon_bag.add_mask, [&](auto&) {
  582. auto* editor = current_image_editor();
  583. VERIFY(editor);
  584. auto active_layer = editor->active_layer();
  585. if (!active_layer)
  586. return;
  587. active_layer->create_mask();
  588. editor->update();
  589. m_layer_list_widget->repaint();
  590. }));
  591. m_layer_menu->add_separator();
  592. m_layer_menu->add_action(GUI::Action::create(
  593. "Select &Previous Layer", { 0, Key_PageUp }, g_icon_bag.previous_layer, [&](auto&) {
  594. m_layer_list_widget->cycle_through_selection(1);
  595. }));
  596. m_layer_menu->add_action(GUI::Action::create(
  597. "Select &Next Layer", { 0, Key_PageDown }, g_icon_bag.next_layer, [&](auto&) {
  598. m_layer_list_widget->cycle_through_selection(-1);
  599. }));
  600. m_layer_menu->add_action(GUI::Action::create(
  601. "Select &Top Layer", { 0, Key_Home }, g_icon_bag.top_layer, [&](auto&) {
  602. m_layer_list_widget->select_top_layer();
  603. }));
  604. m_layer_menu->add_action(GUI::Action::create(
  605. "Select B&ottom Layer", { 0, Key_End }, g_icon_bag.bottom_layer, [&](auto&) {
  606. m_layer_list_widget->select_bottom_layer();
  607. }));
  608. m_layer_menu->add_separator();
  609. m_layer_menu->add_action(GUI::CommonActions::make_move_to_front_action(
  610. [&](auto&) {
  611. auto* editor = current_image_editor();
  612. VERIFY(editor);
  613. auto active_layer = editor->active_layer();
  614. if (!active_layer)
  615. return;
  616. editor->image().move_layer_to_front(*active_layer);
  617. editor->layers_did_change();
  618. }));
  619. m_layer_menu->add_action(GUI::CommonActions::make_move_to_back_action(
  620. [&](auto&) {
  621. auto* editor = current_image_editor();
  622. VERIFY(editor);
  623. auto active_layer = editor->active_layer();
  624. if (!active_layer)
  625. return;
  626. editor->image().move_layer_to_back(*active_layer);
  627. editor->layers_did_change();
  628. }));
  629. m_layer_menu->add_separator();
  630. m_layer_menu->add_action(GUI::Action::create(
  631. "Move Active Layer &Up", { Mod_Ctrl, Key_PageUp }, g_icon_bag.active_layer_up, [&](auto&) {
  632. auto* editor = current_image_editor();
  633. VERIFY(editor);
  634. auto active_layer = editor->active_layer();
  635. if (!active_layer)
  636. return;
  637. editor->image().move_layer_up(*active_layer);
  638. }));
  639. m_layer_menu->add_action(GUI::Action::create(
  640. "Move Active Layer &Down", { Mod_Ctrl, Key_PageDown }, g_icon_bag.active_layer_down, [&](auto&) {
  641. auto* editor = current_image_editor();
  642. VERIFY(editor);
  643. auto active_layer = editor->active_layer();
  644. if (!active_layer)
  645. return;
  646. editor->image().move_layer_down(*active_layer);
  647. }));
  648. m_layer_menu->add_separator();
  649. m_layer_menu->add_action(GUI::Action::create(
  650. "&Remove Active Layer", { Mod_Ctrl, Key_D }, g_icon_bag.delete_layer, [&](auto&) {
  651. auto* editor = current_image_editor();
  652. VERIFY(editor);
  653. auto active_layer = editor->active_layer();
  654. if (!active_layer)
  655. return;
  656. auto active_layer_index = editor->image().index_of(*active_layer);
  657. editor->image().remove_layer(*active_layer);
  658. if (editor->image().layer_count()) {
  659. auto& next_active_layer = editor->image().layer(active_layer_index > 0 ? active_layer_index - 1 : 0);
  660. editor->set_active_layer(&next_active_layer);
  661. } else {
  662. auto layer = PixelPaint::Layer::try_create_with_size(editor->image(), editor->image().size(), "Background").release_value_but_fixme_should_propagate_errors();
  663. editor->image().add_layer(move(layer));
  664. editor->layers_did_change();
  665. m_layer_list_widget->select_top_layer();
  666. }
  667. }));
  668. m_layer_list_widget->on_context_menu_request = [&](auto& event) {
  669. m_layer_menu->popup(event.screen_position());
  670. };
  671. m_layer_menu->add_separator();
  672. m_layer_menu->add_action(GUI::Action::create(
  673. "Fl&atten Image", { Mod_Ctrl, Key_F }, g_icon_bag.flatten_image, [&](auto&) {
  674. auto* editor = current_image_editor();
  675. VERIFY(editor);
  676. editor->image().flatten_all_layers();
  677. editor->did_complete_action("Flatten Image"sv);
  678. }));
  679. m_layer_menu->add_action(GUI::Action::create(
  680. "&Merge Visible", { Mod_Ctrl, Key_M }, g_icon_bag.merge_visible, [&](auto&) {
  681. auto* editor = current_image_editor();
  682. VERIFY(editor);
  683. editor->image().merge_visible_layers();
  684. editor->did_complete_action("Merge Visible"sv);
  685. }));
  686. m_layer_menu->add_action(GUI::Action::create(
  687. "Merge &Active Layer Up", g_icon_bag.merge_active_layer_up, [&](auto&) {
  688. auto* editor = current_image_editor();
  689. VERIFY(editor);
  690. auto active_layer = editor->active_layer();
  691. if (!active_layer)
  692. return;
  693. editor->image().merge_active_layer_up(*active_layer);
  694. editor->did_complete_action("Merge Active Layer Up"sv);
  695. }));
  696. m_layer_menu->add_action(GUI::Action::create(
  697. "M&erge Active Layer Down", { Mod_Ctrl, Key_E }, g_icon_bag.merge_active_layer_down, [&](auto&) {
  698. auto* editor = current_image_editor();
  699. VERIFY(editor);
  700. auto active_layer = editor->active_layer();
  701. if (!active_layer)
  702. return;
  703. editor->image().merge_active_layer_down(*active_layer);
  704. editor->did_complete_action("Merge Active Layer Down"sv);
  705. }));
  706. m_layer_menu->add_separator();
  707. m_layer_menu->add_action(GUI::Action::create(
  708. "Flip Layer &Vertically", g_icon_bag.edit_flip_vertical, [&](auto&) {
  709. auto* editor = current_image_editor();
  710. VERIFY(editor);
  711. auto active_layer = editor->active_layer();
  712. if (!active_layer)
  713. return;
  714. active_layer->flip(Gfx::Orientation::Vertical);
  715. editor->did_complete_action("Flip Layer Vertically"sv);
  716. }));
  717. m_layer_menu->add_action(GUI::Action::create(
  718. "Flip Layer &Horizontally", g_icon_bag.edit_flip_horizontal, [&](auto&) {
  719. auto* editor = current_image_editor();
  720. VERIFY(editor);
  721. auto active_layer = editor->active_layer();
  722. if (!active_layer)
  723. return;
  724. active_layer->flip(Gfx::Orientation::Horizontal);
  725. editor->did_complete_action("Flip Layer Horizontally"sv);
  726. }));
  727. m_layer_menu->add_separator();
  728. m_layer_menu->add_action(GUI::Action::create("Rotate Layer &Counterclockwise", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-rotate-ccw.png"sv).release_value_but_fixme_should_propagate_errors(),
  729. [&](auto&) {
  730. auto* editor = current_image_editor();
  731. VERIFY(editor);
  732. auto active_layer = editor->active_layer();
  733. if (!active_layer)
  734. return;
  735. active_layer->rotate(Gfx::RotationDirection::CounterClockwise);
  736. editor->did_complete_action("Rotate Layer Counterclockwise"sv);
  737. }));
  738. m_layer_menu->add_action(GUI::Action::create("Rotate Layer Clock&wise", Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-rotate-cw.png"sv).release_value_but_fixme_should_propagate_errors(),
  739. [&](auto&) {
  740. auto* editor = current_image_editor();
  741. VERIFY(editor);
  742. auto active_layer = editor->active_layer();
  743. if (!active_layer)
  744. return;
  745. active_layer->rotate(Gfx::RotationDirection::Clockwise);
  746. editor->did_complete_action("Rotate Layer Clockwise"sv);
  747. }));
  748. m_layer_menu->add_separator();
  749. m_layer_menu->add_action(GUI::Action::create(
  750. "&Crop Layer to Selection", g_icon_bag.crop, [&](auto&) {
  751. auto* editor = current_image_editor();
  752. VERIFY(editor);
  753. // FIXME: disable this action if there is no selection
  754. auto active_layer = editor->active_layer();
  755. if (!active_layer || editor->image().selection().is_empty())
  756. return;
  757. auto intersection = editor->image().rect().intersected(editor->image().selection().bounding_rect());
  758. auto crop_rect = intersection.translated(-active_layer->location());
  759. active_layer->crop(crop_rect);
  760. active_layer->set_location(intersection.location());
  761. editor->image().selection().clear();
  762. editor->did_complete_action("Crop Layer to Selection"sv);
  763. }));
  764. m_layer_menu->add_action(GUI::Action::create(
  765. "&Crop Layer to Content", g_icon_bag.crop, [&](auto&) {
  766. auto* editor = current_image_editor();
  767. VERIFY(editor);
  768. auto active_layer = editor->active_layer();
  769. if (!active_layer)
  770. return;
  771. auto content_bounding_rect = active_layer->nonempty_content_bounding_rect();
  772. if (!content_bounding_rect.has_value())
  773. return;
  774. active_layer->crop(content_bounding_rect.value());
  775. active_layer->set_location(content_bounding_rect->location());
  776. editor->did_complete_action("Crop Layer to Content"sv);
  777. }));
  778. m_filter_menu = window.add_menu("&Filter");
  779. m_filter_menu->add_action(GUI::Action::create("Filter &Gallery", g_icon_bag.filter, [&](auto&) {
  780. auto* editor = current_image_editor();
  781. VERIFY(editor);
  782. auto dialog = PixelPaint::FilterGallery::construct(&window, editor);
  783. if (dialog->exec() != GUI::Dialog::ExecResult::OK)
  784. return;
  785. }));
  786. m_filter_menu->add_separator();
  787. m_filter_menu->add_action(GUI::Action::create("Generic 5x5 &Convolution", g_icon_bag.generic_5x5_convolution, [&](auto&) {
  788. auto* editor = current_image_editor();
  789. VERIFY(editor);
  790. if (auto* layer = editor->active_layer()) {
  791. Gfx::GenericConvolutionFilter<5> filter;
  792. if (auto parameters = PixelPaint::FilterParameters<Gfx::GenericConvolutionFilter<5>>::get(&window)) {
  793. filter.apply(layer->content_bitmap(), layer->rect(), layer->content_bitmap(), layer->rect(), *parameters);
  794. layer->did_modify_bitmap(layer->rect());
  795. editor->did_complete_action("Generic 5x5 Convolution"sv);
  796. }
  797. }
  798. }));
  799. auto& help_menu = window.add_menu("&Help");
  800. help_menu.add_action(GUI::CommonActions::make_command_palette_action(&window));
  801. help_menu.add_action(GUI::CommonActions::make_about_action("Pixel Paint", GUI::Icon::default_icon("app-pixel-paint"sv), &window));
  802. m_levels_dialog_action = GUI::Action::create(
  803. "Change &Levels...", { Mod_Ctrl, Key_L }, g_icon_bag.levels, [&](auto&) {
  804. auto* editor = current_image_editor();
  805. VERIFY(editor);
  806. auto dialog = PixelPaint::LevelsDialog::construct(&window, editor);
  807. if (dialog->exec() != GUI::Dialog::ExecResult::OK)
  808. dialog->revert_possible_changes();
  809. });
  810. auto& toolbar = *find_descendant_of_type_named<GUI::Toolbar>("toolbar");
  811. toolbar.add_action(*m_new_image_action);
  812. toolbar.add_action(*m_open_image_action);
  813. toolbar.add_action(*m_save_image_action);
  814. toolbar.add_separator();
  815. toolbar.add_action(*m_cut_action);
  816. toolbar.add_action(*m_copy_action);
  817. toolbar.add_action(*m_paste_action);
  818. toolbar.add_action(*m_undo_action);
  819. toolbar.add_action(*m_redo_action);
  820. toolbar.add_separator();
  821. toolbar.add_action(*m_zoom_in_action);
  822. toolbar.add_action(*m_zoom_out_action);
  823. toolbar.add_action(*m_reset_zoom_action);
  824. m_zoom_combobox = toolbar.add<GUI::ComboBox>();
  825. m_zoom_combobox->set_max_width(75);
  826. m_zoom_combobox->set_model(*GUI::ItemListModel<String>::create(s_suggested_zoom_levels));
  827. m_zoom_combobox->on_change = [this](String const& value, GUI::ModelIndex const& index) {
  828. auto* editor = current_image_editor();
  829. VERIFY(editor);
  830. if (index.is_valid()) {
  831. switch (index.row()) {
  832. case s_zoom_level_fit_width:
  833. editor->fit_image_to_view(ImageEditor::FitType::Width);
  834. return;
  835. case s_zoom_level_fit_height:
  836. editor->fit_image_to_view(ImageEditor::FitType::Height);
  837. return;
  838. case s_zoom_level_fit_image:
  839. editor->fit_image_to_view(ImageEditor::FitType::Both);
  840. return;
  841. }
  842. }
  843. auto zoom_level_optional = value.view().trim("%"sv, TrimMode::Right).to_int();
  844. if (!zoom_level_optional.has_value()) {
  845. // Indicate that a parse-error occurred by resetting the text to the current state.
  846. editor->on_scale_change(editor->scale());
  847. return;
  848. }
  849. editor->set_scale(zoom_level_optional.value() * 1.0f / 100);
  850. // If the selected zoom level got clamped, or a "fit to …" level was selected,
  851. // there is a chance that the new scale is identical to the old scale.
  852. // In these cases, we need to manually reset the text:
  853. editor->on_scale_change(editor->scale());
  854. };
  855. m_zoom_combobox->on_return_pressed = [this]() {
  856. m_zoom_combobox->on_change(m_zoom_combobox->text(), GUI::ModelIndex());
  857. };
  858. toolbar.add_separator();
  859. toolbar.add_action(*m_levels_dialog_action);
  860. }
  861. void MainWidget::set_actions_enabled(bool enabled)
  862. {
  863. m_save_image_action->set_enabled(enabled);
  864. m_save_image_as_action->set_enabled(enabled);
  865. m_close_image_action->set_enabled(enabled);
  866. m_export_submenu->set_children_actions_enabled(enabled);
  867. m_edit_menu->set_children_actions_enabled(enabled);
  868. m_paste_action->set_enabled(true);
  869. m_view_menu->set_children_actions_enabled(enabled);
  870. m_layer_menu->set_children_actions_enabled(enabled);
  871. m_image_menu->set_children_actions_enabled(enabled);
  872. m_tool_menu->set_children_actions_enabled(enabled);
  873. m_filter_menu->set_children_actions_enabled(enabled);
  874. m_zoom_combobox->set_enabled(enabled);
  875. }
  876. void MainWidget::open_image(Core::File& file)
  877. {
  878. auto try_load = m_loader.try_load_from_file(file);
  879. if (try_load.is_error()) {
  880. GUI::MessageBox::show_error(window(), String::formatted("Unable to open file: {}, {}", file.filename(), try_load.error()));
  881. return;
  882. }
  883. auto& image = *m_loader.release_image();
  884. auto& editor = create_new_editor(image);
  885. editor.set_loaded_from_image(m_loader.is_raw_image());
  886. editor.set_path(file.filename());
  887. editor.undo_stack().set_current_unmodified();
  888. m_layer_list_widget->set_image(&image);
  889. }
  890. void MainWidget::create_default_image()
  891. {
  892. auto image = Image::try_create_with_size({ 510, 356 }).release_value_but_fixme_should_propagate_errors();
  893. auto bg_layer = Layer::try_create_with_size(*image, image->size(), "Background").release_value_but_fixme_should_propagate_errors();
  894. image->add_layer(*bg_layer);
  895. bg_layer->content_bitmap().fill(Color::Transparent);
  896. m_layer_list_widget->set_image(image);
  897. auto& editor = create_new_editor(*image);
  898. editor.set_title("Untitled");
  899. editor.set_active_layer(bg_layer);
  900. editor.undo_stack().set_current_unmodified();
  901. }
  902. void MainWidget::create_image_from_clipboard()
  903. {
  904. auto bitmap = GUI::Clipboard::the().fetch_data_and_type().as_bitmap();
  905. if (!bitmap) {
  906. GUI::MessageBox::show(window(), "There is no image in a clipboard to paste."sv, "PixelPaint"sv, GUI::MessageBox::Type::Warning);
  907. return;
  908. }
  909. auto image = PixelPaint::Image::try_create_with_size(bitmap->size()).release_value_but_fixme_should_propagate_errors();
  910. auto layer = PixelPaint::Layer::try_create_with_bitmap(image, *bitmap, "Pasted layer").release_value_but_fixme_should_propagate_errors();
  911. image->add_layer(*layer);
  912. auto& editor = create_new_editor(*image);
  913. editor.set_title("Untitled");
  914. m_layer_list_widget->set_image(image);
  915. m_layer_list_widget->set_selected_layer(layer);
  916. }
  917. bool MainWidget::request_close()
  918. {
  919. while (!m_tab_widget->children().is_empty()) {
  920. auto* editor = current_image_editor();
  921. VERIFY(editor);
  922. if (!editor->request_close())
  923. return false;
  924. m_tab_widget->remove_tab(*m_tab_widget->active_widget());
  925. }
  926. return true;
  927. }
  928. ImageEditor* MainWidget::current_image_editor()
  929. {
  930. if (!m_tab_widget->active_widget())
  931. return nullptr;
  932. return verify_cast<PixelPaint::ImageEditor>(m_tab_widget->active_widget());
  933. }
  934. ImageEditor& MainWidget::create_new_editor(NonnullRefPtr<Image> image)
  935. {
  936. auto& image_editor = m_tab_widget->add_tab<PixelPaint::ImageEditor>("Untitled", image);
  937. image_editor.on_active_layer_change = [&](auto* layer) {
  938. if (current_image_editor() != &image_editor)
  939. return;
  940. m_layer_list_widget->set_selected_layer(layer);
  941. m_layer_properties_widget->set_layer(layer);
  942. };
  943. image_editor.on_title_change = [&](auto const& title) {
  944. m_tab_widget->set_tab_title(image_editor, title);
  945. };
  946. image_editor.on_image_mouse_position_change = [&](auto const& mouse_position) {
  947. auto const& image_size = current_image_editor()->image().size();
  948. auto image_rectangle = Gfx::IntRect { 0, 0, image_size.width(), image_size.height() };
  949. if (image_rectangle.contains(mouse_position)) {
  950. m_statusbar->set_override_text(mouse_position.to_string());
  951. m_histogram_widget->set_color_at_mouseposition(current_image_editor()->image().color_at(mouse_position));
  952. m_vectorscope_widget->set_color_at_mouseposition(current_image_editor()->image().color_at(mouse_position));
  953. } else {
  954. m_statusbar->set_override_text({});
  955. m_histogram_widget->set_color_at_mouseposition(Color::Transparent);
  956. m_vectorscope_widget->set_color_at_mouseposition(Color::Transparent);
  957. }
  958. };
  959. image_editor.on_leave = [&]() {
  960. m_statusbar->set_override_text({});
  961. m_histogram_widget->set_color_at_mouseposition(Color::Transparent);
  962. m_vectorscope_widget->set_color_at_mouseposition(Color::Transparent);
  963. };
  964. image_editor.on_set_guide_visibility = [&](bool show_guides) {
  965. m_show_guides_action->set_checked(show_guides);
  966. };
  967. image_editor.on_set_ruler_visibility = [&](bool show_rulers) {
  968. m_show_rulers_action->set_checked(show_rulers);
  969. };
  970. image_editor.on_scale_change = [this](float scale) {
  971. m_zoom_combobox->set_text(String::formatted("{}%", roundf(scale * 100)));
  972. };
  973. if (image->layer_count())
  974. image_editor.set_active_layer(&image->layer(0));
  975. if (!m_loader.is_raw_image()) {
  976. m_loader.json_metadata().for_each([&](JsonValue const& value) {
  977. if (!value.is_object())
  978. return;
  979. auto& json_object = value.as_object();
  980. auto orientation_value = json_object.get("orientation"sv);
  981. if (!orientation_value.is_string())
  982. return;
  983. auto offset_value = json_object.get("offset"sv);
  984. if (!offset_value.is_number())
  985. return;
  986. auto orientation_string = orientation_value.as_string();
  987. PixelPaint::Guide::Orientation orientation;
  988. if (orientation_string == "horizontal"sv)
  989. orientation = PixelPaint::Guide::Orientation::Horizontal;
  990. else if (orientation_string == "vertical"sv)
  991. orientation = PixelPaint::Guide::Orientation::Vertical;
  992. else
  993. return;
  994. image_editor.add_guide(PixelPaint::Guide::construct(orientation, offset_value.to_number<float>()));
  995. });
  996. }
  997. m_tab_widget->set_active_widget(&image_editor);
  998. image_editor.set_focus(true);
  999. image_editor.fit_image_to_view();
  1000. m_tool_properties_widget->set_enabled(true);
  1001. set_actions_enabled(true);
  1002. return image_editor;
  1003. }
  1004. void MainWidget::drop_event(GUI::DropEvent& event)
  1005. {
  1006. if (!event.mime_data().has_urls())
  1007. return;
  1008. event.accept();
  1009. if (event.mime_data().urls().is_empty())
  1010. return;
  1011. for (auto& url : event.mime_data().urls()) {
  1012. if (url.scheme() != "file")
  1013. continue;
  1014. auto response = FileSystemAccessClient::Client::the().try_request_file(window(), url.path(), Core::OpenMode::ReadOnly);
  1015. if (response.is_error())
  1016. return;
  1017. open_image(response.value());
  1018. }
  1019. }
  1020. }