From 755d2cce056e223cdd25b7d337f0c8b3847c9aff Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Tue, 29 Sep 2015 17:17:08 +0200 Subject: [PATCH] gems: turn launcher into a panel-like application --- repos/gems/run/launcher.run | 172 +++++- repos/gems/src/app/launcher/context_dialog.h | 29 +- repos/gems/src/app/launcher/fading_dialog.h | 9 +- repos/gems/src/app/launcher/main.cc | 155 +++-- repos/gems/src/app/launcher/menu_dialog.h | 238 ++------ repos/gems/src/app/launcher/menu_view_slave.h | 2 +- repos/gems/src/app/launcher/panel_dialog.h | 554 ++++++++++++++++++ 7 files changed, 896 insertions(+), 263 deletions(-) create mode 100644 repos/gems/src/app/launcher/panel_dialog.h diff --git a/repos/gems/run/launcher.run b/repos/gems/run/launcher.run index c2c0cd52a..b23112c16 100644 --- a/repos/gems/run/launcher.run +++ b/repos/gems/run/launcher.run @@ -7,7 +7,9 @@ set build_components { server/dynamic_rom server/nitpicker server/report_rom app/pointer app/menu_view app/scout app/launchpad app/launcher test/nitpicker - server/nit_fader + server/nit_fader server/rom_filter server/wm app/decorator + app/floating_window_layouter app/status_bar server/nit_fb + app/backdrop app/xray_trigger } source ${genode_dir}/repos/base/run/platform_drv.inc @@ -47,6 +49,7 @@ append_if [have_spec sdl] config { + } append_platform_drv_config @@ -73,20 +76,23 @@ append config { - - - - + + + + + - - - + + + + + - - - - - + + + + + @@ -100,18 +106,129 @@ append config { + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <minimizer/> <maximizer/> <closer/> + </controls> + </inline> + <if> + <has_value input="xray_enabled" value="yes" /> + <then> + <inline> + <policy label="" color="#ff0000" gradient="75" /> + </inline> + </then> + </if> + </output> + + </config> + <route> + <service name="ROM"> <child name="report_rom"/> </service> + <any-service> <parent/> </any-service> + </route> + </start> + <start name="launcher"> <resource name="RAM" quantum="60M" /> - <config visibility="xray"> + <config focus_prefix="wm -> launcher -> "> <subsystem name="scout" title="Scout"> <resource name="RAM" quantum="20M" /> <binary name="scout" /> @@ -136,20 +253,28 @@ append config { <resource name="RAM" quantum="2M" /> <binary name="testnit" /> </subsystem> - <subsystem name="testnit5"> + <subsystem name="testnit5" title="Nitpicker Test5"> <resource name="RAM" quantum="2M" /> <binary name="testnit" /> </subsystem> - <subsystem name="testnit6"> - <resource name="RAM" quantum="2M" /> - <binary name="testnit" /> + <subsystem name="backdrop" title="Backdrop"> + <resource name="RAM" quantum="4M"/> + <binary name="backdrop" /> + <config> + <libc> + <vfs> + </vfs> + </libc> + <fill color="#224433" /> + </config> </subsystem> + </config> <route> - <service name="ROM"> <if-arg key="label" value="xray"/> + <service name="ROM"> <if-arg key="label" value="focus"/> <child name="report_rom" /> </service> - <any-service> <parent/> <any-child/> </any-service> + <any-service> <child name="wm"/> <parent/> <any-child/> </any-service> </route> </start> </config>} @@ -166,7 +291,8 @@ set boot_modules { ld.lib.so libpng.lib.so libc.lib.so libm.lib.so zlib.lib.so menu_view_styles.tar scout launchpad testnit - nit_fader report_rom launcher + nit_fader report_rom launcher rom_filter xray_trigger + decorator wm floating_window_layouter status_bar nit_fb backdrop } # platform-specific modules diff --git a/repos/gems/src/app/launcher/context_dialog.h b/repos/gems/src/app/launcher/context_dialog.h index d9c0f03ba..a7e811ce9 100644 --- a/repos/gems/src/app/launcher/context_dialog.h +++ b/repos/gems/src/app/launcher/context_dialog.h @@ -93,6 +93,8 @@ class Launcher::Context_dialog : Input_event_handler, Dialog_generator, Fading_dialog _dialog; + bool _open = false; + unsigned _key_cnt = 0; Label _clicked; bool _click_in_progress = false; @@ -166,8 +168,23 @@ class Launcher::Context_dialog : Input_event_handler, Dialog_generator, */ bool handle_input_event(Input::Event const &ev) override { - if (ev.type() == Input::Event::MOTION) + if (ev.type() == Input::Event::MOTION) { + + /* + * Re-enable the visibility of the menu if we detect motion + * events over the menu. This way, it reappears in situations + * where the pointer temporarily leaves the view and returns. + */ + if (_open) + visible(true); + return true; + } + + if (ev.type() == Input::Event::LEAVE) { + visible(false); + return true; + } if (ev.type() == Input::Event::PRESS) _key_cnt++; if (ev.type() == Input::Event::RELEASE) _key_cnt--; @@ -210,8 +227,12 @@ class Launcher::Context_dialog : Input_event_handler, Dialog_generator, void visible(bool visible) { + if (visible == _dialog.visible()) + return; + /* reset touch state when (re-)opening the context dialog */ if (visible) { + _open = true; _touch(""); _reset_hover(); dialog_changed(); @@ -221,6 +242,12 @@ class Launcher::Context_dialog : Input_event_handler, Dialog_generator, _dialog.visible(visible); } + void close() + { + _open = false; + visible(false); + } + void position(Fading_dialog::Position position) { _dialog.position(position); diff --git a/repos/gems/src/app/launcher/fading_dialog.h b/repos/gems/src/app/launcher/fading_dialog.h index 09e2e9489..2853f324d 100644 --- a/repos/gems/src/app/launcher/fading_dialog.h +++ b/repos/gems/src/app/launcher/fading_dialog.h @@ -192,6 +192,7 @@ class Launcher::Fading_dialog : private Input_event_handler Nit_fader_slave _nit_fader_slave; Menu_view_slave _menu_view_slave; + bool _visible = false; public: @@ -246,7 +247,13 @@ class Launcher::Fading_dialog : private Input_event_handler }); } - void visible(bool visible) { _nit_fader_slave.visible(visible); } + void visible(bool visible) + { + _nit_fader_slave.visible(visible); + _visible = visible; + } + + bool visible() const { return _visible; } void position(Position position) { _menu_view_slave.position(position); } }; diff --git a/repos/gems/src/app/launcher/main.cc b/repos/gems/src/app/launcher/main.cc index f0dc78f8b..920c02db5 100644 --- a/repos/gems/src/app/launcher/main.cc +++ b/repos/gems/src/app/launcher/main.cc @@ -21,115 +21,154 @@ #include <nitpicker_session/connection.h> /* local includes */ -#include <menu_dialog.h> +#include <panel_dialog.h> namespace Launcher { struct Main; } struct Launcher::Main { - Server::Entrypoint ep; + Server::Entrypoint _ep; - Genode::Cap_connection cap; + Genode::Cap_connection _cap; - char const *report_rom_config = + char const *_report_rom_config = "<config> <rom>" " <policy label=\"menu_dialog\" report=\"menu_dialog\"/>" " <policy label=\"menu_hover\" report=\"menu_hover\"/>" + " <policy label=\"panel_dialog\" report=\"panel_dialog\"/>" + " <policy label=\"panel_hover\" report=\"panel_hover\"/>" " <policy label=\"context_dialog\" report=\"context_dialog\"/>" " <policy label=\"context_hover\" report=\"context_hover\"/>" "</rom> </config>"; - Report_rom_slave report_rom_slave = { cap, *env()->ram_session(), report_rom_config }; - + Report_rom_slave _report_rom_slave = { _cap, *env()->ram_session(), _report_rom_config }; /** * Nitpicker session used to perform session-control operations on the - * subsystem's nitpicker sessions. + * subsystem's nitpicker sessions and to receive global keyboard + * shortcuts. */ - Nitpicker::Connection nitpicker; + Nitpicker::Connection _nitpicker; + + Genode::Attached_dataspace _input_ds { _nitpicker.input()->dataspace() }; + + Input::Event const *_ev_buf() { return _input_ds.local_addr<Input::Event>(); } + + Genode::Signal_rpc_member<Main> _input_dispatcher = + { _ep, *this, &Main::_handle_input }; + + void _handle_input(unsigned); + + unsigned _key_cnt = 0; Genode::Signal_rpc_member<Main> _exited_child_dispatcher = - { ep, *this, &Main::_handle_exited_child }; + { _ep, *this, &Main::_handle_exited_child }; - Subsystem_manager subsystem_manager { ep, cap, _exited_child_dispatcher }; + Subsystem_manager _subsystem_manager { _ep, _cap, _exited_child_dispatcher }; - Menu_dialog menu_dialog { ep, cap, *env()->ram_session(), report_rom_slave, - subsystem_manager, nitpicker }; + Panel_dialog _panel_dialog { _ep, _cap, *env()->ram_session(), *env()->heap(), + _report_rom_slave, _subsystem_manager, _nitpicker }; - - Lazy_volatile_object<Attached_rom_dataspace> xray_rom_ds; - - enum Visibility { VISIBILITY_ALWAYS, VISIBILITY_XRAY }; - - Visibility visibility = VISIBILITY_ALWAYS; - - void handle_config(unsigned); - - Genode::Signal_rpc_member<Main> xray_update_dispatcher = - { ep, *this, &Main::handle_xray_update }; - - void handle_xray_update(unsigned); + void _handle_config(unsigned); void _handle_exited_child(unsigned) { - auto kill_child_fn = [&] (Child_base::Label label) { menu_dialog.kill(label); }; + auto kill_child_fn = [&] (Child_base::Label label) { _panel_dialog.kill(label); }; - subsystem_manager.for_each_exited_child(kill_child_fn); + _subsystem_manager.for_each_exited_child(kill_child_fn); } + Label _focus_prefix; + + Genode::Attached_rom_dataspace _focus_rom { "focus" }; + + void _handle_focus_update(unsigned); + + Genode::Signal_rpc_member<Main> _focus_update_dispatcher = + { _ep, *this, &Main::_handle_focus_update }; + /** * Constructor */ - Main(Server::Entrypoint &ep) : ep(ep) + Main(Server::Entrypoint &ep) : _ep(ep) { - handle_config(0); + _nitpicker.input()->sigh(_input_dispatcher); + _focus_rom.sigh(_focus_update_dispatcher); - if (visibility == VISIBILITY_ALWAYS) - menu_dialog.visible(true); + _handle_config(0); + + _panel_dialog.visible(true); } }; -void Launcher::Main::handle_config(unsigned) +void Launcher::Main::_handle_config(unsigned) { config()->reload(); - /* set default visibility */ - visibility = VISIBILITY_ALWAYS; + _focus_prefix = config()->xml_node().attribute_value("focus_prefix", Label()); - /* obtain model about nitpicker's xray mode */ - if (config()->xml_node().has_attribute("visibility")) { - if (config()->xml_node().attribute("visibility").has_value("xray")) { - xray_rom_ds.construct("xray"); - xray_rom_ds->sigh(xray_update_dispatcher); - - visibility = VISIBILITY_XRAY; - - /* manually import the initial xray state */ - handle_xray_update(0); - } - } - - menu_dialog.update(); + _panel_dialog.update(config()->xml_node()); } -void Launcher::Main::handle_xray_update(unsigned) +void Launcher::Main::_handle_input(unsigned) { - xray_rom_ds->update(); - if (!xray_rom_ds->is_valid()) { - PWRN("could not access xray info"); - menu_dialog.visible(false); - return; + unsigned const num_ev = _nitpicker.input()->flush(); + + for (unsigned i = 0; i < num_ev; i++) { + + Input::Event const &e = _ev_buf()[i]; + + if (e.type() == Input::Event::PRESS) _key_cnt++; + if (e.type() == Input::Event::RELEASE) _key_cnt--; + + /* + * The _key_cnt can become 2 only when the global key (as configured + * in the nitpicker config) is pressed together with another key. + * Hence, the following condition triggers on key combinations with + * the global modifier key, whatever the global modifier key is. + */ + if (e.type() == Input::Event::PRESS && _key_cnt == 2) { + + if (e.keycode() == Input::KEY_TAB) + _panel_dialog.focus_next(); + } } +} - Xml_node xray(xray_rom_ds->local_addr<char>()); - bool const visible = xray.has_attribute("enabled") - && xray.attribute("enabled").has_value("yes"); +void Launcher::Main::_handle_focus_update(unsigned) +{ + try { + _focus_rom.update(); - menu_dialog.visible(visible); + Xml_node focus_node(_focus_rom.local_addr<char>()); + + /* + * Propagate focus information to panel such that the focused + * subsystem gets highlighted. + */ + Label label = focus_node.attribute_value("label", Label()); + + size_t const prefix_len = Genode::strlen(_focus_prefix.string()); + if (!Genode::strcmp(_focus_prefix.string(), label.string(), prefix_len)) { + label = Label(label.string() + prefix_len); + + } else { + + /* + * A foreign nitpicker client not started by ourself has the focus. + */ + label = Label(); + } + + _panel_dialog.focus_changed(label); + + } catch (...) { + PWRN("no focus model available"); + } } diff --git a/repos/gems/src/app/launcher/menu_dialog.h b/repos/gems/src/app/launcher/menu_dialog.h index c8e8ef74f..7ee1dfac0 100644 --- a/repos/gems/src/app/launcher/menu_dialog.h +++ b/repos/gems/src/app/launcher/menu_dialog.h @@ -26,11 +26,21 @@ namespace Launcher { struct Menu_dialog; } class Launcher::Menu_dialog : Input_event_handler, Dialog_generator, - Hover_handler, Dialog_model, - Context_dialog::Response_handler + Hover_handler, Dialog_model { + public: + + struct Response_handler + { + virtual void handle_selection(Label const &) = 0; + virtual void handle_menu_leave() = 0; + virtual void handle_menu_motion() = 0; + }; + private: + Response_handler &_response_handler; + typedef String<128> Title; struct Element : List<Element>::Element @@ -58,7 +68,7 @@ class Launcher::Menu_dialog : Input_event_handler, Dialog_generator, xml.node("button", [&] () { xml.attribute("name", e->label.string()); - if ((e->hovered && !_click_in_progress) + if ((e->hovered) || (e->hovered && e->touched)) xml.attribute("hovered", "yes"); @@ -72,43 +82,15 @@ class Launcher::Menu_dialog : Input_event_handler, Dialog_generator, } } - class Lookup_failed { }; + Fading_dialog::Position _position { 0 - 4, 28 - 4 }; - Element const &_lookup_const(Label const &label) const - { - for (Element const *e = _elements.first(); e; e = e->next()) - if (e->label == label) - return *e; - - throw Lookup_failed(); - } - - Element &_lookup(Label const &label) - { - for (Element *e = _elements.first(); e; e = e->next()) - if (e->label == label) - return *e; - - throw Lookup_failed(); - } - - Fading_dialog::Position _position { 32, 32 }; - - Timer::Connection _timer; - Subsystem_manager &_subsystem_manager; - Nitpicker::Session &_nitpicker; - Fading_dialog _dialog; + Fading_dialog _dialog; Rect _hovered_rect; + bool _open = false; + unsigned _key_cnt = 0; - Label _clicked; - bool _click_in_progress = false; - - Signal_rpc_member<Menu_dialog> _timer_dispatcher; - - Label _context_subsystem; - Context_dialog _context_dialog; Label _hovered() const { @@ -119,102 +101,17 @@ class Launcher::Menu_dialog : Input_event_handler, Dialog_generator, return Label(""); } - bool _running(Label const &label) const - { - try { return _lookup_const(label).running; } - catch (Lookup_failed) { return false; } - } - - void _running(Label const &label, bool running) - { - try { _lookup(label).running = running; } - catch (Lookup_failed) { } - } - - void _touch(Label const &label) - { - for (Element *e = _elements.first(); e; e = e->next()) - e->touched = (e->label == label); - } - - /** - * Lookup subsystem in config - */ - static Xml_node _subsystem(Xml_node config, char const *name) - { - Xml_node node = config.sub_node("subsystem"); - for (;; node = node.next("subsystem")) { - if (node.attribute("name").has_value(name)) - return node; - } - } - - void _start(Label const &label) - { - try { - _subsystem_manager.start(_subsystem(config()->xml_node(), - label.string())); - _running(label, true); - - dialog_changed(); - - } catch (Xml_node::Nonexistent_sub_node) { - PERR("no subsystem config found for \"%s\"", label.string()); - } catch (Subsystem_manager::Invalid_config) { - PERR("invalid subsystem configuration for \"%s\"", label.string()); - } - } - - void _kill(Label const &label) - { - _subsystem_manager.kill(label.string()); - _running(label, false); - dialog_changed(); - _dialog.update(); - - _context_dialog.visible(false); - } - - void _hide(Label const &label) - { - _nitpicker.session_control(selector(label.string()), - Nitpicker::Session::SESSION_CONTROL_HIDE); - - _context_dialog.visible(false); - } - - void _handle_timer(unsigned) - { - if (_click_in_progress && _hovered() == _clicked) { - - _touch(""); - - Fading_dialog::Position position(_hovered_rect.p2().x(), - _hovered_rect.p1().y() - 44); - _context_subsystem = _clicked; - _context_dialog.position(_position + position); - _context_dialog.visible(true); - } - - _click_in_progress = false; - } - public: Menu_dialog(Server::Entrypoint &ep, Cap_session &cap, Ram_session &ram, Report_rom_slave &report_rom_slave, - Subsystem_manager &subsystem_manager, - Nitpicker::Session &nitpicker) + Response_handler &response_handler) : - _subsystem_manager(subsystem_manager), - _nitpicker(nitpicker), + _response_handler(response_handler), _dialog(ep, cap, ram, report_rom_slave, "menu_dialog", "menu_hover", *this, *this, *this, *this, - _position), - _timer_dispatcher(ep, *this, &Menu_dialog::_handle_timer), - _context_dialog(ep, cap, ram, report_rom_slave, *this) + _position) { - _timer.sigh(_timer_dispatcher); } /** @@ -287,8 +184,25 @@ class Launcher::Menu_dialog : Input_event_handler, Dialog_generator, */ bool handle_input_event(Input::Event const &ev) override { - if (ev.type() == Input::Event::MOTION) + if (ev.type() == Input::Event::LEAVE) { + _response_handler.handle_menu_leave(); + return false; + } + + if (ev.type() == Input::Event::MOTION) { + + _response_handler.handle_menu_motion(); + + /* + * Re-enable the visibility of the menu if we detect motion + * events over the menu. This way, it reappears in situations + * where the pointer temporarily leaves the view and returns. + */ + if (_open) + visible(true); + return true; + } if (ev.type() == Input::Event::PRESS) _key_cnt++; if (ev.type() == Input::Event::RELEASE) _key_cnt--; @@ -297,71 +211,39 @@ class Launcher::Menu_dialog : Input_event_handler, Dialog_generator, && ev.keycode() == Input::BTN_LEFT && _key_cnt == 1) { - _context_dialog.visible(false); - - Label const hovered = _hovered(); - - _click_in_progress = true; - _clicked = hovered; - _touch(hovered); - - enum { CONTEXT_DELAY = 500 }; - - if (_running(hovered)) { - _nitpicker.session_control(selector(hovered.string()), - Nitpicker::Session::SESSION_CONTROL_TO_FRONT); - _nitpicker.session_control(selector(hovered.string()), - Nitpicker::Session::SESSION_CONTROL_SHOW); - _timer.trigger_once(CONTEXT_DELAY*1000); - } - } - - if (ev.type() == Input::Event::RELEASE - && _click_in_progress && _key_cnt == 0) { - - Label const hovered = _hovered(); - - if (_clicked == hovered) { - - if (!_running(hovered)) - _start(hovered); - } - - _touch(""); - _clicked = Label(""); - _click_in_progress = false; + _response_handler.handle_selection(_hovered()); } return false; } - /** - * Context_dialog::Response_handler interface - */ - void handle_context_kill() override - { - _kill(_context_subsystem); - } - - /** - * Context_dialog::Response_handler interface - */ - void handle_context_hide() override - { - _hide(_context_subsystem); - } - void visible(bool visible) { + if (visible == _dialog.visible()) + return; + _dialog.visible(visible); - if (!visible) - _context_dialog.visible(false); + if (visible) + _open = true; } - void kill(Child_base::Label const &label) { _kill(label); } + void close() + { + _open = false; + visible(false); + } - void update() + void running(Label const &label, bool running) + { + for (Element *e = _elements.first(); e; e = e->next()) + if (e->label == label) + e->running = running; + + _dialog.update(); + } + + void update(Xml_node subsystems) { if (_elements.first()) { PERR("subsequent updates are not supported"); @@ -370,8 +252,6 @@ class Launcher::Menu_dialog : Input_event_handler, Dialog_generator, Element *last = nullptr; - Xml_node subsystems = config()->xml_node(); - subsystems.for_each_sub_node("subsystem", [&] (Xml_node subsystem) { diff --git a/repos/gems/src/app/launcher/menu_view_slave.h b/repos/gems/src/app/launcher/menu_view_slave.h index 06b92db91..cae8ba9b6 100644 --- a/repos/gems/src/app/launcher/menu_view_slave.h +++ b/repos/gems/src/app/launcher/menu_view_slave.h @@ -131,7 +131,7 @@ class Launcher::Menu_view_slave Genode::size_t const _ep_stack_size = 4*1024*sizeof(Genode::addr_t); Genode::Rpc_entrypoint _ep; Policy _policy; - Genode::size_t const _quota = 4*1024*1024; + Genode::size_t const _quota = 6*1024*1024; Genode::Slave _slave; public: diff --git a/repos/gems/src/app/launcher/panel_dialog.h b/repos/gems/src/app/launcher/panel_dialog.h new file mode 100644 index 000000000..777fa73fb --- /dev/null +++ b/repos/gems/src/app/launcher/panel_dialog.h @@ -0,0 +1,554 @@ +/* + * \brief Panel dialog + * \author Norman Feske + * \date 2015-10-07 + */ + +/* + * Copyright (C) 2015 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +#ifndef _PANEL_DIALOG_H_ +#define _PANEL_DIALOG_H_ + +/* Genode includes */ +#include <timer_session/connection.h> + +/* local includes */ +#include <fading_dialog.h> +#include <subsystem_manager.h> +#include <context_dialog.h> +#include <menu_dialog.h> + +namespace Launcher { struct Panel_dialog; } + + +class Launcher::Panel_dialog : Input_event_handler, Dialog_generator, + Hover_handler, Dialog_model, + Context_dialog::Response_handler, + Menu_dialog::Response_handler +{ + private: + + typedef String<128> Title; + + struct Element : List<Element>::Element + { + Label const label; + Title const title; + + bool hovered = false; + bool touched = false; + bool selected = false; + + Element(Label label, Title title) : label(label), title(title) + { } + }; + + Genode::Allocator &_alloc; + + List<Element> _elements; + + Label _focus; + + static char const *_menu_button_label() { return "_menu"; } + + Element _menu_button { _menu_button_label(), "Menu" }; + + bool _is_focused(Element const &e) + { + size_t const label_len = strlen(e.label.string()); + + if (strcmp(e.label.string(), _focus.string(), label_len)) + return false; + + /* + * Even when the strcmp suceeded, the element's label might + * not match the focus. E.g., if two subsystems "scout" and + * "scoutx" are present the focus of "scoutx" would match both + * subsystem labels because the strcmp is limited to the length + * of the subsystem label. Hence, we need to make sure that + * the focus matched at a separator boundary. + */ + char const *char_after_label = _focus.string() + label_len; + + if (*char_after_label == 0 || !strcmp(" -> ", char_after_label, 4)) + return true; + + return false; + } + + void _generate_dialog_element(Xml_generator &xml, Element const &e) + { + xml.node("button", [&] () { + xml.attribute("name", e.label.string()); + + if (&e != &_menu_button) + xml.attribute("style", "subdued"); + + if ((e.hovered && !_click_in_progress) + || (e.hovered && e.touched)) + xml.attribute("hovered", "yes"); + + if (e.selected || e.touched || _is_focused(e)) + xml.attribute("selected", "yes"); + + xml.node("label", [&] () { + xml.attribute("text", e.title.string()); + }); + }); + } + + class Lookup_failed { }; + + Element const &_lookup_const(Label const &label) const + { + for (Element const *e = _elements.first(); e; e = e->next()) + if (e->label == label) + return *e; + + throw Lookup_failed(); + } + + Element &_lookup(Label const &label) + { + for (Element *e = _elements.first(); e; e = e->next()) + if (e->label == label) + return *e; + + throw Lookup_failed(); + } + + Fading_dialog::Position _position { 0, 0 }; + + Timer::Connection _timer; + Subsystem_manager &_subsystem_manager; + Nitpicker::Session &_nitpicker; + Fading_dialog _dialog; + + Rect _hovered_rect; + + unsigned _key_cnt = 0; + Element *_clicked = nullptr; + bool _click_in_progress = false; + + Signal_rpc_member<Panel_dialog> _timer_dispatcher; + + Label _context_subsystem; + Context_dialog _context_dialog; + + Menu_dialog _menu_dialog; + + Element *_hovered() + { + for (Element *e = _elements.first(); e; e = e->next()) + if (e->hovered) + return e; + + return nullptr; + } + + /** + * Lookup subsystem in config + */ + static Xml_node _subsystem(Xml_node config, char const *name) + { + Xml_node node = config.sub_node("subsystem"); + for (;; node = node.next("subsystem")) { + if (node.attribute("name").has_value(name)) + return node; + } + } + + void _start(Label const &label) + { + try { + Xml_node subsystem = _subsystem(config()->xml_node(), + label.string()); + _subsystem_manager.start(subsystem); + + Title const title = subsystem.attribute_value("title", Title()); + + Element *e = new (_alloc) Element(label, title); + + /* find last element of the list */ + Element *at = _elements.first(); + for (; at && at->next(); at = at->next()); + + _elements.insert(e, at); + + dialog_changed(); + + } catch (Xml_node::Nonexistent_sub_node) { + PERR("no subsystem config found for \"%s\"", label.string()); + } catch (Subsystem_manager::Invalid_config) { + PERR("invalid subsystem configuration for \"%s\"", label.string()); + } + } + + void _kill(Label const &label) + { + Element &e = _lookup(label); + + _subsystem_manager.kill(label.string()); + + _elements.remove(&e); + + if (_clicked == &e) + _clicked = nullptr; + + Genode::destroy(_alloc, &e); + + dialog_changed(); + _dialog.update(); + + _context_dialog.close(); + + _menu_dialog.running(label, false); + } + + void _hide(Label const &label) + { + _nitpicker.session_control(selector(label.string()), + Nitpicker::Session::SESSION_CONTROL_HIDE); + + _context_dialog.close(); + } + + void _open_context_dialog(Label const &label) + { + /* reset touch state in each element */ + for (Element *e = _elements.first(); e; e = e->next()) + e->touched = false; + + Fading_dialog::Position position(_hovered_rect.p1().x(), + _hovered_rect.p2().y()); + + _context_subsystem = label; + _context_dialog.position(_position + position); + _context_dialog.visible(true); + } + + void _handle_timer(unsigned) + { + if (_click_in_progress && _clicked && _hovered() == _clicked) { + _open_context_dialog(_clicked->label); + } + _click_in_progress = false; + } + + void _to_front(Label const &label) + { + _nitpicker.session_control(selector(label.string()), + Nitpicker::Session::SESSION_CONTROL_TO_FRONT); + _nitpicker.session_control(selector(label.string()), + Nitpicker::Session::SESSION_CONTROL_SHOW); + } + + public: + + Panel_dialog(Server::Entrypoint &ep, Cap_session &cap, Ram_session &ram, + Genode::Allocator &alloc, + Report_rom_slave &report_rom_slave, + Subsystem_manager &subsystem_manager, + Nitpicker::Session &nitpicker) + : + _alloc(alloc), + _subsystem_manager(subsystem_manager), + _nitpicker(nitpicker), + _dialog(ep, cap, ram, report_rom_slave, "panel_dialog", "panel_hover", + *this, *this, *this, *this, + _position), + _timer_dispatcher(ep, *this, &Panel_dialog::_handle_timer), + _context_dialog(ep, cap, ram, report_rom_slave, *this), + _menu_dialog(ep, cap, ram, report_rom_slave, *this) + { + _elements.insert(&_menu_button); + _timer.sigh(_timer_dispatcher); + } + + /** + * Dialog_generator interface + */ + void generate_dialog(Xml_generator &xml) override + { + xml.node("hbox", [&] () { + for (Element const *e = _elements.first(); e; e = e->next()) + _generate_dialog_element(xml, *e); + }); + } + + Rect _hovered_button_rect(Xml_node hover) const + { + Point p(0, 0); + + for (;; hover = hover.sub_node()) { + + p = p + Point(point_attribute(hover)); + + if (hover.has_type("button")) + return Rect(p, area_attribute(hover)); + + if (!hover.num_sub_nodes()) + break; + } + + return Rect(); + } + + /** + * Hover_handler interface + */ + void hover_changed(Xml_node hover) override + { + Element *old_hovered = _hovered(); + + for (Element *e = _elements.first(); e; e = e->next()) + e->hovered = false; + + try { + Xml_node button = hover.sub_node("dialog") + .sub_node("hbox") + .sub_node("button"); + + for (Element *e = _elements.first(); e; e = e->next()) { + + Label const label = + Decorator::string_attribute(button, "name", Label("")); + + if (e->label == label) { + e->hovered = true; + + _hovered_rect = _hovered_button_rect(hover); + } + } + } catch (Xml_node::Nonexistent_sub_node) { } + + Element *new_hovered = _hovered(); + + if (old_hovered != new_hovered) + dialog_changed(); + } + + /** + * Input_event_handler interface + */ + bool handle_input_event(Input::Event const &ev) override + { + if (ev.type() == Input::Event::LEAVE) { + + /* + * Let menu dialog disappear when the panel is unhovered. One + * would expect that the user had no chance to select an item + * from the menu because when entering the menu, we will no + * longer hover the panel. However, the menu disappears slowly. + * If the pointer moves over to the menu in a reasonable time, + * the visiblity of the menu is re-enabled. + */ + _menu_dialog.visible(false); + _context_dialog.visible(false); + _menu_button.selected = false; + _dialog.update(); + return true; + } + + if (ev.type() == Input::Event::MOTION) + return true; + + if (ev.type() == Input::Event::PRESS) _key_cnt++; + if (ev.type() == Input::Event::RELEASE) _key_cnt--; + + if (ev.type() == Input::Event::PRESS + && ev.keycode() == Input::BTN_LEFT + && _key_cnt == 1) { + + _context_dialog.visible(false); + + Element *hovered = _hovered(); + + _click_in_progress = true; + _clicked = hovered; + + if (!hovered) + return false; + + hovered->touched = true; + + if (hovered == &_menu_button) { + + /* menu button presses */ + if (_menu_button.selected) + _menu_dialog.close(); + else + _menu_dialog.visible(true); + + _menu_button.selected = !_menu_button.selected; + _dialog.update(); + return false; + } + + _menu_dialog.close(); + + _to_front(hovered->label); + + /* + * Open the context dialog after the user keeps pressing the + * button for a while. + */ + enum { CONTEXT_DELAY = 500 }; + _timer.trigger_once(CONTEXT_DELAY*1000); + } + + /* + * Open context dialog on right click + */ + if (ev.type() == Input::Event::PRESS + && ev.keycode() == Input::BTN_RIGHT + && _key_cnt == 1) { + + Element *hovered = _hovered(); + + if (hovered && hovered != &_menu_button) + _open_context_dialog(hovered->label); + } + + if (ev.type() == Input::Event::RELEASE + && _click_in_progress && _key_cnt == 0) { + + Element *hovered = _hovered(); + + if (hovered) + hovered->touched = false; + + _clicked = nullptr; + _click_in_progress = false; + } + + return false; + } + + /** + * Context_dialog::Response_handler interface + */ + void handle_context_kill() override + { + _kill(_context_subsystem); + } + + /** + * Context_dialog::Response_handler interface + */ + void handle_context_hide() override + { + _hide(_context_subsystem); + } + + /** + * Menu_dialog::Response_handler interface + */ + void handle_menu_motion() + { + _menu_button.selected = true; + _dialog.update(); + } + + /** + * Menu_dialog::Response_handler interface + */ + void handle_menu_leave() + { + /* XXX eventually revert the state of the menu button */ + _menu_button.selected = false; + + _dialog.update(); + + _menu_dialog.visible(false); + } + + /** + * Menu_dialog::Response_handler interface + */ + void handle_selection(Label const &label) override + { + /* + * If subsystem of the specified label is already running, ignore + * the click. + */ + bool already_running = false; + for (Element *e = _elements.first(); e; e = e->next()) + if (e->label == label) + already_running = true; + + if (already_running) { + _to_front(label); + + } else { + + _start(label); + + _dialog.update(); + + /* propagate running state of subsystem to menu dialog */ + _menu_dialog.running(label, true); + } + + /* let menu disappear */ + _menu_dialog.close(); + } + + void visible(bool visible) + { + _dialog.visible(visible); + + if (!visible) + _context_dialog.visible(false); + } + + void kill(Child_base::Label const &label) + { + _kill(label); + } + + void update(Xml_node config) + { + /* populate menu dialog with one item per subsystem */ + _menu_dialog.update(config); + + /* evaluate configuration */ + + _dialog.update(); + } + + void focus_changed(Label const &label) + { + _focus = label; + + _dialog.update(); + } + + void focus_next() + { + /* find focused element */ + Element *e = _elements.first(); + + for (; e && !_is_focused(*e); e = e->next()); + + /* none of our subsystems is focused, start with the first one */ + if (!e) + e = _elements.first(); + + /* + * Determine next session in the list, if we reach the end, start + * at the beginning (the element right after the menu button. + */ + Element *new_focused = e->next() ? e->next() : _menu_button.next(); + + if (new_focused) + _to_front(new_focused->label); + } +}; + +#endif /* _PANEL_DIALOG_H_ */