sculpt: system update and presets

The new dialog accessible via the "System" panel button hosts the
system-update dialog and the preset selection.

Fixes #4744
This commit is contained in:
Norman Feske 2023-04-24 17:51:12 +02:00 committed by Christian Helmuth
parent 5687dc06fd
commit 105b3cd21d
16 changed files with 1697 additions and 14 deletions

View File

@ -127,6 +127,8 @@
report="manager -> network_dialog"/>
<policy label="runtime -> leitzentrale -> settings_view -> dialog"
report="manager -> settings_dialog"/>
<policy label="runtime -> leitzentrale -> system_view -> dialog"
report="manager -> system_dialog"/>
<policy label="runtime -> leitzentrale -> file_browser_view -> dialog"
report="manager -> file_browser_dialog"/>
<policy label="runtime -> leitzentrale -> popup_view -> dialog"
@ -141,6 +143,8 @@
report="runtime -> leitzentrale -> network_view -> hover"/>
<policy label="manager -> settings_view_hover"
report="runtime -> leitzentrale -> settings_view -> hover"/>
<policy label="manager -> system_view_hover"
report="runtime -> leitzentrale -> system_view -> hover"/>
<policy label="manager -> file_browser_view_hover"
report="runtime -> leitzentrale -> file_browser_view -> hover"/>
<policy label="manager -> runtime_view_hover"
@ -190,6 +194,7 @@
</vfs>
<policy label="log" decoration="yes" motion="20"/>
<policy label="runtime -> leitzentrale -> settings_view" decoration="no" motion="20"/>
<policy label="runtime -> leitzentrale -> system_view" decoration="no" motion="20"/>
<policy label="runtime -> leitzentrale -> file_browser_view" decoration="no" motion="30"/>
<policy label="runtime -> leitzentrale -> network_view" decoration="no" motion="20"/>
<policy label="runtime -> leitzentrale -> runtime_view" decoration="no" motion="30"/>

View File

@ -40,6 +40,7 @@
#include <view/popup_dialog.h>
#include <view/panel_dialog.h>
#include <view/settings_dialog.h>
#include <view/system_dialog.h>
#include <view/file_browser_dialog.h>
#include <menu_view.h>
#include <gui.h>
@ -61,6 +62,9 @@ struct Sculpt::Main : Input_event_handler,
Panel_dialog::Action,
Popup_dialog::Action,
Settings_dialog::Action,
Software_presets_dialog::Action,
Depot_users_dialog::Action,
Software_update_dialog::Action,
File_browser_dialog::Action,
Popup_dialog::Construction_info,
Depot_query,
@ -75,6 +79,9 @@ struct Sculpt::Main : Input_event_handler,
Sculpt_version const _sculpt_version { _env };
Build_info const _build_info =
Build_info::from_xml(Attached_rom_dataspace(_env, "build_info").xml());
Registry<Child_state> _child_states { };
Input::Seq_number _global_input_seq_number { };
@ -223,10 +230,11 @@ struct Sculpt::Main : Input_event_handler,
/* trigger loading of the configuration from the sculpt partition */
_prepare_version.value++;
_download_queue.remove_inactive_downloads();
_download_queue.reset();
_deploy.restart();
generate_runtime_config();
generate_dialog();
}
Network _network { _env, _heap, *this, _child_states, *this, _runtime_state, _pci_info };
@ -274,6 +282,9 @@ struct Sculpt::Main : Input_event_handler,
Fs_tool_version _fs_tool_version { 0 };
Index_update_queue _index_update_queue {
_heap, _file_operation_queue, _download_queue };
/*****************
** Depot query **
@ -281,6 +292,8 @@ struct Sculpt::Main : Input_event_handler,
Depot_query::Version _query_version { 0 };
Depot::Archive::User _image_index_user = _build_info.depot_user;
Expanding_reporter _depot_query_reporter { _env, "query", "depot_query"};
/**
@ -296,6 +309,11 @@ struct Sculpt::Main : Input_event_handler,
Timer::One_shot_timeout<Main> _deferred_depot_query_handler {
_timer, *this, &Main::_handle_deferred_depot_query };
bool _system_dialog_watches_depot() const
{
return _system_visible && _system_dialog.update_tab_selected();
}
void _handle_deferred_depot_query(Duration)
{
if (_deploy._arch.valid()) {
@ -308,6 +326,17 @@ struct Sculpt::Main : Input_event_handler,
xml.node("scan", [&] () {
xml.attribute("users", "yes"); });
if (_system_dialog_watches_depot() || _scan_rom.xml().has_type("empty"))
xml.node("scan", [&] () {
xml.attribute("users", "yes"); });
if (_system_dialog_watches_depot() || _image_index_rom.xml().has_type("empty"))
xml.node("image_index", [&] () {
xml.attribute("os", "sculpt");
xml.attribute("board", _build_info.board);
xml.attribute("user", _image_index_user);
});
_popup_dialog.gen_depot_query(xml, _scan_rom.xml());
/* update query for blueprints of all unconfigured start nodes */
@ -380,6 +409,16 @@ struct Sculpt::Main : Input_event_handler,
_popup_dialog.depot_users_scan_updated();
}
Attached_rom_dataspace _image_index_rom { _env, "report -> runtime/depot_query/image_index" };
Signal_handler<Main> _image_index_handler { _env.ep(), *this, &Main::_handle_image_index };
void _handle_image_index()
{
_image_index_rom.update();
_system_menu_view.generate();
}
Attached_rom_dataspace _launcher_listing_rom {
_env, "report -> /runtime/launcher_query/listing" };
@ -443,6 +482,7 @@ struct Sculpt::Main : Input_event_handler,
bool _log_visible = false;
bool _network_visible = false;
bool _settings_visible = false;
bool _system_visible = false;
File_browser_state _file_browser_state { };
@ -459,6 +499,8 @@ struct Sculpt::Main : Input_event_handler,
bool settings_visible() const override { return _settings_visible; }
bool system_visible() const override { return _system_visible; }
bool inspect_tab_visible() const override { return _storage.any_file_system_inspected(); }
Panel_dialog::Tab selected_tab() const override { return _selected_tab; }
@ -536,6 +578,9 @@ struct Sculpt::Main : Input_event_handler,
{
_main_menu_view.generate();
_graph_menu_view.generate();
if (_system_visible)
_system_menu_view.generate();
}
Attached_rom_dataspace _runtime_state_rom { _env, "report -> runtime/state" };
@ -689,6 +734,11 @@ struct Sculpt::Main : Input_event_handler,
_settings_menu_view.generate();
_clicked_seq_number.destruct();
}
else if (_system_menu_view.hovered(seq)) {
_system_dialog.click();
_system_menu_view.generate();
_clicked_seq_number.destruct();
}
else if (_network_menu_view.hovered(seq)) {
_network.dialog.click(_network);
_network_menu_view.generate();
@ -717,6 +767,11 @@ struct Sculpt::Main : Input_event_handler,
_graph_menu_view.generate();
_clacked_seq_number.destruct();
}
else if (_system_menu_view.hovered(seq)) {
_system_dialog.clack();
_system_menu_view.generate();
_clacked_seq_number.destruct();
}
else if (_popup_menu_view.hovered(seq)) {
_popup_dialog.clack(*this);
_clacked_seq_number.destruct();
@ -752,9 +807,14 @@ struct Sculpt::Main : Input_event_handler,
_try_handle_clack();
}
if (_keyboard_focus.target == Keyboard_focus::WPA_PASSPHRASE)
ev.handle_press([&] (Input::Keycode, Codepoint code) {
_network.handle_key_press(code); });
ev.handle_press([&] (Input::Keycode, Codepoint code) {
if (_keyboard_focus.target == Keyboard_focus::WPA_PASSPHRASE)
_network.handle_key_press(code);
else if (_system_visible && _system_dialog.keyboard_needed())
_system_dialog.handle_key(code);
need_generate_dialog = true;
});
if (ev.press())
_keyboard_focus.update();
@ -774,7 +834,11 @@ struct Sculpt::Main : Input_event_handler,
_panel_menu_view.generate();
}
void use(Storage_target const &target) override { _storage.use(target); }
void use(Storage_target const &target) override
{
_download_queue.reset();
_storage.use(target);
}
void _reset_storage_dialog_operation()
{
@ -874,6 +938,66 @@ struct Sculpt::Main : Input_event_handler,
_handle_window_layout();
}
/**
* Depot_users_dialog::Action interface
*/
void add_depot_url(Depot_url const &depot_url) override
{
using Content = File_operation_queue::Content;
_file_operation_queue.new_small_file(Path("/rw/depot/", depot_url.user, "/download"),
Content { depot_url.download });
if (!_file_operation_queue.any_operation_in_progress())
_file_operation_queue.schedule_next_operations();
generate_runtime_config();
}
/**
* Software_update_dialog::Action interface
*/
void query_image_index(Depot::Archive::User const &user) override
{
_image_index_user = user;
trigger_depot_query();
}
/**
* Software_update_dialog::Action interface
*/
void trigger_image_download(Path const &path, Verify verify) override
{
_download_queue.remove_inactive_downloads();
_download_queue.add(path, verify);
_deploy.update_installation();
generate_runtime_config();
}
/**
* Software_update_dialog::Action interface
*/
void update_image_index(Depot::Archive::User const &user, Verify verify) override
{
_download_queue.remove_inactive_downloads();
_index_update_queue.remove_inactive_updates();
_index_update_queue.add(Path(user, "/image/index"), verify);
generate_runtime_config();
}
/**
* Software_update_dialog::Action interface
*/
void install_boot_image(Path const &path) override
{
_file_operation_queue.copy_all_files(Path("/rw/depot/", path), "/rw/boot");
if (!_file_operation_queue.any_operation_in_progress())
_file_operation_queue.schedule_next_operations();
generate_runtime_config();
}
/*
* Panel::Action interface
*/
@ -914,6 +1038,39 @@ struct Sculpt::Main : Input_event_handler,
_refresh_panel_and_window_layout();
}
/*
* Panel::Action interface
*/
void toggle_system_visibility() override
{
_system_visible = !_system_visible;
_refresh_panel_and_window_layout();
}
/**
* Software_presets_dialog::Action interface
*/
void load_deploy_preset(Presets::Info::Name const &name) override
{
Xml_node const listing = _launcher_listing_rom.xml();
_download_queue.remove_inactive_downloads();
listing.for_each_sub_node("dir", [&] (Xml_node const &dir) {
if (dir.attribute_value("path", Path()) == "/presets") {
dir.for_each_sub_node("file", [&] (Xml_node const &file) {
if (file.attribute_value("name", Presets::Info::Name()) == name) {
file.with_optional_sub_node("config", [&] (Xml_node const &config) {
_runtime_state.reset_abandoned_and_launched_children();
_deploy.use_as_deploy_template(config);
_deploy.update_managed_deploy_config();
});
}
});
}
});
}
/*
* Settings_dialog::Action interface
*/
@ -1144,6 +1301,7 @@ struct Sculpt::Main : Input_event_handler,
_deploy.update_installation();
generate_runtime_config();
generate_dialog();
}
void remove_index(Depot::Archive::User const &user) override
@ -1181,6 +1339,15 @@ struct Sculpt::Main : Input_event_handler,
Ram_quota{4*1024*1024}, Cap_quota{150},
"settings_dialog", "settings_view_hover", *this };
System_dialog _system_dialog { _presets, _build_info, _network._nic_state,
_download_queue, _index_update_queue,
_file_operation_queue, _scan_rom,
_image_index_rom, *this, *this, *this };
Menu_view _system_menu_view { _env, _child_states, _system_dialog, "system_view",
Ram_quota{4*1024*1024}, Cap_quota{150},
"system_dialog", "system_view_hover", *this };
Menu_view _main_menu_view { _env, _child_states, *this, "menu_view",
Ram_quota{4*1024*1024}, Cap_quota{150},
"menu_dialog", "menu_view_hover", *this };
@ -1269,6 +1436,7 @@ struct Sculpt::Main : Input_event_handler,
_scan_rom .sigh(_scan_handler);
_launcher_listing_rom.sigh(_launcher_and_preset_listing_handler);
_blueprint_rom .sigh(_blueprint_handler);
_image_index_rom .sigh(_image_index_handler);
_editor_saved_rom .sigh(_editor_saved_handler);
_clicked_rom .sigh(_clicked_handler);
@ -1343,6 +1511,7 @@ void Sculpt::Main::_handle_window_layout()
panel_view_label ("runtime -> leitzentrale -> panel_view"),
menu_view_label ("runtime -> leitzentrale -> menu_view"),
popup_view_label ("runtime -> leitzentrale -> popup_view"),
system_view_label ("runtime -> leitzentrale -> system_view"),
settings_view_label ("runtime -> leitzentrale -> settings_view"),
network_view_label ("runtime -> leitzentrale -> network_view"),
file_browser_view_label("runtime -> leitzentrale -> file_browser_view"),
@ -1420,10 +1589,22 @@ void Sculpt::Main::_handle_window_layout()
_with_window(window_list, Label("log"), [&] (Xml_node win) {
gen_window(win, Rect(log_p1, log_p2)); });
int system_right_xpos = 0;
_with_window(window_list, system_view_label, [&] (Xml_node win) {
Area const size = win_size(win);
Point const pos = _system_visible
? Point(0, avail.y1())
: Point(-size.w(), avail.y1());
gen_window(win, Rect(pos, size));
if (_system_visible)
system_right_xpos = size.w();
});
_with_window(window_list, settings_view_label, [&] (Xml_node win) {
Area const size = win_size(win);
Point const pos = _settings_visible
? Point(0, avail.y1())
? Point(system_right_xpos, avail.y1())
: Point(-size.w(), avail.y1());
if (_settings.interactive_settings_available())
@ -1618,23 +1799,25 @@ void Sculpt::Main::_handle_update_state()
Xml_node const update_state = _update_state_rom.xml();
if (update_state.num_sub_nodes() == 0)
return;
bool const popup_watches_downloads =
_popup_dialog.interested_in_download();
_download_queue.apply_update_state(update_state);
bool const any_completed_download = _download_queue.any_completed_download();
_download_queue.remove_completed_downloads();
_index_update_queue.apply_update_state(update_state);
bool const installation_complete =
!update_state.attribute_value("progress", false);
bool const popup_watches_downloads =
_popup_dialog.interested_in_download();
if (installation_complete) {
Xml_node const blueprint = _blueprint_rom.xml();
bool const new_depot_query_needed = popup_watches_downloads
|| blueprint_any_missing(blueprint)
|| blueprint_any_rom_missing(blueprint);
|| blueprint_any_rom_missing(blueprint)
|| any_completed_download;
if (new_depot_query_needed)
trigger_depot_query();
@ -1643,6 +1826,8 @@ void Sculpt::Main::_handle_update_state()
_deploy.reattempt_after_installation();
}
generate_dialog();
}
@ -1789,6 +1974,13 @@ void Sculpt::Main::_handle_runtime_state()
_file_operation_queue.schedule_next_operations();
_fs_tool_version.value++;
reconfigure_runtime = true;
regenerate_dialog = true;
/* try to proceed after the first step of an depot-index update */
unsigned const orig_download_count = _index_update_queue.download_count;
_index_update_queue.try_schedule_downloads();
if (_index_update_queue.download_count != orig_download_count)
_deploy.update_installation();
/*
* The removal of an index file may have completed, re-query index
@ -1886,6 +2078,7 @@ void Sculpt::Main::_generate_runtime_config(Xml_generator &xml) const
_panel_menu_view.gen_start_node(xml);
_main_menu_view.gen_start_node(xml);
_settings_menu_view.gen_start_node(xml);
_system_menu_view.gen_start_node(xml);
_network_menu_view.gen_start_node(xml);
_popup_menu_view.gen_start_node(xml);
_file_browser_menu_view.gen_start_node(xml);

View File

@ -0,0 +1,50 @@
/*
* \brief Interface to obtain version info about the used system image
* \author Norman Feske
* \date 2022-01-24
*/
/*
* Copyright (C) 2023 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 _BUILD_INFO_H_
#define _BUILD_INFO_H_
#include <types.h>
namespace Sculpt { struct Build_info; }
struct Sculpt::Build_info
{
using Value = String<64>;
using Version = String<64>;
Value genode_source, date, depot_user, board;
Version image_version() const
{
return Version(depot_user, "/sculpt-", board, "-", date);
}
Version genode_version() const
{
return Version("Genode ", genode_source);
}
static Build_info from_xml(Xml_node const &info)
{
return Build_info {
.genode_source = info.attribute_value("genode_version", Value()),
.date = info.attribute_value("date", Value()),
.depot_user = info.attribute_value("depot_user", Value()),
.board = info.attribute_value("board", Value())
};
}
};
#endif /* _BUILD_INFO_H_ */

View File

@ -0,0 +1,75 @@
/*
* \brief Utility for parsing a depot URL into download location and user name
* \author Norman Feske
* \date 2023-03-20
*/
/*
* Copyright (C) 2023 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 _MODEL__DEPOT_URL_H_
#define _MODEL__DEPOT_URL_H_
#include <types.h>
namespace Sculpt { struct Depot_url; }
struct Sculpt::Depot_url
{
using Url = String<128>;
using User = Depot::Archive::User;
Url download; /* download location w/o user sub dir */
User user; /* name of user sub dir */
template <size_t N>
static Depot_url from_string(String<N> const &url)
{
Url download { };
User user { };
auto for_each_slash_pos = [&] (auto const &fn)
{
for (size_t i = 0; i < url.length(); i++)
if (url.string()[i] == '/')
fn(i);
};
unsigned num_slashes = 0;
size_t protocol_len = 0;
size_t last_slash_pos = 0;
using Protocol = String<16>; /* "http://" or "https://" */
Protocol protocol { };
for_each_slash_pos([&] (size_t i) {
num_slashes++;
if (num_slashes == 2) {
protocol_len = i + 1;
protocol = { Cstring(url.string(), protocol_len) };
}
if (num_slashes > 2)
last_slash_pos = i;
});
if (protocol_len && last_slash_pos > protocol_len) {
download = { Cstring(url.string(), last_slash_pos) };
user = { Cstring(url.string() + last_slash_pos + 1) };
}
bool const valid = (protocol == "http://" || protocol == "https://")
&& (user.length() > 1);
return valid ? Depot_url { .download = download, .user = user }
: Depot_url { };
}
bool valid() const { return download.valid() && user.valid(); }
};
#endif /* _MODEL__DEPOT_URL_H_ */

View File

@ -153,6 +153,12 @@ struct Sculpt::Download_queue : Noncopyable
destroy(_alloc, &download); });
}
void reset()
{
_downloads.for_each([&] (Download &download) {
destroy(_alloc, &download); });
}
void gen_installation_entries(Xml_generator &xml) const
{
_downloads.for_each([&] (Download const &download) {

View File

@ -0,0 +1,162 @@
/*
* \brief Queue for tracking the update of depot index files
* \author Norman Feske
* \date 2023-01-27
*
* The update of a depot index takes two steps. First, the index must
* be removed. Then, the index can be requested again via the depot-download
* mechanism.
*/
/*
* Copyright (C) 2019 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 _MODEL__INDEX_UPDATE_QUEUE_H_
#define _MODEL__INDEX_UPDATE_QUEUE_H_
#include <model/download_queue.h>
#include <types.h>
namespace Sculpt { struct Index_update_queue; }
struct Sculpt::Index_update_queue : Noncopyable
{
struct Update : Interface
{
Path const path;
Verify const verify;
enum class State { REMOVING, DOWNLOADING, DONE, FAILED } state;
Update(Path const &path, Verify verify)
:
path(path), verify(verify), state(State::REMOVING)
{ }
bool active() const { return state == State::REMOVING
|| state == State::DOWNLOADING; }
};
Allocator &_alloc;
File_operation_queue &_file_operation_queue;
Download_queue &_download_queue;
Registry<Registered<Update> > _updates { };
/* used for detecting the start of new downloads */
unsigned download_count = 0;
Index_update_queue(Allocator &alloc,
File_operation_queue &file_operation_queue,
Download_queue &download_queue)
:
_alloc(alloc),
_file_operation_queue(file_operation_queue),
_download_queue(download_queue)
{ }
void add(Path const &path, Verify const verify)
{
if (!Depot::Archive::index(path) && !Depot::Archive::image_index(path)) {
warning("attempt to add a non-index path '", path, "' to index-download queue");
return;
}
bool already_exists = false;
_updates.for_each([&] (Update const &update) {
if (update.path == path)
already_exists = true; });
if (already_exists) {
warning("index update triggered while update is already in progress");
return;
}
new (_alloc) Registered<Update>(_updates, path, verify);
_file_operation_queue.remove_file(Path("/rw/depot/", path));
_file_operation_queue.remove_file(Path("/rw/public/", path, ".xz"));
_file_operation_queue.remove_file(Path("/rw/public/", path, ".xz.sig"));
if (!_file_operation_queue.any_operation_in_progress())
_file_operation_queue.schedule_next_operations();
}
void try_schedule_downloads()
{
/*
* Once the 'File_operation_queue' is empty, we know that no removal of
* any index file is still in progres.
*/
if (!_file_operation_queue.empty())
return;
_updates.for_each([&] (Update &update) {
if (update.state == Update::State::REMOVING) {
update.state = Update::State::DOWNLOADING;
_download_queue.add(update.path, update.verify);
download_count++;
}
});
}
bool any_download_scheduled() const
{
bool result = false;
_updates.for_each([&] (Update const &update) {
if (update.state == Update::State::DOWNLOADING)
result = true; });
return result;
}
template <typename FN>
void with_update(Path const &path, FN const &fn) const
{
_updates.for_each([&] (Update const &update) {
if (update.path == path)
fn(update); });
}
void apply_update_state(Xml_node state)
{
state.for_each_sub_node([&] (Xml_node const &elem) {
Path const path = elem.attribute_value("path", Path());
_updates.for_each([&] (Update &update) {
if (update.path != path)
return;
using State = String<16>;
State const state = elem.attribute_value("state", State());
if (state == "done") update.state = Update::State::DONE;
if (state == "failed") update.state = Update::State::FAILED;
if (state == "unavailable") update.state = Update::State::FAILED;
if (state == "corrupted") update.state = Update::State::FAILED;
});
});
}
void remove_inactive_updates()
{
_updates.for_each([&] (Update &update) {
if (!update.active())
destroy(_alloc, &update); });
}
void remove_completed_updates()
{
_updates.for_each([&] (Update &update) {
if (update.state == Update::State::DONE)
destroy(_alloc, &update); });
}
};
#endif /* _MODEL__INDEX_UPDATE_QUEUE_H_ */

View File

@ -0,0 +1,330 @@
/*
* \brief Dialog for selecting a depot user
* \author Norman Feske
* \date 2023-03-17
*/
/*
* Copyright (C) 2023 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__DEPOT_USERS_DIALOG_H_
#define _VIEW__DEPOT_USERS_DIALOG_H_
#include <view/dialog.h>
#include <view/text_entry_field.h>
#include <model/depot_url.h>
namespace Sculpt { struct Depot_users_dialog; }
struct Sculpt::Depot_users_dialog
{
public:
using Depot_users = Attached_rom_dataspace;
using User = Depot::Archive::User;
using Url = Depot_url::Url;
using Hover_result = Hoverable_item::Hover_result;
struct Action : Interface
{
virtual void add_depot_url(Depot_url const &depot_url) = 0;
};
private:
using Url_edit_field = Text_entry_field<50>;
Depot_users const &_depot_users;
Action &_action;
User _selected;
bool _unfolded = false;
Hoverable_item _user { };
Hoverable_item _button { };
Url const _orig_edit_url { "https://" };
Url_edit_field _url_edit_field { _orig_edit_url };
Url _url(Xml_node const &user) const
{
if (!user.has_sub_node("url"))
return { };
Url const url = user.sub_node("url").decoded_content<Url>();
/*
* Ensure that the URL does not contain any '"' character because
* it will be taken as an XML attribute value.
*/
for (char const *s = url.string(); *s; s++)
if (*s == '"')
return { };
User const name = user.attribute_value("name", User());
return Url(url, "/", name);
}
static void _gen_vspacer(Xml_generator &xml, char const *name)
{
gen_named_node(xml, "label", name, [&] () {
xml.attribute("text", " ");
xml.attribute("font", "annotation/regular");
});
}
static inline char const *_add_id() { return "/add"; }
template <typename GEN_LABEL_FN, typename RIGHT_FN>
void _gen_item(Xml_generator &xml, User const &name,
GEN_LABEL_FN const &gen_label_fn,
RIGHT_FN const &right_fn) const
{
bool const selected = (name == _selected);
gen_named_node(xml, "hbox", name, [&] () {
gen_named_node(xml, "float", "left", [&] () {
xml.attribute("west", "yes");
xml.node("hbox", [&] () {
gen_named_node(xml, "float", "button", [&] () {
gen_named_node(xml, "button", "button", [&] () {
_user.gen_hovered_attr(xml, name);
if (selected)
xml.attribute("selected", "yes");
xml.attribute("style", "radio");
xml.node("hbox", [&] () { });
});
});
gen_named_node(xml, "label", "name", [&] () {
gen_label_fn(); });
});
});
gen_named_node(xml, "hbox", "right", [&] () {
right_fn(); });
});
}
void _gen_entry(Xml_generator &xml, Xml_node const user, bool last) const
{
User const name = user.attribute_value("name", User());
bool const selected = (name == _selected);
Url const url = _url(user);
Url const label = Depot_url::from_string(url).valid() ? url : Url(name);
if (!selected && !_unfolded)
return;
_gen_item(xml, name,
[&] /* label */ { xml.attribute("text", Path(" ", label)); },
[&] /* right */ { }
);
if (_unfolded && !last)
_gen_vspacer(xml, String<64>("below ", name).string());
}
Depot_url _depot_url(Xml_node const &depot_users) const
{
Depot_url const result =
Depot_url::from_string(Depot_url::Url { _url_edit_field });
/* check for duplicated user name */
bool unique = true;
depot_users.for_each_sub_node("user", [&] (Xml_node user) {
User const name = user.attribute_value("name", User());
if (name == result.user)
unique = false;
});
return unique ? result : Depot_url { };
}
void _gen_add_entry(Xml_generator &xml, Xml_node const &depot_users) const
{
_gen_item(xml, _add_id(),
[&] /* label */ {
xml.attribute("text", Depot_url::Url(" ", _url_edit_field));
xml.attribute("min_ex", 30);
xml.node("cursor", [&] () {
xml.attribute("at", _url_edit_field.cursor_pos + 1); });
},
[&] /* right */ {
gen_named_node(xml, "float", "actions", [&] {
xml.attribute("east", "yes");
bool const editing = (_selected == _add_id());
if (editing) {
bool const url_valid = _depot_url(depot_users).valid();
gen_named_node(xml, "button", "add", [&] {
if (!url_valid)
xml.attribute("style", "unimportant");
xml.node("label", [&] {
if (!url_valid)
xml.attribute("style", "unimportant");
xml.attribute("text", "Add"); });
});
} else {
gen_named_node(xml, "button", "edit", [&] {
xml.node("label", [&] {
xml.attribute("text", "Edit"); }); });
}
});
}
);
}
void _gen_selection(Xml_generator &xml) const
{
Xml_node const depot_users = _depot_users.xml();
size_t remain_count = depot_users.num_sub_nodes();
remain_count++; /* account for '_gen_add_entry' */
bool known_pubkey = false;
gen_named_node(xml, "frame", "user_selection", [&] () {
xml.node("vbox", [&] () {
depot_users.for_each_sub_node("user", [&] (Xml_node user) {
if (_selected == user.attribute_value("name", User()))
known_pubkey = user.attribute_value("known_pubkey", false);
bool const last = (--remain_count == 0);
_gen_entry(xml, user, last); });
if (_unfolded)
_gen_add_entry(xml, depot_users);
});
});
if (!_unfolded && !known_pubkey) {
gen_named_node(xml, "button", "pubkey warning", [&] {
xml.attribute("style", "invisible");
xml.node("label", [&] {
xml.attribute("font", "annotation/regular");
xml.attribute("text", "missing public key for verification"); });
});
}
}
public:
Depot_users_dialog(Depot_users const &depot_users,
User const &default_user,
Action &action)
:
_depot_users(depot_users), _action(action), _selected(default_user)
{ }
User selected() const
{
return (_selected == _add_id()) ? User() : _selected;
}
void generate(Xml_generator &xml) const { _gen_selection(xml); }
bool unfolded() const { return _unfolded; }
struct User_properties
{
bool exists;
bool download_url;
bool public_key;
};
User_properties selected_user_properties() const
{
User_properties result { };
_depot_users.xml().for_each_sub_node([&] (Xml_node const &user) {
if (_selected == user.attribute_value("name", User())) {
result = {
.exists = true,
.download_url = Depot_url::from_string(_url(user)).valid(),
.public_key = user.attribute_value("known_pubkey", false)
};
}
});
return result;
}
template <typename SELECT_FN>
void click(SELECT_FN const &select_fn)
{
/* unfold depot users */
if (!_unfolded) {
_unfolded = true;
return;
}
/* handle click on unfolded depot-user selection */
auto select_depot_user = [&] (User const &user)
{
_selected = user;
select_fn(user);
_unfolded = false;
_url_edit_field = _orig_edit_url;
};
if (_user._hovered.length() <= 1)
return;
if (_user.hovered(_add_id())) {
if (_button.hovered("add")) {
Depot_url const depot_url = _depot_url(_depot_users.xml());
if (depot_url.valid()) {
_action.add_depot_url(depot_url);
select_depot_user(depot_url.user);
}
} else {
_selected = _add_id();
}
} else {
select_depot_user(_user._hovered);
}
}
Hover_result hover(Xml_node const &hover)
{
return Dialog::any_hover_changed(
_user .match(hover, "frame", "vbox", "hbox", "name"),
_button.match(hover, "frame", "vbox", "hbox", "hbox", "float", "button", "name")
);
}
void reset_hover() { _user._hovered = { }; }
bool hovered() const { return _user._hovered.valid(); }
bool keyboard_needed() const { return _selected == _add_id(); }
void handle_key(Codepoint c)
{
if (_selected != _add_id())
return;
/* prevent input of printable yet risky characters as URL */
if (c.value == ' ' || c.value == '"')
return;
_url_edit_field.apply(c);
}
bool one_selected() const { return !_unfolded && _selected.length() > 1; }
};
#endif /* _VIEW__DEPOT_USERS_DIALOG_H_ */

View File

@ -0,0 +1,64 @@
/*
* \brief GUI layout helper
* \author Norman Feske
* \date 2022-05-20
*/
/*
* Copyright (C) 2022 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__LAYOUT_HELPER_H_
#define _VIEW__LAYOUT_HELPER_H_
#include <view/dialog.h>
/**
* Arrange content in two columns, each with a minimum width of 'min_ex'
*/
template <typename LEFT_FN, typename RIGHT_FN>
static void gen_left_right(Genode::Xml_generator &xml, unsigned min_ex,
LEFT_FN const &left_fn, RIGHT_FN const &right_fn)
{
using namespace Sculpt;
auto gen_hspacer = [&] {
gen_named_node(xml, "label", "hspacer", [&] {
xml.attribute("min_ex", min_ex); }); };
xml.node("hbox", [&] {
gen_named_node(xml, "vbox", "left", [&] {
gen_hspacer();
left_fn();
});
gen_named_node(xml, "vbox", "right", [&] {
gen_hspacer();
right_fn();
});
});
}
/**
* Inflate vertical spacing using an invisble button
*/
template <typename ID>
static void gen_item_vspace(Genode::Xml_generator &xml, ID const &id)
{
using namespace Sculpt;
gen_named_node(xml, "button", id, [&] () {
xml.attribute("style", "invisible");
xml.node("label", [&] () {
xml.attribute("text", " ");
xml.attribute("font", "title/regular");
});
});
}
#endif /* _VIEW__LAYOUT_HELPER_H_ */

View File

@ -25,6 +25,14 @@ void Panel_dialog::generate(Xml_generator &xml) const
gen_named_node(xml, "float", "left", [&] () {
xml.attribute("west", true);
xml.node("hbox", [&] () {
xml.node("button", [&] () {
_item.gen_button_attr(xml, "system");
if (_state.system_visible())
xml.attribute("selected", true);
xml.node("label", [&] () {
xml.attribute("text", "System");
});
});
xml.node("button", [&] () {
_item.gen_button_attr(xml, "settings");
if (_state.settings_visible())

View File

@ -33,6 +33,7 @@ struct Sculpt::Panel_dialog : Dialog
{
virtual Tab selected_tab() const = 0;
virtual bool log_visible() const = 0;
virtual bool system_visible() const = 0;
virtual bool settings_visible() const = 0;
virtual bool network_visible() const = 0;
virtual bool inspect_tab_visible() const = 0;
@ -45,6 +46,7 @@ struct Sculpt::Panel_dialog : Dialog
{
virtual void select_tab(Tab) = 0;
virtual void toggle_log_visibility() = 0;
virtual void toggle_system_visibility() = 0;
virtual void toggle_settings_visibility() = 0;
virtual void toggle_network_visibility() = 0;
};
@ -66,6 +68,7 @@ struct Sculpt::Panel_dialog : Dialog
if (_item.hovered("files")) action.select_tab(Tab::FILES);
if (_item.hovered("inspect")) action.select_tab(Tab::INSPECT);
if (_item.hovered("log")) action.toggle_log_visibility();
if (_item.hovered("system")) action.toggle_system_visibility();
if (_item.hovered("settings")) action.toggle_settings_visibility();
if (_item.hovered("network")) action.toggle_network_visibility();
}

View File

@ -135,4 +135,4 @@ struct Sculpt::Settings_dialog : Noncopyable, Dialog
Settings_dialog(Settings const &settings) : _settings(settings) { }
};
#endif /* _VIEW__RAM_FS_DIALOG_H_ */
#endif /* _VIEW__SETTINGS_DIALOG_H_ */

View File

@ -0,0 +1,162 @@
/*
* \brief Dialog for the deploy presets
* \author Norman Feske
* \date 2023-01-11
*/
/*
* Copyright (C) 2023 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__SOFTWARE_PRESETS_DIALOG_H_
#define _VIEW__SOFTWARE_PRESETS_DIALOG_H_
#include <model/presets.h>
#include <view/dialog.h>
#include <string.h>
namespace Sculpt { struct Software_presets_dialog; }
struct Sculpt::Software_presets_dialog
{
Presets const &_presets;
struct Action : Interface
{
virtual void load_deploy_preset(Presets::Info::Name const &) = 0;
};
Action &_action;
using Hover_result = Hoverable_item::Hover_result;
Presets::Info::Name _selected { };
Hoverable_item _item { };
Activatable_item _operation { };
Software_presets_dialog(Presets const &presets, Action &action)
:
_presets(presets), _action(action)
{ }
using Name = Presets::Info::Name;
void _gen_horizontal_spacer(Xml_generator &xml) const
{
gen_named_node(xml, "label", "spacer", [&] {
xml.attribute("min_ex", 35); });
}
void _gen_preset(Xml_generator &xml, Presets::Info const &preset) const
{
gen_named_node(xml, "vbox", preset.name, [&] () {
gen_named_node(xml, "hbox", preset.name, [&] () {
gen_named_node(xml, "float", "left", [&] () {
xml.attribute("west", "yes");
xml.node("hbox", [&] () {
gen_named_node(xml, "float", "radio", [&] () {
gen_named_node(xml, "button", "button", [&] () {
_item.gen_hovered_attr(xml, preset.name);
if (_selected == preset.name)
xml.attribute("selected", "yes");
xml.attribute("style", "radio");
xml.node("hbox", [&] () { });
});
});
gen_named_node(xml, "label", "name", [&] () {
xml.attribute("text", Name(" ", Pretty(preset.name))); });
gen_item_vspace(xml, "vspace");
});
});
});
if (_selected != preset.name)
return;
auto vspacer = [&] (auto name)
{
gen_named_node(xml, "label", name, [&] () {
xml.attribute("text", " "); });
};
vspacer("spacer1");
gen_named_node(xml, "float", "info", [&] () {
gen_named_node(xml, "label", "text", [&] () {
xml.attribute("text", preset.text); }); });
vspacer("spacer2");
gen_named_node(xml, "float", "operations", [&] () {
gen_named_node(xml, "button", "load", [&] () {
_operation.gen_button_attr(xml, "load");
gen_named_node(xml, "label", "text", [&] () {
xml.attribute("text", " Load "); });
});
});
vspacer("spacer3");
});
}
void generate(Xml_generator &xml) const
{
if (_presets.available())
gen_named_node(xml, "float", "presets", [&] {
xml.node("frame", [&] {
xml.node("vbox", [&] {
_gen_horizontal_spacer(xml);
_presets.for_each([&] (Presets::Info const &info) {
_gen_preset(xml, info); }); }); }); });
}
Hover_result hover(Xml_node hover)
{
return Dialog::any_hover_changed(
_item.match (hover, "float", "frame", "vbox", "vbox", "hbox", "name"),
_operation.match(hover, "float", "frame", "vbox", "vbox", "float", "button", "name")
);
}
bool hovered() const { return _item._hovered.valid(); }
void click()
{
if (_item._hovered.length() > 1)
_selected = _item._hovered;
if (_operation.hovered("load"))
_operation.propose_activation_on_click();
}
void clack()
{
if (_selected.length() <= 1)
return;
_operation.confirm_activation_on_clack();
if (_operation.activated("load")) {
_action.load_deploy_preset(_selected);
_selected = { };
}
_operation.reset();
}
};
#endif /* _VIEW__SOFTWARE_PRESETS_DIALOG_H_ */

View File

@ -0,0 +1,361 @@
/*
* \brief Dialog for software update
* \author Norman Feske
* \date 2023-01-23
*/
/*
* Copyright (C) 2023 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__SOFTWARE_UPDATE_DIALOG_H_
#define _VIEW__SOFTWARE_UPDATE_DIALOG_H_
#include <model/nic_state.h>
#include <model/build_info.h>
#include <model/download_queue.h>
#include <model/index_update_queue.h>
#include <view/depot_users_dialog.h>
namespace Sculpt { struct Software_update_dialog; }
struct Sculpt::Software_update_dialog
{
using Depot_users = Depot_users_dialog::Depot_users;
using User = Depot_users_dialog::User;
using Image_index = Attached_rom_dataspace;
using Url = Depot_users_dialog::Url;
using Version = String<16>;
using User_properties = Depot_users_dialog::User_properties;
Build_info const _build_info;
Nic_state const &_nic_state;
Download_queue const &_download_queue;
Index_update_queue const &_index_update_queue;
File_operation_queue const &_file_operation_queue;
Image_index const &_image_index;
struct Action : Interface
{
virtual void query_image_index (User const &) = 0;
virtual void trigger_image_download(Path const &, Verify) = 0;
virtual void update_image_index (User const &, Verify) = 0;
virtual void install_boot_image (Path const &) = 0;
};
Action &_action;
Depot_users_dialog _users;
Path _last_installed { };
Path _last_selected { };
Path _index_path() const { return Path(_users.selected(), "/image/index"); }
bool _index_update_in_progress() const
{
using Update = Index_update_queue::Update;
bool result = false;
_index_update_queue.with_update(_index_path(), [&] (Update const &update) {
if (update.active())
result = true; });
return result;
}
Path _image_path(Version const &version) const
{
return Path(_users.selected(), "/image/sculpt-", _build_info.board, "-", version);
}
bool _installing() const
{
return _file_operation_queue.copying_to_path("/rw/boot");
};
Hoverable_item _check { };
Hoverable_item _version { };
Hoverable_item _operation { };
using Hover_result = Hoverable_item::Hover_result;
Software_update_dialog(Build_info const &build_info,
Nic_state const &nic_state,
Download_queue const &download_queue,
Index_update_queue const &index_update_queue,
File_operation_queue const &file_operation_queue,
Depot_users const &depot_users,
Image_index const &image_index,
Depot_users_dialog::Action &depot_users_action,
Action &action)
:
_build_info(build_info), _nic_state(nic_state),
_download_queue(download_queue),
_index_update_queue(index_update_queue),
_file_operation_queue(file_operation_queue),
_image_index(image_index),
_action(action),
_users(depot_users, _build_info.depot_user, depot_users_action)
{ }
static void _gen_vspacer(Xml_generator &xml, char const *name)
{
gen_named_node(xml, "label", name, [&] () {
xml.attribute("text", " ");
xml.attribute("font", "annotation/regular");
});
}
void _gen_image_main(Xml_generator &xml, Xml_node const &image) const
{
Version const version = image.attribute_value("version", Version());
bool const present = image.attribute_value("present", false);
Path const path = _image_path(version);
struct Download_state
{
bool in_progress;
bool failed;
unsigned percent;
};
using Download = Download_queue::Download;
auto state_from_download_queue = [&]
{
Download_state result { };
_download_queue.with_download(path, [&] (Download const &download) {
if (download.state == Download::State::DOWNLOADING)
result.in_progress = true;
if (download.state == Download::State::FAILED)
result.failed = true;
result.percent = download.percent;
});
return result;
};
Download_state const download_state = state_from_download_queue();
gen_named_node(xml, "float", "label", [&] {
xml.attribute("west", "yes");
gen_named_node(xml, "label", "label", [&] {
xml.attribute("text", String<50>(" ", version));
xml.attribute("min_ex", "15");
});
});
auto gen_status = [&] (auto message)
{
gen_named_node(xml, "float", "status", [&] {
xml.node("label", [&] {
xml.attribute("font", "annotation/regular");
xml.attribute("text", message); }); });
};
if (image.has_sub_node("info")) {
if (_last_selected == path)
gen_status("Changes");
else
gen_status("...");
}
if (download_state.in_progress && download_state.percent)
gen_status(String<16>(download_state.percent, "%"));
if (download_state.failed)
gen_status("unavailable");
if (_last_installed == path) {
if (_installing())
gen_status("installing...");
else
gen_status("reboot to activate");
}
gen_named_node(xml, "float", "buttons", [&] {
xml.attribute("east", "yes");
xml.node("hbox", [&] {
auto gen_button = [&] (auto id, bool selected, auto text)
{
gen_named_node(xml, "button", id, [&] {
if (version == _version._hovered)
_operation.gen_hovered_attr(xml, id);
if (selected) {
xml.attribute("selected", "yes");
xml.attribute("style", "unimportant");
}
xml.node("label", [&] {
xml.attribute("text", text); });
});
};
if (present)
gen_button("install", _installing(), " Install ");
if (!present)
gen_button("download", download_state.in_progress, " Download ");
});
});
}
void _gen_image_info(Xml_generator &xml, Xml_node const &image) const
{
gen_named_node(xml, "vbox", "main", [&] {
unsigned line = 0;
image.for_each_sub_node("info", [&] (Xml_node const &info) {
/* limit changelog to a sensible maximum of lines */
if (++line > 8)
return;
using Text = String<80>;
Text const text = info.attribute_value("text", Text());
gen_named_node(xml, "float", String<16>(line), [&] {
xml.attribute("west", "yes");
xml.node("label", [&] {
xml.attribute("text", text);
xml.attribute("font", "annotation/regular");
});
});
});
});
}
void _gen_image_entry(Xml_generator &xml, Xml_node const &image) const
{
Version const version = image.attribute_value("version", Version());
Path const path = _image_path(version);
gen_named_node(xml, "frame", version, [&] {
xml.attribute("style", "important");
xml.node("vbox", [&] {
gen_named_node(xml, "float", "main", [&] {
xml.attribute("east", "yes");
xml.attribute("west", "yes");
_gen_image_main(xml, image);
});
if (path == _last_selected && image.has_sub_node("info")) {
_gen_vspacer(xml, "above");
gen_named_node(xml, "float", "info", [&] {
_gen_image_info(xml, image); });
_gen_vspacer(xml, "below");
}
});
});
}
void _gen_image_list(Xml_generator &xml) const
{
Xml_node const index = _image_index.xml();
index.for_each_sub_node("user", [&] (Xml_node const &user) {
if (user.attribute_value("name", User()) == _users.selected())
user.for_each_sub_node("image", [&] (Xml_node const &image) {
_gen_image_entry(xml, image); }); });
}
void _gen_update_dialog(Xml_generator &xml) const
{
gen_named_node(xml, "frame", "update_dialog", [&] {
xml.node("vbox", [&] {
_users.generate(xml);
User_properties const properties = _users.selected_user_properties();
bool const offer_index_update = _users.one_selected()
&& _nic_state.ready()
&& properties.download_url;
if (offer_index_update) {
_gen_vspacer(xml, "above check");
gen_named_node(xml, "float", "check", [&] {
gen_named_node(xml, "button", "check", [&] {
_check.gen_hovered_attr(xml, "check");
if (_index_update_in_progress()) {
xml.attribute("selected", "yes");
xml.attribute("style", "unimportant");
}
xml.node("label", [&] {
auto const text = properties.public_key
? " Check for Updates "
: " Check for unverified Updates ";
xml.attribute("text", text); });
});
});
_gen_vspacer(xml, "below check");
}
});
});
_gen_image_list(xml);
}
void generate(Xml_generator &xml) const
{
gen_named_node(xml, "vbox", "update", [&] {
_gen_update_dialog(xml); });
}
Hover_result hover(Xml_node const &hover)
{
_users.reset_hover();
return Dialog::any_hover_changed(
match_sub_dialog(hover, _users, "vbox", "frame", "vbox"),
_check .match(hover, "vbox", "frame", "vbox", "float", "button", "name"),
_version .match(hover, "vbox", "frame", "name"),
_operation.match(hover, "vbox", "frame", "vbox", "float", "float", "hbox", "button", "name")
);
}
bool hovered() const { return _users.hovered(); }
void click()
{
Verify const verify { _users.selected_user_properties().public_key };
if (_users.hovered())
_users.click([&] (User const &selected_user) {
_action.query_image_index(selected_user); });
if (_check.hovered("check") && !_index_update_in_progress())
_action.update_image_index(_users.selected(), verify);
if (_operation.hovered("download"))
_action.trigger_image_download(_image_path(_version._hovered), verify);
if (_version._hovered.length() > 1)
_last_selected = _image_path(_version._hovered);
if (_operation.hovered("install") && !_installing()) {
_last_installed = _image_path(_version._hovered);
_action.install_boot_image(_last_installed);
}
}
void clack() { }
bool keyboard_needed() const { return _users.keyboard_needed(); }
void handle_key(Codepoint c) { _users.handle_key(c); }
};
#endif /* _VIEW__SOFTWARE_UPDATE_DIALOG_H_ */

View File

@ -0,0 +1,47 @@
/*
* \brief Dialog for showing the system version
* \author Norman Feske
* \date 2023-01-23
*/
/*
* Copyright (C) 2023 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__SOFTWARE_VERSION_DIALOG_H_
#define _VIEW__SOFTWARE_VERSION_DIALOG_H_
#include <model/build_info.h>
#include <view/dialog.h>
namespace Sculpt { struct Software_version_dialog; }
struct Sculpt::Software_version_dialog
{
Build_info const _build_info;
Software_version_dialog(Build_info const &info) : _build_info(info) { }
void generate(Xml_generator &xml) const
{
using Version = Build_info::Version;
auto padded = [] (Version const &v) { return Version(" ", v, " "); };
gen_named_node(xml, "frame", "version", [&] {
xml.node("vbox", [&] {
gen_named_node(xml, "label", "image", [&] {
xml.attribute("text", padded(_build_info.image_version())); });
gen_named_node(xml, "label", "genode", [&] {
xml.attribute("text", padded(_build_info.genode_version()));
xml.attribute("font", "annotation/regular");
});
});
});
}
};
#endif /* _VIEW__SOFTWARE_VERSION_DIALOG_H_ */

View File

@ -0,0 +1,142 @@
/*
* \brief System dialog
* \author Norman Feske
* \date 2023-04-24
*/
/*
* Copyright (C) 2023 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__SYSTEM_DIALOG_H_
#define _VIEW__SYSTEM_DIALOG_H_
#include <view/layout_helper.h>
#include <view/dialog.h>
#include <view/software_presets_dialog.h>
#include <view/software_update_dialog.h>
#include <view/software_version_dialog.h>
#include <model/settings.h>
namespace Sculpt { struct System_dialog; }
struct Sculpt::System_dialog : Noncopyable, Dialog
{
using Depot_users = Depot_users_dialog::Depot_users;
using Image_index = Attached_rom_dataspace;
Hoverable_item _tab_item { };
enum Tab { PRESETS, UPDATE } _selected_tab = Tab::PRESETS;
Software_presets_dialog _presets_dialog;
Software_update_dialog _update_dialog;
Software_version_dialog _version_dialog;
Hover_result hover(Xml_node hover) override
{
Hover_result dialog_hover_result = Hover_result::UNMODIFIED;
hover.with_optional_sub_node("frame", [&] (Xml_node const &frame) {
frame.with_optional_sub_node("vbox", [&] (Xml_node const &vbox) {
switch (_selected_tab) {
case Tab::PRESETS: dialog_hover_result = _presets_dialog.hover(vbox); break;
case Tab::UPDATE: dialog_hover_result = _update_dialog.hover(vbox); break;
}
});
});
return any_hover_changed(
dialog_hover_result,
_tab_item.match(hover, "frame", "vbox", "hbox", "button", "name"));
}
void reset() override { }
void generate(Xml_generator &xml) const override
{
gen_named_node(xml, "frame", "system", [&] {
xml.node("vbox", [&] {
gen_named_node(xml, "hbox", "tabs", [&] {
auto gen_tab = [&] (auto const &id, auto tab, auto const &text)
{
gen_named_node(xml, "button", id, [&] {
_tab_item.gen_hovered_attr(xml, id);
if (_selected_tab == tab)
xml.attribute("selected", "yes");
xml.node("label", [&] {
xml.attribute("text", text);
});
});
};
gen_tab("presets", Tab::PRESETS, " Presets ");
gen_tab("update", Tab::UPDATE, " Update ");
});
switch (_selected_tab) {
case Tab::PRESETS:
_presets_dialog.generate(xml);
break;
case Tab::UPDATE:
_update_dialog .generate(xml);
_version_dialog.generate(xml);
break;
};
});
});
}
Click_result click()
{
if (_tab_item._hovered.valid()) {
if (_tab_item._hovered == "presets") _selected_tab = Tab::PRESETS;
if (_tab_item._hovered == "update") _selected_tab = Tab::UPDATE;
return Click_result::CONSUMED;
};
switch (_selected_tab) {
case Tab::PRESETS: _presets_dialog.click(); break;
case Tab::UPDATE: _update_dialog .click(); break;
}
return Click_result::CONSUMED;
}
Click_result clack()
{
switch (_selected_tab) {
case Tab::PRESETS: _presets_dialog.clack(); break;
case Tab::UPDATE: _update_dialog .clack(); break;
};
return Click_result::CONSUMED;
}
System_dialog(Presets const &presets,
Build_info const &build_info,
Nic_state const &nic_state,
Download_queue const &download_queue,
Index_update_queue const &index_update_queue,
File_operation_queue const &file_operation_queue,
Depot_users const &depot_users,
Image_index const &image_index,
Software_presets_dialog::Action &presets_action,
Depot_users_dialog::Action &depot_users_action,
Software_update_dialog::Action &update_action)
:
_presets_dialog(presets, presets_action),
_update_dialog(build_info, nic_state, download_queue,
index_update_queue, file_operation_queue, depot_users,
image_index, depot_users_action, update_action),
_version_dialog(build_info)
{ }
bool update_tab_selected() const { return _selected_tab == Tab::UPDATE; }
bool keyboard_needed() const { return _update_dialog.keyboard_needed(); }
void handle_key(Codepoint c) { _update_dialog.handle_key(c); }
};
#endif /* _VIEW__SYSTEM_DIALOG_H_ */

View File

@ -0,0 +1,75 @@
/*
* \brief Helper for implementing editable text fields
* \author Norman Feske
* \date 2023-03-17
*/
/*
* Copyright (C) 2023 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__TEXT_ENTRY_FIELD_H_
#define _VIEW__TEXT_ENTRY_FIELD_H_
#include <view/dialog.h>
namespace Sculpt {
template <size_t> struct Text_entry_field;
enum {
CODEPOINT_BACKSPACE = 8, CODEPOINT_NEWLINE = 10,
CODEPOINT_UP = 0xf700, CODEPOINT_DOWN = 0xf701,
CODEPOINT_LEFT = 0xf702, CODEPOINT_RIGHT = 0xf703,
CODEPOINT_HOME = 0xf729, CODEPOINT_INSERT = 0xf727,
CODEPOINT_DELETE = 0xf728, CODEPOINT_END = 0xf72b,
CODEPOINT_PAGEUP = 0xf72c, CODEPOINT_PAGEDOWN = 0xf72d,
};
}
template <Sculpt::size_t N>
struct Sculpt::Text_entry_field
{
Codepoint _elements[N] { };
unsigned cursor_pos = 0;
static bool _printable(Codepoint c)
{
return (c.value >= 32) && (c.value <= 126);
}
void apply(Codepoint c)
{
if (c.value == CODEPOINT_BACKSPACE && cursor_pos > 0) {
cursor_pos--;
_elements[cursor_pos] = { };
}
if (_printable(c) && (cursor_pos + 1) < N) {
_elements[cursor_pos] = c;
cursor_pos++;
}
}
template <typename STRING>
Text_entry_field(STRING const &s)
{
for (Utf8_ptr utf8 = s.string(); utf8.complete(); utf8 = utf8.next())
apply(utf8.codepoint());
}
void print(Output &out) const
{
for (unsigned i = 0; i < N; i++)
if (_printable(_elements[i]))
Genode::print(out, _elements[i]);
}
};
#endif /* _VIEW__TEXT_ENTRY_FIELD_H_ */