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));