diff --git a/repos/gems/sculpt/leitzentrale/default b/repos/gems/sculpt/leitzentrale/default index d9aca015e7..b032b2ef72 100644 --- a/repos/gems/sculpt/leitzentrale/default +++ b/repos/gems/sculpt/leitzentrale/default @@ -127,6 +127,8 @@ report="manager -> network_dialog"/> + + + diff --git a/repos/gems/src/app/sculpt_manager/main.cc b/repos/gems/src/app/sculpt_manager/main.cc index 000e2dc4cf..c6b862b628 100644 --- a/repos/gems/src/app/sculpt_manager/main.cc +++ b/repos/gems/src/app/sculpt_manager/main.cc @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -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_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
_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
_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); diff --git a/repos/gems/src/app/sculpt_manager/model/build_info.h b/repos/gems/src/app/sculpt_manager/model/build_info.h new file mode 100644 index 0000000000..2ff88ad7d9 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/build_info.h @@ -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 + +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_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/depot_url.h b/repos/gems/src/app/sculpt_manager/model/depot_url.h new file mode 100644 index 0000000000..f4bfff778e --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/depot_url.h @@ -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 + +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 + static Depot_url from_string(String 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_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/download_queue.h b/repos/gems/src/app/sculpt_manager/model/download_queue.h index 6d524bc197..3abe016a21 100644 --- a/repos/gems/src/app/sculpt_manager/model/download_queue.h +++ b/repos/gems/src/app/sculpt_manager/model/download_queue.h @@ -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) { diff --git a/repos/gems/src/app/sculpt_manager/model/index_update_queue.h b/repos/gems/src/app/sculpt_manager/model/index_update_queue.h new file mode 100644 index 0000000000..e467888621 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/index_update_queue.h @@ -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 +#include + +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 > _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(_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 + 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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/depot_users_dialog.h b/repos/gems/src/app/sculpt_manager/view/depot_users_dialog.h new file mode 100644 index 0000000000..24d72eb874 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/depot_users_dialog.h @@ -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 +#include +#include + +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(); + + /* + * 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 + 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 + 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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/layout_helper.h b/repos/gems/src/app/sculpt_manager/view/layout_helper.h new file mode 100644 index 0000000000..3ab1e727db --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/layout_helper.h @@ -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 + + +/** + * Arrange content in two columns, each with a minimum width of 'min_ex' + */ +template +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 +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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc b/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc index 7536c15f34..f70129ba2a 100644 --- a/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc +++ b/repos/gems/src/app/sculpt_manager/view/panel_dialog.cc @@ -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()) diff --git a/repos/gems/src/app/sculpt_manager/view/panel_dialog.h b/repos/gems/src/app/sculpt_manager/view/panel_dialog.h index c032570891..7c1ad5e056 100644 --- a/repos/gems/src/app/sculpt_manager/view/panel_dialog.h +++ b/repos/gems/src/app/sculpt_manager/view/panel_dialog.h @@ -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(); } diff --git a/repos/gems/src/app/sculpt_manager/view/settings_dialog.h b/repos/gems/src/app/sculpt_manager/view/settings_dialog.h index b0b93c6ef8..1705662456 100644 --- a/repos/gems/src/app/sculpt_manager/view/settings_dialog.h +++ b/repos/gems/src/app/sculpt_manager/view/settings_dialog.h @@ -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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/software_presets_dialog.h b/repos/gems/src/app/sculpt_manager/view/software_presets_dialog.h new file mode 100644 index 0000000000..d387a3d27a --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/software_presets_dialog.h @@ -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 +#include +#include + +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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/software_update_dialog.h b/repos/gems/src/app/sculpt_manager/view/software_update_dialog.h new file mode 100644 index 0000000000..9fa59e4f21 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/software_update_dialog.h @@ -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 +#include +#include +#include +#include + +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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/software_version_dialog.h b/repos/gems/src/app/sculpt_manager/view/software_version_dialog.h new file mode 100644 index 0000000000..16a5ef693b --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/software_version_dialog.h @@ -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 +#include + +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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/system_dialog.h b/repos/gems/src/app/sculpt_manager/view/system_dialog.h new file mode 100644 index 0000000000..2199c56eee --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/system_dialog.h @@ -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 +#include +#include +#include +#include +#include + +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_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/text_entry_field.h b/repos/gems/src/app/sculpt_manager/view/text_entry_field.h new file mode 100644 index 0000000000..01831dbeae --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/text_entry_field.h @@ -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 + + +namespace Sculpt { + + template 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 +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 + 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_ */