From 36ef41626acbd87ba1cbc1e2d2898548d52f47b7 Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Fri, 19 Mar 2021 17:47:53 +0100 Subject: [PATCH] sculpt: keyboard-layout selection dialog This patch extends the settings dialog with the ability to select the keyboard layout between the options that are included in the sculpt image. The manual configuration is of course still possible by editing the /config/event_filter directly. If both the fonts configuration and the event-filter configuration are managed manually, the settings button and window are not displayed. Fixes #4055 --- .../raw/drivers_managed-pc/drivers.config | 2 +- repos/gems/run/sculpt.run | 1 - repos/gems/src/app/sculpt_manager/main.cc | 160 +++++++++++++++++- .../src/app/sculpt_manager/model/settings.h | 47 ++++- .../app/sculpt_manager/view/panel_dialog.cc | 22 +-- .../app/sculpt_manager/view/panel_dialog.h | 1 + .../sculpt_manager/view/radio_choice_dialog.h | 158 +++++++++++++++++ .../app/sculpt_manager/view/settings_dialog.h | 113 ++++++++----- 8 files changed, 440 insertions(+), 64 deletions(-) create mode 100644 repos/gems/src/app/sculpt_manager/view/radio_choice_dialog.h diff --git a/repos/gems/recipes/raw/drivers_managed-pc/drivers.config b/repos/gems/recipes/raw/drivers_managed-pc/drivers.config index 27997ac861..10f5355aac 100644 --- a/repos/gems/recipes/raw/drivers_managed-pc/drivers.config +++ b/repos/gems/recipes/raw/drivers_managed-pc/drivers.config @@ -209,7 +209,7 @@ - + diff --git a/repos/gems/run/sculpt.run b/repos/gems/run/sculpt.run index 04d66f8075..c64c0af3c4 100644 --- a/repos/gems/run/sculpt.run +++ b/repos/gems/run/sculpt.run @@ -105,7 +105,6 @@ install_config { - diff --git a/repos/gems/src/app/sculpt_manager/main.cc b/repos/gems/src/app/sculpt_manager/main.cc index 6e5ad0547e..e3afd47163 100644 --- a/repos/gems/src/app/sculpt_manager/main.cc +++ b/repos/gems/src/app/sculpt_manager/main.cc @@ -109,14 +109,42 @@ struct Sculpt::Main : Input_event_handler, _font_size_px = px; }); } }); } }); }); _handle_gui_mode(); + + /* visibility of fonts section of settings dialog may have changed */ + _settings_menu_view.generate(); + + /* visibility of settings button may have changed */ + _refresh_panel_and_window_layout(); } Managed_config
_event_filter_config { _env, "config", "event_filter", *this, &Main::_handle_event_filter_config }; + void _generate_event_filter_config(Xml_generator &); + void _handle_event_filter_config(Xml_node) { - _event_filter_config.try_generate_manually_managed(); + _update_event_filter_config(); + } + + void _update_event_filter_config() + { + bool const orig_settings_available = _settings.interactive_settings_available(); + + _settings.manual_event_filter_config = + _event_filter_config.try_generate_manually_managed(); + + if (!_settings.manual_event_filter_config) + _event_filter_config.generate([&] (Xml_generator &xml) { + _generate_event_filter_config(xml); }); + + _settings_menu_view.generate(); + + /* visibility of the settings dialog may have changed */ + if (orig_settings_available != _settings.interactive_settings_available()) { + _refresh_panel_and_window_layout(); + _handle_gui_mode(); + } } @@ -347,7 +375,7 @@ struct Sculpt::Main : Input_event_handler, ** Global ** ************/ - Font_size _font_size = Font_size::MEDIUM; + Settings _settings { }; float _font_size_px = 14; @@ -378,6 +406,8 @@ struct Sculpt::Main : Input_event_handler, Panel_dialog::Tab selected_tab() const override { return _selected_tab; } + bool settings_available() const override { return _settings.interactive_settings_available(); } + /** * Dialog interface */ @@ -734,12 +764,28 @@ struct Sculpt::Main : Input_event_handler, /* * Settings_dialog::Action interface */ - void select_font_size(Font_size font_size) override + void select_font_size(Settings::Font_size font_size) override { - _font_size = font_size; + if (_settings.font_size == font_size) + return; + + _settings.font_size = font_size; _handle_gui_mode(); } + /* + * Settings_dialog::Action interface + */ + void select_keyboard_layout(Settings::Keyboard_layout::Name const &keyboard_layout) override + { + if (_settings.keyboard_layout == keyboard_layout) + return; + + _settings.keyboard_layout = keyboard_layout; + + _update_event_filter_config(); + } + Signal_handler
_fs_query_result_handler { _env.ep(), *this, &Main::_handle_fs_query_result }; @@ -974,7 +1020,7 @@ struct Sculpt::Main : Input_event_handler, Ram_quota{4*1024*1024}, Cap_quota{150}, "panel_dialog", "panel_view_hover" }; - Settings_dialog _settings_dialog { _font_size }; + Settings_dialog _settings_dialog { _settings }; Menu_view _settings_menu_view { _env, _child_states, _settings_dialog, "settings_view", Ram_quota{4*1024*1024}, Cap_quota{150}, @@ -1073,6 +1119,7 @@ struct Sculpt::Main : Input_event_handler, * Generate initial configurations */ _network.wifi_disconnect(); + _update_event_filter_config(); /* * Import initial report content @@ -1219,7 +1266,9 @@ void Sculpt::Main::_handle_window_layout() Point const pos = _settings_visible ? Point(0, avail.y1()) : Point(-size.w(), avail.y1()); - gen_window(win, Rect(pos, size)); + + if (_settings.interactive_settings_available()) + gen_window(win, Rect(pos, size)); }); _with_window(window_list, network_view_label, [&] (Xml_node win) { @@ -1330,7 +1379,9 @@ void Sculpt::Main::_handle_gui_mode() _handle_window_layout(); - if (!_fonts_config.try_generate_manually_managed()) { + _settings.manual_fonts_config = _fonts_config.try_generate_manually_managed(); + + if (!_settings.manual_fonts_config) { _font_size_px = (float)mode.area.h() / 60.0; @@ -1340,8 +1391,8 @@ void Sculpt::Main::_handle_gui_mode() */ _font_size_px = max(_font_size_px, 2.0); - if (_font_size == Font_size::SMALL) _font_size_px *= 0.85; - if (_font_size == Font_size::LARGE) _font_size_px *= 1.35; + if (_settings.font_size == Settings::Font_size::SMALL) _font_size_px *= 0.85; + if (_settings.font_size == Settings::Font_size::LARGE) _font_size_px *= 1.35; _fonts_config.generate([&] (Xml_generator &xml) { xml.attribute("copy", true); @@ -1743,6 +1794,97 @@ void Sculpt::Main::_generate_runtime_config(Xml_generator &xml) const } +void Sculpt::Main::_generate_event_filter_config(Xml_generator &xml) +{ + auto gen_include = [&] (auto rom) { + xml.node("include", [&] () { + xml.attribute("rom", rom); }); }; + + xml.node("output", [&] () { + xml.node("chargen", [&] () { + xml.node("remap", [&] () { + + auto gen_key = [&] (auto from, auto to) { + xml.node("key", [&] () { + xml.attribute("name", from); + xml.attribute("to", to); }); }; + + gen_key("KEY_CAPSLOCK", "KEY_CAPSLOCK"); + gen_key("KEY_F12", "KEY_DASHBOARD"); + gen_key("KEY_LEFTMETA", "KEY_SCREEN"); + gen_include("numlock.remap"); + + xml.node("merge", [&] () { + + auto gen_input = [&] (auto name) { + xml.node("input", [&] () { + xml.attribute("name", name); }); }; + + xml.node("accelerate", [&] () { + xml.attribute("max", 50); + xml.attribute("sensitivity_percent", 1000); + xml.attribute("curve", 127); + + xml.node("button-scroll", [&] () { + gen_input("ps2"); + + xml.node("vertical", [&] () { + xml.attribute("button", "BTN_MIDDLE"); + xml.attribute("speed_percent", -10); }); + + xml.node("horizontal", [&] () { + xml.attribute("button", "BTN_MIDDLE"); + xml.attribute("speed_percent", -10); }); + }); + }); + + gen_input("usb"); + gen_input("touch"); + }); + }); + + auto gen_key = [&] (auto key) { + gen_named_node(xml, "key", key, [&] () {}); }; + + xml.node("mod1", [&] () { + gen_key("KEY_LEFTSHIFT"); + gen_key("KEY_RIGHTSHIFT"); }); + + xml.node("mod2", [&] () { + gen_key("KEY_LEFTCTRL"); + gen_key("KEY_RIGHTCTRL"); }); + + xml.node("mod3", [&] () { + gen_key("KEY_RIGHTALT"); /* AltGr */ }); + + xml.node("mod4", [&] () { + xml.node("rom", [&] () { + xml.attribute("name", "capslock"); }); }); + + xml.node("repeat", [&] () { + xml.attribute("delay_ms", 230); + xml.attribute("rate_ms", 40); }); + + using Keyboard_layout = Settings::Keyboard_layout; + Keyboard_layout::for_each([&] (Keyboard_layout const &layout) { + if (layout.name == _settings.keyboard_layout) + gen_include(layout.chargen_file); }); + + gen_include("special.chargen"); + }); + }); + + auto gen_policy = [&] (auto label) { + xml.node("policy", [&] () { + xml.attribute("label", label); + xml.attribute("input", label); }); }; + + gen_policy("ps2"); + gen_policy("usb"); + gen_policy("touch"); +} + + void Component::construct(Genode::Env &env) { static Sculpt::Main main(env); diff --git a/repos/gems/src/app/sculpt_manager/model/settings.h b/repos/gems/src/app/sculpt_manager/model/settings.h index 4f82e6419b..4f997622d0 100644 --- a/repos/gems/src/app/sculpt_manager/model/settings.h +++ b/repos/gems/src/app/sculpt_manager/model/settings.h @@ -16,8 +16,51 @@ #include "types.h" -namespace Sculpt { enum class Font_size; } +namespace Sculpt { struct Settings; } -enum class Sculpt::Font_size { SMALL, MEDIUM, LARGE }; + +struct Sculpt::Settings +{ + enum class Font_size; + + enum class Font_size { SMALL, MEDIUM, LARGE }; + + Font_size font_size = Font_size::MEDIUM; + + bool manual_fonts_config = false; + + struct Keyboard_layout + { + using Name = String<32>; + + Name name; + Path chargen_file; + + template + static void for_each(FN const &fn) + { + static Keyboard_layout layouts[] = { + { .name = "French", .chargen_file = "fr_fr.chargen" }, + { .name = "German", .chargen_file = "de_de.chargen" }, + { .name = "Swiss French", .chargen_file = "fr_ch.chargen" }, + { .name = "Swiss German", .chargen_file = "de_ch.chargen" }, + { .name = "US English", .chargen_file = "en_us.chargen" }, + }; + + for (auto layout : layouts) + fn(layout); + } + }; + + Keyboard_layout::Name keyboard_layout { "US English" }; + + bool manual_event_filter_config = false; + + bool interactive_settings_available() const + { + return !manual_event_filter_config + || !manual_fonts_config; + } +}; #endif /* _MODEL__SETTINGS_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc b/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc index c3988b21c8..7536c15f34 100644 --- a/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc +++ b/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc @@ -21,19 +21,21 @@ void Panel_dialog::generate(Xml_generator &xml) const xml.node("frame", [&] () { xml.attribute("style", "unimportant"); - gen_named_node(xml, "float", "left", [&] () { - xml.attribute("west", true); - xml.node("hbox", [&] () { - xml.node("button", [&] () { - _item.gen_button_attr(xml, "settings"); - if (_state.settings_visible()) - xml.attribute("selected", true); - xml.node("label", [&] () { - xml.attribute("text", "Settings"); + if (_state.settings_available()) { + gen_named_node(xml, "float", "left", [&] () { + xml.attribute("west", true); + xml.node("hbox", [&] () { + xml.node("button", [&] () { + _item.gen_button_attr(xml, "settings"); + if (_state.settings_visible()) + xml.attribute("selected", true); + xml.node("label", [&] () { + xml.attribute("text", "Settings"); + }); }); }); }); - }); + } gen_named_node(xml, "float", "center", [&] () { xml.node("hbox", [&] () { diff --git a/repos/gems/src/app/sculpt_manager/view/panel_dialog.h b/repos/gems/src/app/sculpt_manager/view/panel_dialog.h index 6369a3019f..c032570891 100644 --- a/repos/gems/src/app/sculpt_manager/view/panel_dialog.h +++ b/repos/gems/src/app/sculpt_manager/view/panel_dialog.h @@ -36,6 +36,7 @@ struct Sculpt::Panel_dialog : Dialog virtual bool settings_visible() const = 0; virtual bool network_visible() const = 0; virtual bool inspect_tab_visible() const = 0; + virtual bool settings_available() const = 0; }; State const &_state; diff --git a/repos/gems/src/app/sculpt_manager/view/radio_choice_dialog.h b/repos/gems/src/app/sculpt_manager/view/radio_choice_dialog.h new file mode 100644 index 0000000000..5b7e220049 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/radio_choice_dialog.h @@ -0,0 +1,158 @@ +/* + * \brief Radio-button dialog + * \author Norman Feske + * \date 2021-03-19 + */ + +/* + * Copyright (C) 2021 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _VIEW__RADIO_CHOICE_DIALOG_H_ +#define _VIEW__RADIO_CHOICE_DIALOG_H_ + +#include + +namespace Sculpt { struct Radio_choice_dialog; } + + +struct Sculpt::Radio_choice_dialog : Noncopyable, Dialog +{ + typedef Hoverable_item::Id Id; + + Id const _id; + + struct Min_ex { unsigned left; unsigned right; }; + + Min_ex const _min_ex; + + Hoverable_item _choice_item { }; + + bool _unfolded = false; + + Hover_result hover(Xml_node hover) override + { + return any_hover_changed(_choice_item.match(hover, "hbox", "frame", "vbox", "hbox", "name")); + } + + void reset() override + { + _unfolded = false; + } + + struct Choice : Interface, Noncopyable + { + virtual void generate(Id const &option_id) const = 0; + }; + + void generate(Xml_generator &) const override { }; + + template + void generate(Xml_generator &xml, Id const &selected_id, FN const &fn) const + { + struct Choice_generator : Choice + { + Xml_generator &xml; + + Radio_choice_dialog const &dialog; + + Id const selected_id; + + Choice_generator(Xml_generator &xml, Id const &selected_id, + Radio_choice_dialog const &dialog) + : + xml(xml), dialog(dialog), selected_id(selected_id) + { } + + void generate(Id const &option_id) const override + { + bool const selected = (option_id == selected_id); + + if (!selected && !dialog._unfolded) + return; + + gen_named_node(xml, "hbox", option_id, [&] () { + + gen_named_node(xml, "float", "left", [&] () { + xml.attribute("west", "yes"); + + xml.node("hbox", [&] () { + gen_named_node(xml, "button", "button", [&] () { + + if (selected) + xml.attribute("selected", "yes"); + + xml.attribute("style", "radio"); + dialog._choice_item.gen_hovered_attr(xml, option_id); + xml.node("hbox", [&] () { }); + }); + gen_named_node(xml, "label", "name", [&] () { + xml.attribute("text", Path(" ", option_id)); }); + }); + }); + + gen_named_node(xml, "hbox", "right", [&] () { }); + }); + } + + } choice_generator { xml, selected_id, *this }; + + gen_named_node(xml, "hbox", _id, [&] () { + gen_named_node(xml, "vbox", "left", [&] () { + + gen_named_node(xml, "float", "title", [&] () { + xml.attribute("north", true); + xml.attribute("west", true); + xml.node("frame", [&] () { + xml.attribute("style", "invisible"); + + xml.node("hbox", [&] () { + xml.node("label", [&] () { + xml.attribute("text", _id); }); }); + + /* used for consistent vertical text alignment */ + gen_named_node(xml, "button", "vspace", [&] () { + xml.attribute("style", "invisible"); + xml.node("hbox", [&] () { }); + }); + }); + }); + + gen_named_node(xml, "label", "hspace", [&] () { + xml.attribute("min_ex", _min_ex.left); }); + }); + + gen_named_node(xml, "frame", "right", [&] () { + xml.node("vbox", [&] () { + + fn(choice_generator); + + gen_named_node(xml, "label", "hspace", [&] () { + xml.attribute("min_ex", _min_ex.right); }); + }); + }); + }); + } + + Click_result click() + { + /* unfold choice on click */ + if (_choice_item._hovered == "") + return Click_result::IGNORED; + + _unfolded = true; + return Click_result::CONSUMED; + } + + Id hovered_choice() const { return _choice_item._hovered; } + + Radio_choice_dialog(Id const &id, Min_ex min_ex) + : + _id(id), _min_ex(min_ex) + { } +}; + +#endif /* _VIEW__RADIO_CHOICE_DIALOG_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/settings_dialog.h b/repos/gems/src/app/sculpt_manager/view/settings_dialog.h index e42ba10491..b0b93c6ef8 100644 --- a/repos/gems/src/app/sculpt_manager/view/settings_dialog.h +++ b/repos/gems/src/app/sculpt_manager/view/settings_dialog.h @@ -15,6 +15,7 @@ #define _VIEW__SETTINGS_DIALOG_H_ #include +#include #include namespace Sculpt { struct Settings_dialog; } @@ -22,86 +23,116 @@ namespace Sculpt { struct Settings_dialog; } struct Sculpt::Settings_dialog : Noncopyable, Dialog { - Font_size const &_current_font_size; + Settings const &_settings; - Hoverable_item _item { }; + Hoverable_item _section { }; - static Hoverable_item::Id _font_size_id(Font_size font_size) + Radio_choice_dialog::Min_ex const _ratio { .left = 10, .right = 24 }; + + Radio_choice_dialog _font_size_choice { "Font size", _ratio }; + Radio_choice_dialog _keyboard_layout_choice { "Keyboard", _ratio }; + + static Radio_choice_dialog::Id _font_size_id(Settings::Font_size font_size) { switch (font_size) { - case Font_size::SMALL: return "small"; - case Font_size::MEDIUM: return "medium"; - case Font_size::LARGE: return "large"; + case Settings::Font_size::SMALL: return "Small"; + case Settings::Font_size::MEDIUM: return "Medium"; + case Settings::Font_size::LARGE: return "Large"; } - return Hoverable_item::Id(); + return Radio_choice_dialog::Id(); + } + + static Settings::Font_size _font_size(Radio_choice_dialog::Id id) + { + if (id == "Small") return Settings::Font_size::SMALL; + if (id == "Medium") return Settings::Font_size::MEDIUM; + if (id == "Large") return Settings::Font_size::LARGE; + + return Settings::Font_size::MEDIUM; } Hover_result hover(Xml_node hover) override { return any_hover_changed( - _item.match(hover, "frame", "vbox", "hbox", "button", "name")); + _section.match(hover, "frame", "vbox", "hbox", "name"), + _font_size_choice.match_sub_dialog(hover, "frame", "vbox"), + _keyboard_layout_choice.match_sub_dialog(hover, "frame", "vbox")); } void reset() override { } struct Action : Interface, Noncopyable { - virtual void select_font_size(Font_size) = 0; + virtual void select_font_size(Settings::Font_size) = 0; + + virtual void select_keyboard_layout(Settings::Keyboard_layout::Name const &) = 0; }; void generate(Xml_generator &xml) const override { - gen_named_node(xml, "frame", "network", [&] () { + gen_named_node(xml, "frame", "settings", [&] () { xml.node("vbox", [&] () { - gen_named_node(xml, "hbox", "font_size", [&] () { - gen_named_node(xml, "label", "label", [&] () { - xml.attribute("text", " Font size "); }); - auto gen_font_size_button = [&] (Font_size font_size) - { - Label const label = _font_size_id(font_size); - gen_named_node(xml, "button", label, [&] () { - _item.gen_hovered_attr(xml, label); + using Choice = Radio_choice_dialog::Choice; - if (font_size == _current_font_size) - xml.attribute("selected", true); + if (!_settings.manual_fonts_config) { + _font_size_choice.generate(xml, _font_size_id(_settings.font_size), + [&] (Choice const &choice) { + choice.generate("Small"); + choice.generate("Medium"); + choice.generate("Large"); + }); + } - xml.node("label", [&] () { - xml.attribute("text", label); }); - }); - }; - - gen_font_size_button(Font_size::SMALL); - gen_font_size_button(Font_size::MEDIUM); - gen_font_size_button(Font_size::LARGE); - }); + if (!_settings.manual_event_filter_config) { + _keyboard_layout_choice.generate(xml, _settings.keyboard_layout, + [&] (Choice const &choice) { + using Keyboard_layout = Settings::Keyboard_layout; + Keyboard_layout::for_each([&] (Keyboard_layout const &layout) { + choice.generate(layout.name); }); + }); + } }); }); } Click_result click(Action &action) { + _font_size_choice.reset(); + _keyboard_layout_choice.reset(); + Click_result result = Click_result::IGNORED; - auto apply_font_size = [&] (Font_size font_size) + auto handle_section = [&] (Radio_choice_dialog &dialog, auto fn_clicked) { - if (_item.hovered(_font_size_id(font_size))) { - action.select_font_size(font_size); - result = Click_result::CONSUMED; - } + if (result == Click_result::CONSUMED || !_section.hovered(dialog._id)) + return; + + /* unfold radio choice */ + dialog.click(); + + auto selection = dialog.hovered_choice(); + if (selection == "") + return; + + fn_clicked(selection); + + result = Click_result::CONSUMED; }; - apply_font_size(Font_size::SMALL); - apply_font_size(Font_size::MEDIUM); - apply_font_size(Font_size::LARGE); + handle_section(_font_size_choice, [&] (auto selection) { + action.select_font_size(_font_size(selection)); }); + + handle_section(_keyboard_layout_choice, [&] (auto selection) { + using Keyboard_layout = Settings::Keyboard_layout; + Keyboard_layout::for_each([&] (Keyboard_layout const &layout) { + if (selection == layout.name) + action.select_keyboard_layout(selection); }); }); return result; } - Settings_dialog(Font_size const ¤t_font_size) - : - _current_font_size(current_font_size) - { } + Settings_dialog(Settings const &settings) : _settings(settings) { } }; #endif /* _VIEW__RAM_FS_DIALOG_H_ */