Serenity Operating System
1/*
2 * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021-2022, networkException <networkexception@serenityos.org>
4 * Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org>
5 * Copyright (c) 2021, Antonio Di Stefano <tonio9681@gmail.com>
6 * Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
7 *
8 * SPDX-License-Identifier: BSD-2-Clause
9 */
10
11#include "MainWidget.h"
12#include <Applications/ThemeEditor/AlignmentPropertyGML.h>
13#include <Applications/ThemeEditor/ColorPropertyGML.h>
14#include <Applications/ThemeEditor/FlagPropertyGML.h>
15#include <Applications/ThemeEditor/MetricPropertyGML.h>
16#include <Applications/ThemeEditor/PathPropertyGML.h>
17#include <Applications/ThemeEditor/ThemeEditorGML.h>
18#include <LibCore/DeprecatedFile.h>
19#include <LibFileSystemAccessClient/Client.h>
20#include <LibGUI/ActionGroup.h>
21#include <LibGUI/BoxLayout.h>
22#include <LibGUI/Button.h>
23#include <LibGUI/ConnectionToWindowServer.h>
24#include <LibGUI/FilePicker.h>
25#include <LibGUI/Frame.h>
26#include <LibGUI/GroupBox.h>
27#include <LibGUI/Icon.h>
28#include <LibGUI/ItemListModel.h>
29#include <LibGUI/Label.h>
30#include <LibGUI/Menu.h>
31#include <LibGUI/Menubar.h>
32#include <LibGUI/MessageBox.h>
33#include <LibGUI/ScrollableContainerWidget.h>
34#include <LibGfx/Filters/ColorBlindnessFilter.h>
35
36namespace ThemeEditor {
37
38static const PropertyTab window_tab {
39 "Windows",
40 {
41 { "General",
42 { { Gfx::FlagRole::IsDark },
43 { Gfx::AlignmentRole::TitleAlignment },
44 { Gfx::MetricRole::TitleHeight },
45 { Gfx::MetricRole::TitleButtonWidth },
46 { Gfx::MetricRole::TitleButtonHeight },
47 { Gfx::PathRole::TitleButtonIcons },
48 { Gfx::FlagRole::TitleButtonsIconOnly } } },
49
50 { "Border",
51 { { Gfx::MetricRole::BorderThickness },
52 { Gfx::MetricRole::BorderRadius } } },
53
54 { "Active Window",
55 { { Gfx::ColorRole::ActiveWindowBorder1 },
56 { Gfx::ColorRole::ActiveWindowBorder2 },
57 { Gfx::ColorRole::ActiveWindowTitle },
58 { Gfx::ColorRole::ActiveWindowTitleShadow },
59 { Gfx::ColorRole::ActiveWindowTitleStripes },
60 { Gfx::PathRole::ActiveWindowShadow } } },
61
62 { "Inactive Window",
63 { { Gfx::ColorRole::InactiveWindowBorder1 },
64 { Gfx::ColorRole::InactiveWindowBorder2 },
65 { Gfx::ColorRole::InactiveWindowTitle },
66 { Gfx::ColorRole::InactiveWindowTitleShadow },
67 { Gfx::ColorRole::InactiveWindowTitleStripes },
68 { Gfx::PathRole::InactiveWindowShadow } } },
69
70 { "Highlighted Window",
71 { { Gfx::ColorRole::HighlightWindowBorder1 },
72 { Gfx::ColorRole::HighlightWindowBorder2 },
73 { Gfx::ColorRole::HighlightWindowTitle },
74 { Gfx::ColorRole::HighlightWindowTitleShadow },
75 { Gfx::ColorRole::HighlightWindowTitleStripes } } },
76
77 { "Moving Window",
78 { { Gfx::ColorRole::MovingWindowBorder1 },
79 { Gfx::ColorRole::MovingWindowBorder2 },
80 { Gfx::ColorRole::MovingWindowTitle },
81 { Gfx::ColorRole::MovingWindowTitleShadow },
82 { Gfx::ColorRole::MovingWindowTitleStripes } } },
83
84 { "Contents",
85 { { Gfx::ColorRole::Window },
86 { Gfx::ColorRole::WindowText } } },
87
88 { "Desktop",
89 { { Gfx::ColorRole::DesktopBackground },
90 { Gfx::PathRole::TaskbarShadow } } },
91 }
92};
93
94static const PropertyTab widgets_tab {
95 "Widgets",
96 {
97 { "General",
98 { { Gfx::ColorRole::Accent },
99 { Gfx::ColorRole::Base },
100 { Gfx::ColorRole::ThreedHighlight },
101 { Gfx::ColorRole::ThreedShadow1 },
102 { Gfx::ColorRole::ThreedShadow2 },
103 { Gfx::ColorRole::HoverHighlight } } },
104
105 { "Text",
106 { { Gfx::ColorRole::BaseText },
107 { Gfx::ColorRole::DisabledTextFront },
108 { Gfx::ColorRole::DisabledTextBack },
109 { Gfx::ColorRole::PlaceholderText } } },
110
111 { "Links",
112 { { Gfx::ColorRole::Link },
113 { Gfx::ColorRole::ActiveLink },
114 { Gfx::ColorRole::VisitedLink } } },
115
116 { "Buttons",
117 { { Gfx::ColorRole::Button },
118 { Gfx::ColorRole::ButtonText } } },
119
120 { "Tooltips",
121 { { Gfx::ColorRole::Tooltip },
122 { Gfx::ColorRole::TooltipText },
123 { Gfx::PathRole::TooltipShadow } } },
124
125 { "Trays",
126 { { Gfx::ColorRole::Tray },
127 { Gfx::ColorRole::TrayText } } },
128
129 { "Ruler",
130 { { Gfx::ColorRole::Ruler },
131 { Gfx::ColorRole::RulerBorder },
132 { Gfx::ColorRole::RulerActiveText },
133 { Gfx::ColorRole::RulerInactiveText } } },
134
135 { "Gutter",
136 { { Gfx::ColorRole::Gutter },
137 { Gfx::ColorRole::GutterBorder } } },
138
139 { "Rubber Band",
140 { { Gfx::ColorRole::RubberBandBorder },
141 { Gfx::ColorRole::RubberBandFill } } },
142
143 { "Menus",
144 { { Gfx::ColorRole::MenuBase },
145 { Gfx::ColorRole::MenuBaseText },
146 { Gfx::ColorRole::MenuSelection },
147 { Gfx::ColorRole::MenuSelectionText },
148 { Gfx::ColorRole::MenuStripe },
149 { Gfx::PathRole::MenuShadow } } },
150
151 { "Selection",
152 { { Gfx::ColorRole::FocusOutline },
153 { Gfx::ColorRole::TextCursor },
154 { Gfx::ColorRole::Selection },
155 { Gfx::ColorRole::SelectionText },
156 { Gfx::ColorRole::InactiveSelection },
157 { Gfx::ColorRole::InactiveSelectionText },
158 { Gfx::ColorRole::HighlightSearching },
159 { Gfx::ColorRole::HighlightSearchingText } } },
160 }
161};
162
163static const PropertyTab syntax_highlighting_tab {
164 "Syntax Highlighting",
165 {
166 { "General",
167 { { Gfx::ColorRole::SyntaxComment },
168 { Gfx::ColorRole::SyntaxControlKeyword },
169 { Gfx::ColorRole::SyntaxIdentifier },
170 { Gfx::ColorRole::SyntaxKeyword },
171 { Gfx::ColorRole::SyntaxNumber },
172 { Gfx::ColorRole::SyntaxOperator },
173 { Gfx::ColorRole::SyntaxPreprocessorStatement },
174 { Gfx::ColorRole::SyntaxPreprocessorValue },
175 { Gfx::ColorRole::SyntaxPunctuation },
176 { Gfx::ColorRole::SyntaxString },
177 { Gfx::ColorRole::SyntaxType },
178 { Gfx::ColorRole::SyntaxFunction },
179 { Gfx::ColorRole::SyntaxVariable },
180 { Gfx::ColorRole::SyntaxCustomType },
181 { Gfx::ColorRole::SyntaxNamespace },
182 { Gfx::ColorRole::SyntaxMember },
183 { Gfx::ColorRole::SyntaxParameter } } },
184 }
185};
186
187static const PropertyTab color_scheme_tab {
188 "Color Scheme",
189 {
190 { "General",
191 { { Gfx::FlagRole::BoldTextAsBright },
192 { Gfx::ColorRole::Black },
193 { Gfx::ColorRole::Red },
194 { Gfx::ColorRole::Green },
195 { Gfx::ColorRole::Yellow },
196 { Gfx::ColorRole::Blue },
197 { Gfx::ColorRole::Magenta },
198 { Gfx::ColorRole::ColorSchemeBackground },
199 { Gfx::ColorRole::ColorSchemeForeground },
200 { Gfx::ColorRole::Cyan },
201 { Gfx::ColorRole::White },
202 { Gfx::ColorRole::BrightBlack },
203 { Gfx::ColorRole::BrightRed },
204 { Gfx::ColorRole::BrightGreen },
205 { Gfx::ColorRole::BrightYellow },
206 { Gfx::ColorRole::BrightBlue },
207 { Gfx::ColorRole::BrightMagenta },
208 { Gfx::ColorRole::BrightCyan },
209 { Gfx::ColorRole::BrightWhite } } },
210 }
211};
212
213ErrorOr<NonnullRefPtr<MainWidget>> MainWidget::try_create()
214{
215 auto alignment_model = TRY(AlignmentModel::try_create());
216
217 auto main_widget = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) MainWidget(move(alignment_model))));
218
219 TRY(main_widget->load_from_gml(theme_editor_gml));
220 main_widget->m_preview_widget = main_widget->find_descendant_of_type_named<ThemeEditor::PreviewWidget>("preview_widget");
221 main_widget->m_property_tabs = main_widget->find_descendant_of_type_named<GUI::TabWidget>("property_tabs");
222
223 TRY(main_widget->add_property_tab(window_tab));
224 TRY(main_widget->add_property_tab(widgets_tab));
225 TRY(main_widget->add_property_tab(syntax_highlighting_tab));
226 TRY(main_widget->add_property_tab(color_scheme_tab));
227
228 main_widget->build_override_controls();
229
230 return main_widget;
231}
232
233MainWidget::MainWidget(NonnullRefPtr<AlignmentModel> alignment_model)
234 : m_current_palette(GUI::Application::the()->palette())
235 , m_alignment_model(move(alignment_model))
236{
237}
238
239ErrorOr<void> MainWidget::initialize_menubar(GUI::Window& window)
240{
241 auto file_menu = TRY(window.try_add_menu("&File"));
242 TRY(file_menu->try_add_action(GUI::CommonActions::make_open_action([&](auto&) {
243 if (request_close() == GUI::Window::CloseRequestDecision::StayOpen)
244 return;
245 auto response = FileSystemAccessClient::Client::the().open_file(&window, "Select theme file", "/res/themes"sv);
246 if (response.is_error())
247 return;
248 auto load_from_file_result = load_from_file(response.value().filename(), response.value().release_stream());
249 if (load_from_file_result.is_error()) {
250 GUI::MessageBox::show_error(&window, DeprecatedString::formatted("Can't open file named {}: {}", response.value().filename(), load_from_file_result.error()));
251 return;
252 }
253 })));
254
255 m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
256 if (m_path.has_value()) {
257 auto result = FileSystemAccessClient::Client::the().request_file(&window, *m_path, Core::File::OpenMode::ReadWrite | Core::File::OpenMode::Truncate);
258 if (result.is_error())
259 return;
260 save_to_file(result.value().filename(), result.value().release_stream());
261 } else {
262 auto result = FileSystemAccessClient::Client::the().save_file(&window, "Theme", "ini", Core::File::OpenMode::ReadWrite | Core::File::OpenMode::Truncate);
263 if (result.is_error())
264 return;
265 save_to_file(result.value().filename(), result.value().release_stream());
266 }
267 });
268 TRY(file_menu->try_add_action(*m_save_action));
269
270 TRY(file_menu->try_add_action(GUI::CommonActions::make_save_as_action([&](auto&) {
271 auto result = FileSystemAccessClient::Client::the().save_file(&window, "Theme", "ini", Core::File::OpenMode::ReadWrite | Core::File::OpenMode::Truncate);
272 if (result.is_error())
273 return;
274 save_to_file(result.value().filename(), result.value().release_stream());
275 })));
276
277 TRY(file_menu->try_add_separator());
278 TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([&](auto&) {
279 if (request_close() == GUI::Window::CloseRequestDecision::Close)
280 GUI::Application::the()->quit();
281 })));
282
283 TRY(window.try_add_menu(TRY(GUI::CommonMenus::make_accessibility_menu(*m_preview_widget))));
284
285 auto help_menu = TRY(window.try_add_menu("&Help"));
286 TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(&window)));
287 TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Theme Editor", GUI::Icon::default_icon("app-theme-editor"sv), &window)));
288
289 return {};
290}
291
292void MainWidget::update_title()
293{
294 window()->set_title(DeprecatedString::formatted("{}[*] - Theme Editor", m_path.value_or("Untitled")));
295}
296
297GUI::Window::CloseRequestDecision MainWidget::request_close()
298{
299 if (!window()->is_modified())
300 return GUI::Window::CloseRequestDecision::Close;
301
302 auto result = GUI::MessageBox::ask_about_unsaved_changes(window(), m_path.value_or(""), m_last_modified_time);
303 if (result == GUI::MessageBox::ExecResult::Yes) {
304 m_save_action->activate();
305 if (window()->is_modified())
306 return GUI::Window::CloseRequestDecision::StayOpen;
307 return GUI::Window::CloseRequestDecision::Close;
308 }
309
310 if (result == GUI::MessageBox::ExecResult::No)
311 return GUI::Window::CloseRequestDecision::Close;
312
313 return GUI::Window::CloseRequestDecision::StayOpen;
314}
315
316void MainWidget::set_path(DeprecatedString path)
317{
318 m_path = path;
319 update_title();
320}
321
322void MainWidget::save_to_file(String const& filename, NonnullOwnPtr<Core::File> file)
323{
324 auto theme = Core::ConfigFile::open(filename.to_deprecated_string(), move(file)).release_value_but_fixme_should_propagate_errors();
325
326#define __ENUMERATE_ALIGNMENT_ROLE(role) theme->write_entry("Alignments", to_string(Gfx::AlignmentRole::role), to_string(m_current_palette.alignment(Gfx::AlignmentRole::role)));
327 ENUMERATE_ALIGNMENT_ROLES(__ENUMERATE_ALIGNMENT_ROLE)
328#undef __ENUMERATE_ALIGNMENT_ROLE
329
330#define __ENUMERATE_COLOR_ROLE(role) theme->write_entry("Colors", to_string(Gfx::ColorRole::role), m_current_palette.color(Gfx::ColorRole::role).to_deprecated_string());
331 ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE)
332#undef __ENUMERATE_COLOR_ROLE
333
334#define __ENUMERATE_FLAG_ROLE(role) theme->write_bool_entry("Flags", to_string(Gfx::FlagRole::role), m_current_palette.flag(Gfx::FlagRole::role));
335 ENUMERATE_FLAG_ROLES(__ENUMERATE_FLAG_ROLE)
336#undef __ENUMERATE_FLAG_ROLE
337
338#define __ENUMERATE_METRIC_ROLE(role) theme->write_num_entry("Metrics", to_string(Gfx::MetricRole::role), m_current_palette.metric(Gfx::MetricRole::role));
339 ENUMERATE_METRIC_ROLES(__ENUMERATE_METRIC_ROLE)
340#undef __ENUMERATE_METRIC_ROLE
341
342#define __ENUMERATE_PATH_ROLE(role) theme->write_entry("Paths", to_string(Gfx::PathRole::role), m_current_palette.path(Gfx::PathRole::role));
343 ENUMERATE_PATH_ROLES(__ENUMERATE_PATH_ROLE)
344#undef __ENUMERATE_PATH_ROLE
345
346 auto sync_result = theme->sync();
347 if (sync_result.is_error()) {
348 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to save theme file: {}", sync_result.error()));
349 } else {
350 m_last_modified_time = Time::now_monotonic();
351 set_path(filename.to_deprecated_string());
352 window()->set_modified(false);
353 }
354}
355
356ErrorOr<Core::AnonymousBuffer> MainWidget::encode()
357{
358 auto buffer = TRY(Core::AnonymousBuffer::create_with_size(sizeof(Gfx::SystemTheme)));
359 auto* data = buffer.data<Gfx::SystemTheme>();
360
361#define __ENUMERATE_ALIGNMENT_ROLE(role) \
362 data->alignment[(int)Gfx::AlignmentRole::role] = m_current_palette.alignment(Gfx::AlignmentRole::role);
363 ENUMERATE_ALIGNMENT_ROLES(__ENUMERATE_ALIGNMENT_ROLE)
364#undef __ENUMERATE_ALIGNMENT_ROLE
365
366#define __ENUMERATE_COLOR_ROLE(role) \
367 data->color[(int)Gfx::ColorRole::role] = m_current_palette.color(Gfx::ColorRole::role).value();
368 ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE)
369#undef __ENUMERATE_COLOR_ROLE
370
371#define __ENUMERATE_FLAG_ROLE(role) \
372 data->flag[(int)Gfx::FlagRole::role] = m_current_palette.flag(Gfx::FlagRole::role);
373 ENUMERATE_FLAG_ROLES(__ENUMERATE_FLAG_ROLE)
374#undef __ENUMERATE_FLAG_ROLE
375
376#define __ENUMERATE_METRIC_ROLE(role) \
377 data->metric[(int)Gfx::MetricRole::role] = m_current_palette.metric(Gfx::MetricRole::role);
378 ENUMERATE_METRIC_ROLES(__ENUMERATE_METRIC_ROLE)
379#undef __ENUMERATE_METRIC_ROLE
380
381#define ENCODE_PATH(role, allow_empty) \
382 do { \
383 auto path = m_current_palette.path(Gfx::PathRole::role); \
384 char const* characters; \
385 if (path.is_empty()) { \
386 switch (Gfx::PathRole::role) { \
387 case Gfx::PathRole::TitleButtonIcons: \
388 characters = "/res/icons/16x16/"; \
389 break; \
390 default: \
391 characters = allow_empty ? "" : "/res/"; \
392 } \
393 } \
394 characters = path.characters(); \
395 memcpy(data->path[(int)Gfx::PathRole::role], characters, min(strlen(characters) + 1, sizeof(data->path[(int)Gfx::PathRole::role]))); \
396 data->path[(int)Gfx::PathRole::role][sizeof(data->path[(int)Gfx::PathRole::role]) - 1] = '\0'; \
397 } while (0)
398
399 ENCODE_PATH(TitleButtonIcons, false);
400 ENCODE_PATH(ActiveWindowShadow, true);
401 ENCODE_PATH(InactiveWindowShadow, true);
402 ENCODE_PATH(TaskbarShadow, true);
403 ENCODE_PATH(MenuShadow, true);
404 ENCODE_PATH(TooltipShadow, true);
405
406 return buffer;
407}
408
409void MainWidget::build_override_controls()
410{
411 auto* theme_override_controls = find_descendant_of_type_named<GUI::Widget>("theme_override_controls");
412
413 m_theme_override_apply = theme_override_controls->find_child_of_type_named<GUI::DialogButton>("apply_button");
414 m_theme_override_reset = theme_override_controls->find_child_of_type_named<GUI::DialogButton>("reset_button");
415
416 m_theme_override_apply->on_click = [&](auto) {
417 auto encoded = encode();
418 if (encoded.is_error())
419 return;
420 // Empty the color scheme path to signal that it exists only in memory.
421 m_current_palette.path(Gfx::PathRole::ColorScheme) = "";
422 GUI::ConnectionToWindowServer::the().async_set_system_theme_override(encoded.value());
423 };
424
425 m_theme_override_reset->on_click = [&](auto) {
426 GUI::ConnectionToWindowServer::the().async_clear_system_theme_override();
427 };
428
429 GUI::Application::the()->on_theme_change = [&]() {
430 auto override_active = GUI::ConnectionToWindowServer::the().is_system_theme_overridden();
431 m_theme_override_apply->set_enabled(!override_active && window()->is_modified());
432 m_theme_override_reset->set_enabled(override_active);
433 };
434}
435
436ErrorOr<void> MainWidget::add_property_tab(PropertyTab const& property_tab)
437{
438 auto scrollable_container = TRY(m_property_tabs->try_add_tab<GUI::ScrollableContainerWidget>(property_tab.title));
439 scrollable_container->set_should_hide_unnecessary_scrollbars(true);
440
441 auto properties_list = TRY(GUI::Widget::try_create());
442 scrollable_container->set_widget(properties_list);
443 TRY(properties_list->try_set_layout<GUI::VerticalBoxLayout>(GUI::Margins { 8 }, 12));
444
445 for (auto const& group : property_tab.property_groups) {
446 NonnullRefPtr<GUI::GroupBox> group_box = TRY(properties_list->try_add<GUI::GroupBox>(group.title));
447 // 1px less on the left makes the text line up with the group title.
448 TRY(group_box->try_set_layout<GUI::VerticalBoxLayout>(GUI::Margins { 8, 8, 8, 7 }, 12));
449 group_box->set_preferred_height(GUI::SpecialDimension::Fit);
450
451 for (auto const& property : group.properties) {
452 NonnullRefPtr<GUI::Widget> row_widget = TRY(group_box->try_add<GUI::Widget>());
453 row_widget->set_fixed_height(22);
454 TRY(property.role.visit(
455 [&](Gfx::AlignmentRole role) -> ErrorOr<void> {
456 TRY(row_widget->load_from_gml(alignment_property_gml));
457
458 auto& name_label = *row_widget->find_descendant_of_type_named<GUI::Label>("name");
459 name_label.set_text(to_string(role));
460
461 auto& alignment_picker = *row_widget->find_descendant_of_type_named<GUI::ComboBox>("combo_box");
462 alignment_picker.set_model(*m_alignment_model);
463 alignment_picker.on_change = [&, role](auto&, auto& index) {
464 set_alignment(role, index.data(GUI::ModelRole::Custom).to_text_alignment(Gfx::TextAlignment::CenterLeft));
465 };
466 alignment_picker.set_selected_index(m_alignment_model->index_of(m_current_palette.alignment(role)), GUI::AllowCallback::No);
467
468 VERIFY(m_alignment_inputs[to_underlying(role)].is_null());
469 m_alignment_inputs[to_underlying(role)] = alignment_picker;
470 return {};
471 },
472 [&](Gfx::ColorRole role) -> ErrorOr<void> {
473 TRY(row_widget->load_from_gml(color_property_gml));
474
475 auto& name_label = *row_widget->find_descendant_of_type_named<GUI::Label>("name");
476 name_label.set_text(to_string(role));
477
478 auto& color_input = *row_widget->find_descendant_of_type_named<GUI::ColorInput>("color_input");
479 color_input.on_change = [&, role] {
480 set_color(role, color_input.color());
481 };
482 color_input.set_color(m_current_palette.color(role), GUI::AllowCallback::No);
483
484 VERIFY(m_color_inputs[to_underlying(role)].is_null());
485 m_color_inputs[to_underlying(role)] = color_input;
486 return {};
487 },
488 [&](Gfx::FlagRole role) -> ErrorOr<void> {
489 TRY(row_widget->load_from_gml(flag_property_gml));
490
491 auto& checkbox = *row_widget->find_descendant_of_type_named<GUI::CheckBox>("checkbox");
492 checkbox.set_text(String::from_deprecated_string(DeprecatedString(to_string(role))).release_value_but_fixme_should_propagate_errors());
493 checkbox.on_checked = [&, role](bool checked) {
494 set_flag(role, checked);
495 };
496 checkbox.set_checked(m_current_palette.flag(role), GUI::AllowCallback::No);
497
498 VERIFY(m_flag_inputs[to_underlying(role)].is_null());
499 m_flag_inputs[to_underlying(role)] = checkbox;
500 return {};
501 },
502 [&](Gfx::MetricRole role) -> ErrorOr<void> {
503 TRY(row_widget->load_from_gml(metric_property_gml));
504
505 auto& name_label = *row_widget->find_descendant_of_type_named<GUI::Label>("name");
506 name_label.set_text(to_string(role));
507
508 auto& spin_box = *row_widget->find_descendant_of_type_named<GUI::SpinBox>("spin_box");
509 spin_box.on_change = [&, role](int value) {
510 set_metric(role, value);
511 };
512 spin_box.set_value(m_current_palette.metric(role), GUI::AllowCallback::No);
513
514 VERIFY(m_metric_inputs[to_underlying(role)].is_null());
515 m_metric_inputs[to_underlying(role)] = spin_box;
516 return {};
517 },
518 [&](Gfx::PathRole role) -> ErrorOr<void> {
519 TRY(row_widget->load_from_gml(path_property_gml));
520
521 auto& name_label = *row_widget->find_descendant_of_type_named<GUI::Label>("name");
522 name_label.set_text(to_string(role));
523
524 auto& path_input = *row_widget->find_descendant_of_type_named<GUI::TextBox>("path_input");
525 path_input.on_change = [&, role] {
526 set_path(role, path_input.text());
527 };
528 path_input.set_text(m_current_palette.path(role), GUI::AllowCallback::No);
529
530 auto& path_picker_button = *row_widget->find_descendant_of_type_named<GUI::Button>("path_picker_button");
531 auto picker_target = (role == Gfx::PathRole::TitleButtonIcons) ? PathPickerTarget::Folder : PathPickerTarget::File;
532 path_picker_button.on_click = [&, role, picker_target](auto) {
533 show_path_picker_dialog(to_string(role), path_input, picker_target);
534 };
535
536 VERIFY(m_path_inputs[to_underlying(role)].is_null());
537 m_path_inputs[to_underlying(role)] = path_input;
538 return {};
539 }));
540 }
541 }
542
543 return {};
544}
545
546void MainWidget::set_alignment(Gfx::AlignmentRole role, Gfx::TextAlignment value)
547{
548 auto preview_palette = m_current_palette;
549 preview_palette.set_alignment(role, value);
550 set_palette(preview_palette);
551}
552
553void MainWidget::set_color(Gfx::ColorRole role, Gfx::Color value)
554{
555 auto preview_palette = m_current_palette;
556 preview_palette.set_color(role, value);
557 set_palette(preview_palette);
558}
559
560void MainWidget::set_flag(Gfx::FlagRole role, bool value)
561{
562 auto preview_palette = m_current_palette;
563 preview_palette.set_flag(role, value);
564 set_palette(preview_palette);
565}
566
567void MainWidget::set_metric(Gfx::MetricRole role, int value)
568{
569 auto preview_palette = m_current_palette;
570 preview_palette.set_metric(role, value);
571 set_palette(preview_palette);
572}
573
574void MainWidget::set_path(Gfx::PathRole role, DeprecatedString value)
575{
576 auto preview_palette = m_current_palette;
577 preview_palette.set_path(role, value);
578 set_palette(preview_palette);
579}
580
581void MainWidget::set_palette(Gfx::Palette palette)
582{
583 m_current_palette = move(palette);
584 m_preview_widget->set_preview_palette(m_current_palette);
585 m_theme_override_apply->set_enabled(true);
586 window()->set_modified(true);
587}
588
589void MainWidget::show_path_picker_dialog(StringView property_display_name, GUI::TextBox& path_input, PathPickerTarget path_picker_target)
590{
591 bool open_folder = path_picker_target == PathPickerTarget::Folder;
592 auto window_title = DeprecatedString::formatted(open_folder ? "Select {} folder"sv : "Select {} file"sv, property_display_name);
593 auto target_path = path_input.text();
594 if (Core::DeprecatedFile::exists(target_path)) {
595 if (!Core::DeprecatedFile::is_directory(target_path))
596 target_path = LexicalPath::dirname(target_path);
597 } else {
598 target_path = "/res/icons";
599 }
600 auto result = GUI::FilePicker::get_open_filepath(window(), window_title, target_path, open_folder);
601 if (!result.has_value())
602 return;
603 path_input.set_text(*result);
604}
605
606ErrorOr<void> MainWidget::load_from_file(String const& filename, NonnullOwnPtr<Core::File> file)
607{
608 auto config_file = TRY(Core::ConfigFile::open(filename.to_deprecated_string(), move(file)));
609 auto theme = TRY(Gfx::load_system_theme(config_file));
610 VERIFY(theme.is_valid());
611
612 auto new_palette = Gfx::Palette(Gfx::PaletteImpl::create_with_anonymous_buffer(theme));
613 set_palette(move(new_palette));
614 set_path(filename.to_deprecated_string());
615
616#define __ENUMERATE_ALIGNMENT_ROLE(role) \
617 if (auto alignment_input = m_alignment_inputs[to_underlying(Gfx::AlignmentRole::role)]) \
618 alignment_input->set_selected_index(m_alignment_model->index_of(m_current_palette.alignment(Gfx::AlignmentRole::role)), GUI::AllowCallback::No);
619 ENUMERATE_ALIGNMENT_ROLES(__ENUMERATE_ALIGNMENT_ROLE)
620#undef __ENUMERATE_ALIGNMENT_ROLE
621
622#define __ENUMERATE_COLOR_ROLE(role) \
623 if (auto color_input = m_color_inputs[to_underlying(Gfx::ColorRole::role)]) \
624 color_input->set_color(m_current_palette.color(Gfx::ColorRole::role), GUI::AllowCallback::No);
625 ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE)
626#undef __ENUMERATE_COLOR_ROLE
627
628#define __ENUMERATE_FLAG_ROLE(role) \
629 if (auto flag_input = m_flag_inputs[to_underlying(Gfx::FlagRole::role)]) \
630 flag_input->set_checked(m_current_palette.flag(Gfx::FlagRole::role), GUI::AllowCallback::No);
631 ENUMERATE_FLAG_ROLES(__ENUMERATE_FLAG_ROLE)
632#undef __ENUMERATE_FLAG_ROLE
633
634#define __ENUMERATE_METRIC_ROLE(role) \
635 if (auto metric_input = m_metric_inputs[to_underlying(Gfx::MetricRole::role)]) \
636 metric_input->set_value(m_current_palette.metric(Gfx::MetricRole::role), GUI::AllowCallback::No);
637 ENUMERATE_METRIC_ROLES(__ENUMERATE_METRIC_ROLE)
638#undef __ENUMERATE_METRIC_ROLE
639
640#define __ENUMERATE_PATH_ROLE(role) \
641 if (auto path_input = m_path_inputs[to_underlying(Gfx::PathRole::role)]) \
642 path_input->set_text(m_current_palette.path(Gfx::PathRole::role), GUI::AllowCallback::No);
643 ENUMERATE_PATH_ROLES(__ENUMERATE_PATH_ROLE)
644#undef __ENUMERATE_PATH_ROLE
645
646 m_last_modified_time = Time::now_monotonic();
647 window()->set_modified(false);
648 return {};
649}
650
651void MainWidget::drag_enter_event(GUI::DragEvent& event)
652{
653 auto const& mime_types = event.mime_types();
654 if (mime_types.contains_slow("text/uri-list"))
655 event.accept();
656}
657
658void MainWidget::drop_event(GUI::DropEvent& event)
659{
660 event.accept();
661 window()->move_to_front();
662
663 if (event.mime_data().has_urls()) {
664 auto urls = event.mime_data().urls();
665 if (urls.is_empty())
666 return;
667 if (urls.size() > 1) {
668 GUI::MessageBox::show(window(), "ThemeEditor can only open one file at a time!"sv, "One at a time please!"sv, GUI::MessageBox::Type::Error);
669 return;
670 }
671 if (request_close() == GUI::Window::CloseRequestDecision::StayOpen)
672 return;
673
674 auto response = FileSystemAccessClient::Client::the().request_file(window(), urls.first().path(), Core::File::OpenMode::Read);
675 if (response.is_error())
676 return;
677
678 auto load_from_file_result = load_from_file(response.value().filename(), response.value().release_stream());
679 if (load_from_file_result.is_error())
680 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Can't open file named {}: {}", response.value().filename(), load_from_file_result.error()));
681 }
682}
683
684}