Serenity Operating System
at master 684 lines 31 kB view raw
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}