1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 23:07:35 +00:00

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.
This commit is contained in:
Sam Atkins 2022-05-11 12:56:16 +01:00 committed by Andreas Kling
parent aadb35ff46
commit 423383e9aa
10 changed files with 514 additions and 322 deletions

View file

@ -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
}
}

View file

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

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -9,87 +9,157 @@
*/
#include "MainWidget.h"
#include <Applications/ThemeEditor/AlignmentPropertyGML.h>
#include <Applications/ThemeEditor/ColorPropertyGML.h>
#include <Applications/ThemeEditor/FlagPropertyGML.h>
#include <Applications/ThemeEditor/MetricPropertyGML.h>
#include <Applications/ThemeEditor/PathPropertyGML.h>
#include <Applications/ThemeEditor/ThemeEditorGML.h>
#include <LibFileSystemAccessClient/Client.h>
#include <LibGUI/ActionGroup.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/ColorInput.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/Frame.h>
#include <LibGUI/Icon.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Menubar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/SpinBox.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ScrollableContainerWidget.h>
#include <LibGfx/Filters/ColorBlindnessFilter.h>
namespace ThemeEditor {
template<typename T>
class RoleModel final : public GUI::ItemListModel<T> {
public:
static ErrorOr<NonnullRefPtr<RoleModel>> try_create(Vector<T> const& data)
static const PropertyTab window_tab {
"Windows",
{
return adopt_nonnull_ref_or_enomem(new (nothrow) RoleModel<T>(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<T>::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<T> const& data)
: GUI::ItemListModel<T>(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<NonnullRefPtr<AlignmentModel>> 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<AlignmentValue> m_alignments {
{ "Center", Gfx::TextAlignment::Center },
{ "Left", Gfx::TextAlignment::CenterLeft },
{ "Right", Gfx::TextAlignment::CenterRight },
};
};
MainWidget::MainWidget(Optional<String> path, Gfx::Palette startup_preview_palette)
@ -97,163 +167,21 @@ MainWidget::MainWidget(Optional<String> 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<GUI::Frame>("preview_frame")
->add<ThemeEditor::PreviewWidget>(startup_preview_palette);
auto& color_combo_box = *find_descendant_of_type_named<GUI::ComboBox>("color_combo_box");
auto& color_input = *find_descendant_of_type_named<GUI::ColorInput>("color_input");
auto& alignment_combo_box = *find_descendant_of_type_named<GUI::ComboBox>("alignment_combo_box");
auto& alignment_input = *find_descendant_of_type_named<GUI::ComboBox>("alignment_input");
auto& flag_combo_box = *find_descendant_of_type_named<GUI::ComboBox>("flag_combo_box");
auto& flag_input = *find_descendant_of_type_named<GUI::CheckBox>("flag_input");
auto& metric_combo_box = *find_descendant_of_type_named<GUI::ComboBox>("metric_combo_box");
auto& metric_input = *find_descendant_of_type_named<GUI::SpinBox>("metric_input");
auto& path_combo_box = *find_descendant_of_type_named<GUI::ComboBox>("path_combo_box");
auto& path_input = *find_descendant_of_type_named<GUI::TextBox>("path_input");
auto& path_picker_button = *find_descendant_of_type_named<GUI::Button>("path_picker_button");
color_combo_box.set_model(MUST(RoleModel<Gfx::ColorRole>::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<Gfx::AlignmentRole>::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<Gfx::FlagRole>::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<Gfx::MetricRole>::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<Gfx::PathRole>::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<GUI::TabWidget>("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<GUI::ScrollableContainerWidget>(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<GUI::VerticalBoxLayout>();
properties_list->layout()->set_spacing(12);
properties_list->layout()->set_margins({ 8 });
for (auto const& property : property_tab.properties) {
NonnullRefPtr<GUI::Widget> row_widget = properties_list->add<GUI::Widget>();
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<GUI::Label>("name");
name_label.set_text(to_string(role));
auto& alignment_picker = *row_widget->find_descendant_of_type_named<GUI::ComboBox>("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<GUI::Label>("name");
name_label.set_text(to_string(role));
auto& color_input = *row_widget->find_descendant_of_type_named<GUI::ColorInput>("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<GUI::CheckBox>("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<GUI::Label>("name");
name_label.set_text(to_string(role));
auto& spin_box = *row_widget->find_descendant_of_type_named<GUI::SpinBox>("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<GUI::Label>("name");
name_label.set_text(to_string(role));
auto& path_input = *row_widget->find_descendant_of_type_named<GUI::TextBox>("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<GUI::Button>("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);
}
}

View file

@ -7,13 +7,70 @@
#pragma once
#include "PreviewWidget.h"
#include <AK/FixedArray.h>
#include <AK/Time.h>
#include <LibGUI/Widget.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/ColorInput.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/SpinBox.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Window.h>
#include <LibGfx/SystemTheme.h>
namespace ThemeEditor {
class AlignmentModel final : public GUI::Model {
public:
static ErrorOr<NonnullRefPtr<AlignmentModel>> 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<AlignmentValue> m_alignments {
{ "Center", Gfx::TextAlignment::Center },
{ "Left", Gfx::TextAlignment::CenterLeft },
{ "Right", Gfx::TextAlignment::CenterRight },
};
};
struct Property {
Variant<Gfx::AlignmentRole, Gfx::ColorRole, Gfx::FlagRole, Gfx::MetricRole, Gfx::PathRole> role;
};
struct PropertyTab {
String title;
Vector<Property> properties;
};
class MainWidget final : public GUI::Widget {
C_OBJECT(MainWidget);
@ -27,20 +84,37 @@ public:
private:
explicit MainWidget(Optional<String> 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<PreviewWidget> m_preview_widget;
RefPtr<GUI::TabWidget> m_property_tabs;
RefPtr<GUI::Action> m_save_action;
Optional<String> m_path;
Time m_last_modified_time { Time::now_monotonic() };
Vector<Gfx::AlignmentRole> m_alignment_roles;
Vector<Gfx::ColorRole> m_color_roles;
Vector<Gfx::FlagRole> m_flag_roles;
Vector<Gfx::MetricRole> m_metric_roles;
Vector<Gfx::PathRole> m_path_roles;
RefPtr<AlignmentModel> m_alignment_model;
Array<RefPtr<GUI::ComboBox>, to_underlying(Gfx::AlignmentRole::__Count)> m_alignment_inputs;
Array<RefPtr<GUI::ColorInput>, to_underlying(Gfx::ColorRole::__Count)> m_color_inputs;
Array<RefPtr<GUI::CheckBox>, to_underlying(Gfx::FlagRole::__Count)> m_flag_inputs;
Array<RefPtr<GUI::SpinBox>, to_underlying(Gfx::MetricRole::__Count)> m_metric_inputs;
Array<RefPtr<GUI::TextBox>, to_underlying(Gfx::PathRole::__Count)> m_path_inputs;
OwnPtr<GUI::ActionGroup> m_preview_type_action_group;
};

View file

@ -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"
}
}

View file

@ -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..."
}
}

View file

@ -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"
}
}

View file

@ -65,7 +65,7 @@ ErrorOr<int> 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));