From 423383e9aa1c0b66a1a6f73566ea6e1b4dbda8e5 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 11 May 2022 12:56:16 +0100 Subject: [PATCH] ThemeEditor: Give each theme property its own editing widget This is quite a radical change. The previous UI was very easy to add new properties to (just add to the FooRole enum and it automatically works), but not so nice to use: A ComboBox for selecting a property, and then a box to edit that property's value. This makes it difficult to compare different properties or edit multiple together without a lot of back-and-forth. This new design gives each property its own editing widgets, with those categorized into several tabs. To try and avoid increasing the maintenance burden for this, the UI is generated from the `windows_tab`, `widgets_tab` and `syntax_highlighting_tab` variables, which are basically just lists of which properties go in that tab. One of the `FooProperty.gml` files is loaded to create each property's widgets. --- .../ThemeEditor/AlignmentProperty.gml | 18 + .../Applications/ThemeEditor/CMakeLists.txt | 10 + .../ThemeEditor/ColorProperty.gml | 17 + .../Applications/ThemeEditor/FlagProperty.gml | 12 + .../Applications/ThemeEditor/MainWidget.cpp | 547 +++++++++++------- .../Applications/ThemeEditor/MainWidget.h | 86 ++- .../ThemeEditor/MetricProperty.gml | 17 + .../Applications/ThemeEditor/PathProperty.gml | 24 + .../Applications/ThemeEditor/ThemeEditor.gml | 103 +--- Userland/Applications/ThemeEditor/main.cpp | 2 +- 10 files changed, 514 insertions(+), 322 deletions(-) create mode 100644 Userland/Applications/ThemeEditor/AlignmentProperty.gml create mode 100644 Userland/Applications/ThemeEditor/ColorProperty.gml create mode 100644 Userland/Applications/ThemeEditor/FlagProperty.gml create mode 100644 Userland/Applications/ThemeEditor/MetricProperty.gml create mode 100644 Userland/Applications/ThemeEditor/PathProperty.gml diff --git a/Userland/Applications/ThemeEditor/AlignmentProperty.gml b/Userland/Applications/ThemeEditor/AlignmentProperty.gml new file mode 100644 index 0000000000..e0a62ee7e6 --- /dev/null +++ b/Userland/Applications/ThemeEditor/AlignmentProperty.gml @@ -0,0 +1,18 @@ +@GUI::Frame { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + shrink_to_fit: true + + @GUI::Label { + name: "name" + text: "Some alignment" + text_alignment: "CenterLeft" + fixed_width: 200 + } + + @GUI::ComboBox { + name: "combo_box" + model_only: true + } +} diff --git a/Userland/Applications/ThemeEditor/CMakeLists.txt b/Userland/Applications/ThemeEditor/CMakeLists.txt index 92698eeee7..f98563a658 100644 --- a/Userland/Applications/ThemeEditor/CMakeLists.txt +++ b/Userland/Applications/ThemeEditor/CMakeLists.txt @@ -4,11 +4,21 @@ serenity_component( ) compile_gml(ThemeEditor.gml ThemeEditorGML.h theme_editor_gml) +compile_gml(AlignmentProperty.gml AlignmentPropertyGML.h alignment_property_gml) +compile_gml(ColorProperty.gml ColorPropertyGML.h color_property_gml) +compile_gml(FlagProperty.gml FlagPropertyGML.h flag_property_gml) +compile_gml(MetricProperty.gml MetricPropertyGML.h metric_property_gml) +compile_gml(PathProperty.gml PathPropertyGML.h path_property_gml) set(SOURCES main.cpp MainWidget.cpp PreviewWidget.cpp + AlignmentPropertyGML.h + ColorPropertyGML.h + FlagPropertyGML.h + MetricPropertyGML.h + PathPropertyGML.h ThemeEditorGML.h ) diff --git a/Userland/Applications/ThemeEditor/ColorProperty.gml b/Userland/Applications/ThemeEditor/ColorProperty.gml new file mode 100644 index 0000000000..77504033a0 --- /dev/null +++ b/Userland/Applications/ThemeEditor/ColorProperty.gml @@ -0,0 +1,17 @@ +@GUI::Frame { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + shrink_to_fit: true + + @GUI::Label { + name: "name" + text: "Some color" + text_alignment: "CenterLeft" + fixed_width: 200 + } + + @GUI::ColorInput { + name: "color_input" + } +} diff --git a/Userland/Applications/ThemeEditor/FlagProperty.gml b/Userland/Applications/ThemeEditor/FlagProperty.gml new file mode 100644 index 0000000000..e5e4cb66cb --- /dev/null +++ b/Userland/Applications/ThemeEditor/FlagProperty.gml @@ -0,0 +1,12 @@ +@GUI::Frame { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + shrink_to_fit: true + + @GUI::CheckBox { + name: "checkbox" + text: "Some flag" + checkbox_position: "Right" + } +} diff --git a/Userland/Applications/ThemeEditor/MainWidget.cpp b/Userland/Applications/ThemeEditor/MainWidget.cpp index f4966662cc..b2bacf17fa 100644 --- a/Userland/Applications/ThemeEditor/MainWidget.cpp +++ b/Userland/Applications/ThemeEditor/MainWidget.cpp @@ -9,87 +9,157 @@ */ #include "MainWidget.h" +#include +#include +#include +#include +#include #include #include #include #include #include #include -#include -#include -#include #include #include #include #include +#include #include #include #include -#include -#include +#include #include namespace ThemeEditor { -template -class RoleModel final : public GUI::ItemListModel { -public: - static ErrorOr> try_create(Vector const& data) +static const PropertyTab window_tab { + "Windows", { - return adopt_nonnull_ref_or_enomem(new (nothrow) RoleModel(data)); - } + { Gfx::FlagRole::IsDark }, + { Gfx::AlignmentRole::TitleAlignment }, + { Gfx::MetricRole::TitleHeight }, + { Gfx::MetricRole::TitleButtonWidth }, + { Gfx::MetricRole::TitleButtonHeight }, + { Gfx::PathRole::TitleButtonIcons }, + { Gfx::FlagRole::TitleButtonsIconOnly }, - virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role) const override - { - if (role == GUI::ModelRole::Display) - return Gfx::to_string(this->m_data[index.row()]); - if (role == GUI::ModelRole::Custom) - return this->m_data[index.row()]; + { Gfx::MetricRole::BorderThickness }, + { Gfx::MetricRole::BorderRadius }, - return GUI::ItemListModel::data(index, role); - } + { Gfx::ColorRole::ActiveWindowBorder1 }, + { Gfx::ColorRole::ActiveWindowBorder2 }, + { Gfx::ColorRole::ActiveWindowTitle }, + { Gfx::ColorRole::ActiveWindowTitleShadow }, + { Gfx::ColorRole::ActiveWindowTitleStripes }, + { Gfx::PathRole::ActiveWindowShadow }, -private: - explicit RoleModel(Vector const& data) - : GUI::ItemListModel(data) - { + { Gfx::ColorRole::InactiveWindowBorder1 }, + { Gfx::ColorRole::InactiveWindowBorder2 }, + { Gfx::ColorRole::InactiveWindowTitle }, + { Gfx::ColorRole::InactiveWindowTitleShadow }, + { Gfx::ColorRole::InactiveWindowTitleStripes }, + { Gfx::PathRole::InactiveWindowShadow }, + + { Gfx::ColorRole::HighlightWindowBorder1 }, + { Gfx::ColorRole::HighlightWindowBorder2 }, + { Gfx::ColorRole::HighlightWindowTitle }, + { Gfx::ColorRole::HighlightWindowTitleShadow }, + { Gfx::ColorRole::HighlightWindowTitleStripes }, + + { Gfx::ColorRole::MovingWindowBorder1 }, + { Gfx::ColorRole::MovingWindowBorder2 }, + { Gfx::ColorRole::MovingWindowTitle }, + { Gfx::ColorRole::MovingWindowTitleShadow }, + { Gfx::ColorRole::MovingWindowTitleStripes }, + + { Gfx::ColorRole::Window }, + { Gfx::ColorRole::WindowText }, + + { Gfx::ColorRole::DesktopBackground }, + { Gfx::PathRole::TaskbarShadow }, } }; -class AlignmentModel final : public GUI::Model { -public: - static ErrorOr> try_create() +static const PropertyTab widgets_tab { + "Widgets", { - return adopt_nonnull_ref_or_enomem(new (nothrow) AlignmentModel()); + { Gfx::ColorRole::Accent }, + { Gfx::ColorRole::Base }, + { Gfx::ColorRole::ThreedHighlight }, + { Gfx::ColorRole::ThreedShadow1 }, + { Gfx::ColorRole::ThreedShadow2 }, + { Gfx::ColorRole::HoverHighlight }, + + { Gfx::ColorRole::BaseText }, + { Gfx::ColorRole::DisabledTextFront }, + { Gfx::ColorRole::DisabledTextBack }, + { Gfx::ColorRole::PlaceholderText }, + + { Gfx::ColorRole::Link }, + { Gfx::ColorRole::ActiveLink }, + { Gfx::ColorRole::VisitedLink }, + + { Gfx::ColorRole::Button }, + { Gfx::ColorRole::ButtonText }, + + { Gfx::ColorRole::Tooltip }, + { Gfx::ColorRole::TooltipText }, + { Gfx::PathRole::TooltipShadow }, + + { Gfx::ColorRole::Tray }, + { Gfx::ColorRole::TrayText }, + + { Gfx::ColorRole::Ruler }, + { Gfx::ColorRole::RulerBorder }, + { Gfx::ColorRole::RulerActiveText }, + { Gfx::ColorRole::RulerInactiveText }, + + { Gfx::ColorRole::Gutter }, + { Gfx::ColorRole::GutterBorder }, + + { Gfx::ColorRole::RubberBandBorder }, + { Gfx::ColorRole::RubberBandFill }, + + { Gfx::ColorRole::MenuBase }, + { Gfx::ColorRole::MenuBaseText }, + { Gfx::ColorRole::MenuSelection }, + { Gfx::ColorRole::MenuSelectionText }, + { Gfx::ColorRole::MenuStripe }, + { Gfx::PathRole::MenuShadow }, + + { Gfx::ColorRole::FocusOutline }, + { Gfx::ColorRole::TextCursor }, + { Gfx::ColorRole::Selection }, + { Gfx::ColorRole::SelectionText }, + { Gfx::ColorRole::InactiveSelection }, + { Gfx::ColorRole::InactiveSelectionText }, + { Gfx::ColorRole::HighlightSearching }, + { Gfx::ColorRole::HighlightSearchingText }, } +}; - virtual ~AlignmentModel() = default; - - virtual int row_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override { return 3; } - virtual int column_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override { return 2; } - - virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role) const override +static const PropertyTab syntax_highlighting_tab { + "Syntax Highlighting", { - if (role == GUI::ModelRole::Display) - return m_alignments[index.row()].title; - if (role == GUI::ModelRole::Custom) - return m_alignments[index.row()].setting_value; - - return {}; + { Gfx::ColorRole::SyntaxComment }, + { Gfx::ColorRole::SyntaxControlKeyword }, + { Gfx::ColorRole::SyntaxIdentifier }, + { Gfx::ColorRole::SyntaxKeyword }, + { Gfx::ColorRole::SyntaxNumber }, + { Gfx::ColorRole::SyntaxOperator }, + { Gfx::ColorRole::SyntaxPreprocessorStatement }, + { Gfx::ColorRole::SyntaxPreprocessorValue }, + { Gfx::ColorRole::SyntaxPunctuation }, + { Gfx::ColorRole::SyntaxString }, + { Gfx::ColorRole::SyntaxType }, + { Gfx::ColorRole::SyntaxFunction }, + { Gfx::ColorRole::SyntaxVariable }, + { Gfx::ColorRole::SyntaxCustomType }, + { Gfx::ColorRole::SyntaxNamespace }, + { Gfx::ColorRole::SyntaxMember }, + { Gfx::ColorRole::SyntaxParameter }, } - -private: - AlignmentModel() = default; - - struct AlignmentValue { - String title; - Gfx::TextAlignment setting_value; - }; - Vector m_alignments { - { "Center", Gfx::TextAlignment::Center }, - { "Left", Gfx::TextAlignment::CenterLeft }, - { "Right", Gfx::TextAlignment::CenterRight }, - }; }; MainWidget::MainWidget(Optional path, Gfx::Palette startup_preview_palette) @@ -97,163 +167,21 @@ MainWidget::MainWidget(Optional path, Gfx::Palette startup_preview_palet { load_from_gml(theme_editor_gml); -#define __ENUMERATE_COLOR_ROLE(role) m_color_roles.append(Gfx::ColorRole::role); - ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE) -#undef __ENUMERATE_COLOR_ROLE - -#define __ENUMERATE_ALIGNMENT_ROLE(role) m_alignment_roles.append(Gfx::AlignmentRole::role); - ENUMERATE_ALIGNMENT_ROLES(__ENUMERATE_ALIGNMENT_ROLE) -#undef __ENUMERATE_ALIGNMENT_ROLE - -#define __ENUMERATE_FLAG_ROLE(role) m_flag_roles.append(Gfx::FlagRole::role); - ENUMERATE_FLAG_ROLES(__ENUMERATE_FLAG_ROLE) -#undef __ENUMERATE_FLAG_ROLE - -#define __ENUMERATE_METRIC_ROLE(role) m_metric_roles.append(Gfx::MetricRole::role); - ENUMERATE_METRIC_ROLES(__ENUMERATE_METRIC_ROLE) -#undef __ENUMERATE_METRIC_ROLE - -#define __ENUMERATE_PATH_ROLE(role) m_path_roles.append(Gfx::PathRole::role); - ENUMERATE_PATH_ROLES(__ENUMERATE_PATH_ROLE) -#undef __ENUMERATE_PATH_ROLE + m_alignment_model = MUST(AlignmentModel::try_create()); m_preview_widget = find_descendant_of_type_named("preview_frame") ->add(startup_preview_palette); - auto& color_combo_box = *find_descendant_of_type_named("color_combo_box"); - auto& color_input = *find_descendant_of_type_named("color_input"); - - auto& alignment_combo_box = *find_descendant_of_type_named("alignment_combo_box"); - auto& alignment_input = *find_descendant_of_type_named("alignment_input"); - - auto& flag_combo_box = *find_descendant_of_type_named("flag_combo_box"); - auto& flag_input = *find_descendant_of_type_named("flag_input"); - - auto& metric_combo_box = *find_descendant_of_type_named("metric_combo_box"); - auto& metric_input = *find_descendant_of_type_named("metric_input"); - - auto& path_combo_box = *find_descendant_of_type_named("path_combo_box"); - auto& path_input = *find_descendant_of_type_named("path_input"); - auto& path_picker_button = *find_descendant_of_type_named("path_picker_button"); - - color_combo_box.set_model(MUST(RoleModel::try_create(m_color_roles))); - color_combo_box.on_change = [&](auto&, auto& index) { - auto role = index.model()->data(index, GUI::ModelRole::Custom).to_color_role(); - color_input.set_color(m_preview_widget->preview_palette().color(role), GUI::AllowCallback::No); - }; - color_combo_box.set_selected_index((size_t)Gfx::ColorRole::Window - 1); - - color_input.on_change = [&] { - auto role = color_combo_box.model()->index(color_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_color_role(); - auto preview_palette = m_preview_widget->preview_palette(); - preview_palette.set_color(role, color_input.color()); - m_preview_widget->set_preview_palette(preview_palette); - }; - color_input.set_color(startup_preview_palette.color(Gfx::ColorRole::Window), GUI::AllowCallback::No); - - alignment_combo_box.set_model(MUST(RoleModel::try_create(m_alignment_roles))); - alignment_combo_box.on_change = [&](auto&, auto& index) { - auto role = index.model()->data(index, GUI::ModelRole::Custom).to_alignment_role(); - alignment_input.set_selected_index((size_t)m_preview_widget->preview_palette().alignment(role), GUI::AllowCallback::No); - }; - alignment_combo_box.set_selected_index((size_t)Gfx::AlignmentRole::TitleAlignment - 1); - - alignment_input.set_only_allow_values_from_model(true); - alignment_input.set_model(MUST(AlignmentModel::try_create())); - alignment_input.set_selected_index((size_t)startup_preview_palette.alignment(Gfx::AlignmentRole::TitleAlignment), GUI::AllowCallback::No); - alignment_input.on_change = [&](auto&, auto& index) { - auto role = alignment_combo_box.model()->index(alignment_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_alignment_role(); - auto preview_palette = m_preview_widget->preview_palette(); - - preview_palette.set_alignment(role, index.data(GUI::ModelRole::Custom).to_text_alignment(Gfx::TextAlignment::CenterLeft)); - m_preview_widget->set_preview_palette(preview_palette); - }; - - flag_combo_box.set_model(MUST(RoleModel::try_create(m_flag_roles))); - flag_combo_box.on_change = [&](auto&, auto& index) { - auto role = index.model()->data(index, GUI::ModelRole::Custom).to_flag_role(); - flag_input.set_checked(m_preview_widget->preview_palette().flag(role), GUI::AllowCallback::No); - }; - flag_combo_box.set_selected_index((size_t)Gfx::FlagRole::IsDark - 1); - - flag_input.on_checked = [&](bool checked) { - auto role = flag_combo_box.model()->index(flag_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_flag_role(); - auto preview_palette = m_preview_widget->preview_palette(); - preview_palette.set_flag(role, checked); - m_preview_widget->set_preview_palette(preview_palette); - }; - flag_input.set_checked(startup_preview_palette.flag(Gfx::FlagRole::IsDark), GUI::AllowCallback::No); - - metric_combo_box.set_model(MUST(RoleModel::try_create(m_metric_roles))); - metric_combo_box.on_change = [&](auto&, auto& index) { - auto role = index.model()->data(index, GUI::ModelRole::Custom).to_metric_role(); - metric_input.set_value(m_preview_widget->preview_palette().metric(role), GUI::AllowCallback::No); - }; - metric_combo_box.set_selected_index((size_t)Gfx::MetricRole::TitleButtonHeight - 1); - - metric_input.on_change = [&](int value) { - auto role = metric_combo_box.model()->index(metric_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_metric_role(); - auto preview_palette = m_preview_widget->preview_palette(); - preview_palette.set_metric(role, value); - m_preview_widget->set_preview_palette(preview_palette); - }; - metric_input.set_value(startup_preview_palette.metric(Gfx::MetricRole::TitleButtonHeight), GUI::AllowCallback::No); - - path_combo_box.set_model(MUST(RoleModel::try_create(m_path_roles))); - path_combo_box.on_change = [&](auto&, auto& index) { - auto role = index.model()->data(index, GUI::ModelRole::Custom).to_path_role(); - path_input.set_text(m_preview_widget->preview_palette().path(role), GUI::AllowCallback::No); - }; - path_combo_box.set_selected_index((size_t)Gfx::PathRole::TitleButtonIcons - 1); - - path_input.on_change = [&] { - auto role = path_combo_box.model()->index(path_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_path_role(); - auto preview_palette = m_preview_widget->preview_palette(); - preview_palette.set_path(role, path_input.text()); - m_preview_widget->set_preview_palette(preview_palette); - }; - path_input.set_text(startup_preview_palette.path(Gfx::PathRole::TitleButtonIcons), GUI::AllowCallback::No); - - path_picker_button.on_click = [&](auto) { - auto role = path_combo_box.model()->index(path_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_path_role(); - bool open_folder = (role == Gfx::PathRole::TitleButtonIcons); - auto window_title = String::formatted(open_folder ? "Select {} folder" : "Select {} file", path_combo_box.text()); - auto target_path = path_input.text(); - if (Core::File::exists(target_path)) { - if (!Core::File::is_directory(target_path)) - target_path = LexicalPath::dirname(target_path); - } else { - target_path = "/res/icons"; - } - auto result = GUI::FilePicker::get_open_filepath(window(), window_title, target_path, open_folder); - if (!result.has_value()) - return; - path_input.set_text(*result); - }; + m_property_tabs = find_descendant_of_type_named("property_tabs"); + add_property_tab(window_tab); + add_property_tab(widgets_tab); + add_property_tab(syntax_highlighting_tab); m_preview_widget->on_palette_change = [&] { window()->set_modified(true); }; m_preview_widget->on_theme_load_from_file = [&](String const& new_path) { - set_path(new_path); - - auto selected_color_role = color_combo_box.model()->index(color_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_color_role(); - color_input.set_color(m_preview_widget->preview_palette().color(selected_color_role), GUI::AllowCallback::No); - - auto selected_alignment_role = alignment_combo_box.model()->index(alignment_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_alignment_role(); - alignment_input.set_selected_index((size_t)(m_preview_widget->preview_palette().alignment(selected_alignment_role), GUI::AllowCallback::No)); - - auto selected_flag_role = flag_combo_box.model()->index(flag_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_flag_role(); - flag_input.set_checked(m_preview_widget->preview_palette().flag(selected_flag_role), GUI::AllowCallback::No); - - auto selected_metric_role = metric_combo_box.model()->index(metric_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_metric_role(); - metric_input.set_value(m_preview_widget->preview_palette().metric(selected_metric_role), GUI::AllowCallback::No); - - auto selected_path_role = path_combo_box.model()->index(path_combo_box.selected_index()).data(GUI::ModelRole::Custom).to_path_role(); - path_input.set_text(m_preview_widget->preview_palette().path(selected_path_role), GUI::AllowCallback::No); - - m_last_modified_time = Time::now_monotonic(); - window()->set_modified(false); + load_from_file(new_path); }; } @@ -393,25 +321,25 @@ void MainWidget::save_to_file(Core::File& file) { auto theme = Core::ConfigFile::open(file.filename(), file.leak_fd()).release_value_but_fixme_should_propagate_errors(); - for (auto role : m_alignment_roles) { - theme->write_entry("Alignments", to_string(role), to_string(m_preview_widget->preview_palette().alignment(role))); - } +#define __ENUMERATE_ALIGNMENT_ROLE(role) theme->write_entry("Alignments", to_string(Gfx::AlignmentRole::role), to_string(m_preview_widget->preview_palette().alignment(Gfx::AlignmentRole::role))); + ENUMERATE_ALIGNMENT_ROLES(__ENUMERATE_ALIGNMENT_ROLE) +#undef __ENUMERATE_ALIGNMENT_ROLE - for (auto role : m_color_roles) { - theme->write_entry("Colors", to_string(role), m_preview_widget->preview_palette().color(role).to_string()); - } +#define __ENUMERATE_COLOR_ROLE(role) theme->write_entry("Colors", to_string(Gfx::ColorRole::role), m_preview_widget->preview_palette().color(Gfx::ColorRole::role).to_string()); + ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE) +#undef __ENUMERATE_COLOR_ROLE - for (auto role : m_flag_roles) { - theme->write_bool_entry("Flags", to_string(role), m_preview_widget->preview_palette().flag(role)); - } +#define __ENUMERATE_FLAG_ROLE(role) theme->write_bool_entry("Flags", to_string(Gfx::FlagRole::role), m_preview_widget->preview_palette().flag(Gfx::FlagRole::role)); + ENUMERATE_FLAG_ROLES(__ENUMERATE_FLAG_ROLE) +#undef __ENUMERATE_FLAG_ROLE - for (auto role : m_metric_roles) { - theme->write_num_entry("Metrics", to_string(role), m_preview_widget->preview_palette().metric(role)); - } +#define __ENUMERATE_METRIC_ROLE(role) theme->write_num_entry("Metrics", to_string(Gfx::MetricRole::role), m_preview_widget->preview_palette().metric(Gfx::MetricRole::role)); + ENUMERATE_METRIC_ROLES(__ENUMERATE_METRIC_ROLE) +#undef __ENUMERATE_METRIC_ROLE - for (auto role : m_path_roles) { - theme->write_entry("Paths", to_string(role), m_preview_widget->preview_palette().path(role)); - } +#define __ENUMERATE_PATH_ROLE(role) theme->write_entry("Paths", to_string(Gfx::PathRole::role), m_preview_widget->preview_palette().path(Gfx::PathRole::role)); + ENUMERATE_PATH_ROLES(__ENUMERATE_PATH_ROLE) +#undef __ENUMERATE_PATH_ROLE auto sync_result = theme->sync(); if (sync_result.is_error()) { @@ -423,4 +351,193 @@ void MainWidget::save_to_file(Core::File& file) } } +void MainWidget::add_property_tab(PropertyTab const& property_tab) +{ + auto& scrollable_container = m_property_tabs->add_tab(property_tab.title); + scrollable_container.set_should_hide_unnecessary_scrollbars(true); + + auto properties_list = GUI::Widget::construct(); + scrollable_container.set_widget(properties_list); + properties_list->set_layout(); + properties_list->layout()->set_spacing(12); + properties_list->layout()->set_margins({ 8 }); + + for (auto const& property : property_tab.properties) { + NonnullRefPtr row_widget = properties_list->add(); + row_widget->set_fixed_height(24); + + property.role.visit( + [&](Gfx::AlignmentRole role) { + row_widget->load_from_gml(alignment_property_gml); + + auto& name_label = *row_widget->find_descendant_of_type_named("name"); + name_label.set_text(to_string(role)); + + auto& alignment_picker = *row_widget->find_descendant_of_type_named("combo_box"); + alignment_picker.set_model(*m_alignment_model); + alignment_picker.on_change = [&, role](auto&, auto& index) { + set_alignment(role, index.data(GUI::ModelRole::Custom).to_text_alignment(Gfx::TextAlignment::CenterLeft)); + }; + alignment_picker.set_selected_index(m_alignment_model->index_of(m_preview_widget->preview_palette().alignment(role)), GUI::AllowCallback::No); + + VERIFY(m_alignment_inputs[to_underlying(role)].is_null()); + m_alignment_inputs[to_underlying(role)] = alignment_picker; + }, + [&](Gfx::ColorRole role) { + row_widget->load_from_gml(color_property_gml); + + auto& name_label = *row_widget->find_descendant_of_type_named("name"); + name_label.set_text(to_string(role)); + + auto& color_input = *row_widget->find_descendant_of_type_named("color_input"); + color_input.on_change = [&, role] { + set_color(role, color_input.color()); + }; + color_input.set_color(m_preview_widget->preview_palette().color(role), GUI::AllowCallback::No); + + VERIFY(m_color_inputs[to_underlying(role)].is_null()); + m_color_inputs[to_underlying(role)] = color_input; + }, + [&](Gfx::FlagRole role) { + row_widget->load_from_gml(flag_property_gml); + + auto& checkbox = *row_widget->find_descendant_of_type_named("checkbox"); + checkbox.set_text(to_string(role)); + checkbox.on_checked = [&, role](bool checked) { + set_flag(role, checked); + }; + checkbox.set_checked(m_preview_widget->preview_palette().flag(role), GUI::AllowCallback::No); + + VERIFY(m_flag_inputs[to_underlying(role)].is_null()); + m_flag_inputs[to_underlying(role)] = checkbox; + }, + [&](Gfx::MetricRole role) { + row_widget->load_from_gml(metric_property_gml); + + auto& name_label = *row_widget->find_descendant_of_type_named("name"); + name_label.set_text(to_string(role)); + + auto& spin_box = *row_widget->find_descendant_of_type_named("spin_box"); + spin_box.on_change = [&, role](int value) { + set_metric(role, value); + }; + spin_box.set_value(m_preview_widget->preview_palette().metric(role), GUI::AllowCallback::No); + + VERIFY(m_metric_inputs[to_underlying(role)].is_null()); + m_metric_inputs[to_underlying(role)] = spin_box; + }, + [&](Gfx::PathRole role) { + row_widget->load_from_gml(path_property_gml); + + auto& name_label = *row_widget->find_descendant_of_type_named("name"); + name_label.set_text(to_string(role)); + + auto& path_input = *row_widget->find_descendant_of_type_named("path_input"); + path_input.on_change = [&, role] { + set_path(role, path_input.text()); + }; + path_input.set_text(m_preview_widget->preview_palette().path(role), GUI::AllowCallback::No); + + auto& path_picker_button = *row_widget->find_descendant_of_type_named("path_picker_button"); + auto picker_target = (role == Gfx::PathRole::TitleButtonIcons) ? PathPickerTarget::Folder : PathPickerTarget::File; + path_picker_button.on_click = [&, role, picker_target](auto) { + show_path_picker_dialog(to_string(role), path_input, picker_target); + }; + + VERIFY(m_path_inputs[to_underlying(role)].is_null()); + m_path_inputs[to_underlying(role)] = path_input; + }); + } +} + +void MainWidget::set_alignment(Gfx::AlignmentRole role, Gfx::TextAlignment value) +{ + auto preview_palette = m_preview_widget->preview_palette(); + preview_palette.set_alignment(role, value); + m_preview_widget->set_preview_palette(preview_palette); +} + +void MainWidget::set_color(Gfx::ColorRole role, Gfx::Color value) +{ + auto preview_palette = m_preview_widget->preview_palette(); + preview_palette.set_color(role, value); + m_preview_widget->set_preview_palette(preview_palette); +} + +void MainWidget::set_flag(Gfx::FlagRole role, bool value) +{ + auto preview_palette = m_preview_widget->preview_palette(); + preview_palette.set_flag(role, value); + m_preview_widget->set_preview_palette(preview_palette); +} + +void MainWidget::set_metric(Gfx::MetricRole role, int value) +{ + auto preview_palette = m_preview_widget->preview_palette(); + preview_palette.set_metric(role, value); + m_preview_widget->set_preview_palette(preview_palette); +} + +void MainWidget::set_path(Gfx::PathRole role, String value) +{ + auto preview_palette = m_preview_widget->preview_palette(); + preview_palette.set_path(role, value); + m_preview_widget->set_preview_palette(preview_palette); +} + +void MainWidget::show_path_picker_dialog(StringView property_display_name, GUI::TextBox& path_input, PathPickerTarget path_picker_target) +{ + bool open_folder = path_picker_target == PathPickerTarget::Folder; + auto window_title = String::formatted(open_folder ? "Select {} folder" : "Select {} file", property_display_name); + auto target_path = path_input.text(); + if (Core::File::exists(target_path)) { + if (!Core::File::is_directory(target_path)) + target_path = LexicalPath::dirname(target_path); + } else { + target_path = "/res/icons"; + } + auto result = GUI::FilePicker::get_open_filepath(window(), window_title, target_path, open_folder); + if (!result.has_value()) + return; + path_input.set_text(*result); +} + +void MainWidget::load_from_file(String const& new_path) +{ + set_path(new_path); + +#define __ENUMERATE_ALIGNMENT_ROLE(role) \ + if (auto alignment_input = m_alignment_inputs[to_underlying(Gfx::AlignmentRole::role)]) \ + alignment_input->set_selected_index(m_alignment_model->index_of(m_preview_widget->preview_palette().alignment(Gfx::AlignmentRole::role)), GUI::AllowCallback::No); + ENUMERATE_ALIGNMENT_ROLES(__ENUMERATE_ALIGNMENT_ROLE) +#undef __ENUMERATE_ALIGNMENT_ROLE + +#define __ENUMERATE_COLOR_ROLE(role) \ + if (auto color_input = m_color_inputs[to_underlying(Gfx::ColorRole::role)]) \ + color_input->set_color(m_preview_widget->preview_palette().color(Gfx::ColorRole::role), GUI::AllowCallback::No); + ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE) +#undef __ENUMERATE_COLOR_ROLE + +#define __ENUMERATE_FLAG_ROLE(role) \ + if (auto flag_input = m_flag_inputs[to_underlying(Gfx::FlagRole::role)]) \ + flag_input->set_checked(m_preview_widget->preview_palette().flag(Gfx::FlagRole::role), GUI::AllowCallback::No); + ENUMERATE_FLAG_ROLES(__ENUMERATE_FLAG_ROLE) +#undef __ENUMERATE_FLAG_ROLE + +#define __ENUMERATE_METRIC_ROLE(role) \ + if (auto metric_input = m_metric_inputs[to_underlying(Gfx::MetricRole::role)]) \ + metric_input->set_value(m_preview_widget->preview_palette().metric(Gfx::MetricRole::role), GUI::AllowCallback::No); + ENUMERATE_METRIC_ROLES(__ENUMERATE_METRIC_ROLE) +#undef __ENUMERATE_METRIC_ROLE + +#define __ENUMERATE_PATH_ROLE(role) \ + if (auto path_input = m_path_inputs[to_underlying(Gfx::PathRole::role)]) \ + path_input->set_text(m_preview_widget->preview_palette().path(Gfx::PathRole::role), GUI::AllowCallback::No); + ENUMERATE_PATH_ROLES(__ENUMERATE_PATH_ROLE) +#undef __ENUMERATE_PATH_ROLE + + m_last_modified_time = Time::now_monotonic(); + window()->set_modified(false); +} + } diff --git a/Userland/Applications/ThemeEditor/MainWidget.h b/Userland/Applications/ThemeEditor/MainWidget.h index 250742eed3..8dfad9b105 100644 --- a/Userland/Applications/ThemeEditor/MainWidget.h +++ b/Userland/Applications/ThemeEditor/MainWidget.h @@ -7,13 +7,70 @@ #pragma once #include "PreviewWidget.h" +#include #include -#include +#include +#include +#include +#include +#include +#include #include #include namespace ThemeEditor { +class AlignmentModel final : public GUI::Model { +public: + static ErrorOr> try_create() + { + return adopt_nonnull_ref_or_enomem(new (nothrow) AlignmentModel()); + } + + virtual ~AlignmentModel() = default; + + virtual int row_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override { return 3; } + virtual int column_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override { return 2; } + + virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role) const override + { + if (role == GUI::ModelRole::Display) + return m_alignments[index.row()].title; + if (role == GUI::ModelRole::Custom) + return m_alignments[index.row()].setting_value; + + return {}; + } + + size_t index_of(Gfx::TextAlignment alignment) const + { + auto match = m_alignments.find_if([&](auto& it) { return it.setting_value == alignment; }); + return match.index(); + } + +private: + AlignmentModel() = default; + + struct AlignmentValue { + String title; + Gfx::TextAlignment setting_value; + }; + Vector m_alignments { + { "Center", Gfx::TextAlignment::Center }, + { "Left", Gfx::TextAlignment::CenterLeft }, + { "Right", Gfx::TextAlignment::CenterRight }, + }; +}; + +struct Property { + Variant role; +}; + +struct PropertyTab { + String title; + Vector properties; +}; + class MainWidget final : public GUI::Widget { C_OBJECT(MainWidget); @@ -27,20 +84,37 @@ public: private: explicit MainWidget(Optional path, Gfx::Palette startup_preview_palette); + void load_from_file(String const& path); void save_to_file(Core::File&); void set_path(String); + void add_property_tab(PropertyTab const&); + void set_alignment(Gfx::AlignmentRole, Gfx::TextAlignment); + void set_color(Gfx::ColorRole, Gfx::Color); + void set_flag(Gfx::FlagRole, bool); + void set_metric(Gfx::MetricRole, int); + void set_path(Gfx::PathRole, String); + + enum class PathPickerTarget { + File, + Folder, + }; + void show_path_picker_dialog(StringView property_display_name, GUI::TextBox&, PathPickerTarget); + RefPtr m_preview_widget; + RefPtr m_property_tabs; RefPtr m_save_action; Optional m_path; Time m_last_modified_time { Time::now_monotonic() }; - Vector m_alignment_roles; - Vector m_color_roles; - Vector m_flag_roles; - Vector m_metric_roles; - Vector m_path_roles; + RefPtr m_alignment_model; + + Array, to_underlying(Gfx::AlignmentRole::__Count)> m_alignment_inputs; + Array, to_underlying(Gfx::ColorRole::__Count)> m_color_inputs; + Array, to_underlying(Gfx::FlagRole::__Count)> m_flag_inputs; + Array, to_underlying(Gfx::MetricRole::__Count)> m_metric_inputs; + Array, to_underlying(Gfx::PathRole::__Count)> m_path_inputs; OwnPtr m_preview_type_action_group; }; diff --git a/Userland/Applications/ThemeEditor/MetricProperty.gml b/Userland/Applications/ThemeEditor/MetricProperty.gml new file mode 100644 index 0000000000..b49896bcff --- /dev/null +++ b/Userland/Applications/ThemeEditor/MetricProperty.gml @@ -0,0 +1,17 @@ +@GUI::Frame { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + shrink_to_fit: true + + @GUI::Label { + name: "name" + text: "Some metric" + text_alignment: "CenterLeft" + fixed_width: 200 + } + + @GUI::SpinBox { + name: "spin_box" + } +} diff --git a/Userland/Applications/ThemeEditor/PathProperty.gml b/Userland/Applications/ThemeEditor/PathProperty.gml new file mode 100644 index 0000000000..8ef7aec6b2 --- /dev/null +++ b/Userland/Applications/ThemeEditor/PathProperty.gml @@ -0,0 +1,24 @@ +@GUI::Frame { + layout: @GUI::HorizontalBoxLayout { + spacing: 4 + } + shrink_to_fit: true + + @GUI::Label { + name: "name" + text: "Some path" + text_alignment: "CenterLeft" + fixed_width: 200 + } + + @GUI::TextBox { + name: "path_input" + } + + @GUI::Button { + name: "path_picker_button" + fixed_width: 20 + text: "..." + tooltip: "Choose..." + } +} diff --git a/Userland/Applications/ThemeEditor/ThemeEditor.gml b/Userland/Applications/ThemeEditor/ThemeEditor.gml index 2bbedb7519..36d6f48bab 100644 --- a/Userland/Applications/ThemeEditor/ThemeEditor.gml +++ b/Userland/Applications/ThemeEditor/ThemeEditor.gml @@ -1,5 +1,5 @@ @GUI::Widget { - layout: @GUI::VerticalBoxLayout {} + layout: @GUI::HorizontalBoxLayout {} fill_with_background_color: true @GUI::Frame { @@ -7,104 +7,7 @@ name: "preview_frame" } - @GUI::GroupBox { - layout: @GUI::HorizontalBoxLayout { - margins: [4, 4, 4, 4] - } - shrink_to_fit: true - title: "Colors" - - @GUI::ComboBox { - name: "color_combo_box" - model_only: true - fixed_width: 230 - } - - @GUI::ColorInput { - name: "color_input" - } - } - - @GUI::GroupBox { - layout: @GUI::HorizontalBoxLayout { - margins: [4, 4, 4, 4] - } - shrink_to_fit: true - title: "Alignments" - - @GUI::ComboBox { - name: "alignment_combo_box" - model_only: true - fixed_width: 230 - } - - @GUI::ComboBox { - name: "alignment_input" - } - } - - @GUI::GroupBox { - layout: @GUI::HorizontalBoxLayout { - margins: [4, 4, 4, 4] - } - shrink_to_fit: true - title: "Flags" - - @GUI::ComboBox { - name: "flag_combo_box" - model_only: true - fixed_width: 230 - } - - @GUI::Widget {} - - @GUI::CheckBox { - name: "flag_input" - fixed_width: 13 - } - } - - @GUI::GroupBox { - layout: @GUI::HorizontalBoxLayout { - margins: [4, 4, 4, 4] - } - shrink_to_fit: true - title: "Metrics" - - @GUI::ComboBox { - name: "metric_combo_box" - model_only: true - fixed_width: 230 - } - - @GUI::SpinBox { - name: "metric_input" - } - } - - @GUI::GroupBox { - layout: @GUI::HorizontalBoxLayout { - margins: [4, 4, 4, 4] - } - shrink_to_fit: true - title: "Paths" - - @GUI::ComboBox { - name: "path_combo_box" - model_only: true - fixed_width: 230 - } - - @GUI::TextBox { - name: "path_input" - mode: "Editable" - } - - @GUI::Button { - name: "path_picker_button" - fixed_width: 20 - text: "..." - tooltip: "Choose..." - } + @GUI::TabWidget { + name: "property_tabs" } } diff --git a/Userland/Applications/ThemeEditor/main.cpp b/Userland/Applications/ThemeEditor/main.cpp index 5af9fdd4ce..e9ac5e965c 100644 --- a/Userland/Applications/ThemeEditor/main.cpp +++ b/Userland/Applications/ThemeEditor/main.cpp @@ -65,7 +65,7 @@ ErrorOr serenity_main(Main::Arguments arguments) return main_widget->request_close(); }; - window->resize(480, 520); + window->resize(820, 520); window->set_resizable(false); window->show(); window->set_icon(app_icon.bitmap_for_size(16));