mirror of
https://github.com/genodelabs/genode.git
synced 2025-02-06 02:59:54 +00:00
595 lines
13 KiB
C++
595 lines
13 KiB
C++
/*
|
|
* \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;
|
|
|
|
struct Buffered_xml
|
|
{
|
|
Allocator &_alloc;
|
|
char const * const _ptr; /* pointer to dynamically allocated buffer */
|
|
Xml_node const _xml; /* referring to buffer of '_ptr' */
|
|
|
|
/**
|
|
* \throw Allocator::Out_of_memory
|
|
*/
|
|
static char const *_init_ptr(Allocator &alloc, Xml_node node)
|
|
{
|
|
char *ptr = (char *)alloc.alloc(node.size());
|
|
Genode::memcpy(ptr, node.addr(), node.size());
|
|
return ptr;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* \throw Allocator::Out_of_memory
|
|
*/
|
|
Buffered_xml(Allocator &alloc, Xml_node node)
|
|
:
|
|
_alloc(alloc), _ptr(_init_ptr(alloc, node)), _xml(_ptr, node.size())
|
|
{ }
|
|
|
|
~Buffered_xml() { _alloc.free(const_cast<char *>(_ptr), _xml.size()); }
|
|
|
|
Xml_node xml() const { return _xml; }
|
|
};
|
|
|
|
Constructible<Buffered_xml> _config;
|
|
|
|
List<Element> _elements;
|
|
|
|
Label _focus;
|
|
|
|
static char const *_menu_button_label() { return "_menu"; }
|
|
|
|
Element _menu_button { _menu_button_label(), "Menu" };
|
|
|
|
bool _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 || _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_handler<Panel_dialog> _timer_handler;
|
|
|
|
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)
|
|
{
|
|
if (!_config.constructed()) {
|
|
warning("attempt to start subsystem without prior configuration");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Xml_node subsystem = _subsystem(_config->xml(), 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) {
|
|
Genode::error("no subsystem config found for \"", label, "\"");
|
|
} catch (Subsystem_manager::Invalid_config) {
|
|
Genode::error("invalid subsystem configuration for \"", label, "\"");
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
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(Env &env,
|
|
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(env, report_rom_slave, "panel_dialog", "panel_hover",
|
|
*this, *this, *this, *this, _position),
|
|
_timer_handler(env.ep(), *this, &Panel_dialog::_handle_timer),
|
|
_context_dialog(env, report_rom_slave, *this),
|
|
_menu_dialog(env, report_rom_slave, *this)
|
|
{
|
|
_elements.insert(&_menu_button);
|
|
_timer.sigh(_timer_handler);
|
|
}
|
|
|
|
/**
|
|
* 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(Label const &label)
|
|
{
|
|
_kill(label);
|
|
}
|
|
|
|
/**
|
|
* \throw Allocator::Out_of_memory
|
|
*/
|
|
void update(Xml_node config)
|
|
{
|
|
_config.construct(_alloc, config);
|
|
|
|
/* populate menu dialog with one item per subsystem */
|
|
_menu_dialog.update(_config->xml());
|
|
|
|
/* 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 && !_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_ */
|