From 5d8f799a1171f92054d4b45ba130cd7fdad0bd01 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Fri, 23 May 2025 14:04:27 +0200 Subject: app: prepare restructuring --- gui/ui/CMakeLists.txt | 59 ++++++ gui/ui/include/turns/ui/fwd.hpp | 14 ++ gui/ui/include/turns/ui/init.hpp | 15 ++ gui/ui/include/turns/ui/participant_editor.hpp | 82 ++++++++ gui/ui/include/turns/ui/participant_row.hpp | 50 +++++ gui/ui/include/turns/ui/preferences.hpp | 53 +++++ gui/ui/include/turns/ui/template_widget.hpp | 67 +++++++ gui/ui/include/turns/ui/tracker.hpp | 104 ++++++++++ gui/ui/include/turns/ui/turn_order_view.hpp | 40 ++++ gui/ui/src/init.cpp | 23 +++ gui/ui/src/participant_editor.cpp | 162 ++++++++++++++++ gui/ui/src/participant_editor.ui | 71 +++++++ gui/ui/src/participant_row.cpp | 149 +++++++++++++++ gui/ui/src/participant_row.ui | 89 +++++++++ gui/ui/src/preferences.cpp | 81 ++++++++ gui/ui/src/preferences.ui | 104 ++++++++++ gui/ui/src/tracker.cpp | 255 +++++++++++++++++++++++++ gui/ui/src/tracker.ui | 153 +++++++++++++++ gui/ui/src/tracker/actions.cpp | 125 ++++++++++++ gui/ui/src/tracker/event_handlers.cpp | 105 ++++++++++ gui/ui/src/turn_order_view.cpp | 64 +++++++ gui/ui/src/turn_order_view.ui | 38 ++++ gui/ui/tests/gtk_test_init.cpp | 41 ++++ gui/ui/tests/participant_editor.cpp | 150 +++++++++++++++ gui/ui/tests/participant_row.cpp | 30 +++ gui/ui/tests/resources.cpp | 21 ++ gui/ui/tests/tracker.cpp | 79 ++++++++ gui/ui/ui.cmb | 10 + 28 files changed, 2234 insertions(+) create mode 100644 gui/ui/CMakeLists.txt create mode 100644 gui/ui/include/turns/ui/fwd.hpp create mode 100644 gui/ui/include/turns/ui/init.hpp create mode 100644 gui/ui/include/turns/ui/participant_editor.hpp create mode 100644 gui/ui/include/turns/ui/participant_row.hpp create mode 100644 gui/ui/include/turns/ui/preferences.hpp create mode 100644 gui/ui/include/turns/ui/template_widget.hpp create mode 100644 gui/ui/include/turns/ui/tracker.hpp create mode 100644 gui/ui/include/turns/ui/turn_order_view.hpp create mode 100644 gui/ui/src/init.cpp create mode 100644 gui/ui/src/participant_editor.cpp create mode 100644 gui/ui/src/participant_editor.ui create mode 100644 gui/ui/src/participant_row.cpp create mode 100644 gui/ui/src/participant_row.ui create mode 100644 gui/ui/src/preferences.cpp create mode 100644 gui/ui/src/preferences.ui create mode 100644 gui/ui/src/tracker.cpp create mode 100644 gui/ui/src/tracker.ui create mode 100644 gui/ui/src/tracker/actions.cpp create mode 100644 gui/ui/src/tracker/event_handlers.cpp create mode 100644 gui/ui/src/turn_order_view.cpp create mode 100644 gui/ui/src/turn_order_view.ui create mode 100644 gui/ui/tests/gtk_test_init.cpp create mode 100644 gui/ui/tests/participant_editor.cpp create mode 100644 gui/ui/tests/participant_row.cpp create mode 100644 gui/ui/tests/resources.cpp create mode 100644 gui/ui/tests/tracker.cpp create mode 100644 gui/ui/ui.cmb (limited to 'gui/ui') diff --git a/gui/ui/CMakeLists.txt b/gui/ui/CMakeLists.txt new file mode 100644 index 0000000..1584479 --- /dev/null +++ b/gui/ui/CMakeLists.txt @@ -0,0 +1,59 @@ +# Library + +file(GLOB_RECURSE UI_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/src" CONFIGURE_DEPENDS "*.ui") +file(GLOB_RECURSE UI_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" CONFIGURE_DEPENDS "src/*.cpp") +file(GLOB_RECURSE UI_TESTS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" CONFIGURE_DEPENDS "tests/*.cpp") + +add_library("ui" ${UI_SOURCES}) +add_library("turns::ui" ALIAS "ui") + +target_compile_options("ui" PUBLIC + "$<$:-Wall>" + "$<$:-Wextra>" + "$<$:-Werror>" + "$<$:-pedantic-errors>" +) + +target_include_directories("ui" PUBLIC + "include" +) + +target_link_libraries("ui" PUBLIC + "turns::core" + "turns::lang" + + "adwaitamm::adwaitamm" + "PkgConfig::gtkmm" +) + +target_add_glib_resources("ui" + PREFIX "/ch/arknet/Turns/" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src" + UI_FILES ${UI_FILES} +) + +enable_coverage("ui") + +# Tests + +get_target_property(TRANSLATIONS_BINARY_DIR "lang" BINARY_DIR) + +add_executable("ui-tests" ${UI_TESTS}) + +target_compile_definitions("ui-tests" PUBLIC + "TESTLOCALEDIR=\"${TRANSLATIONS_BINARY_DIR}\"" +) + +target_link_libraries("ui-tests" PRIVATE + "Catch2::Catch2WithMain" + + "$<$:-Wl,--whole-archive>" + "turns::ui" + "$<$:-Wl,--no-whole-archive>" +) + +target_link_options("ui-tests" PRIVATE + "$<$,$>:--coverage>" +) + +catch_discover_tests("ui-tests") diff --git a/gui/ui/include/turns/ui/fwd.hpp b/gui/ui/include/turns/ui/fwd.hpp new file mode 100644 index 0000000..69dc0b5 --- /dev/null +++ b/gui/ui/include/turns/ui/fwd.hpp @@ -0,0 +1,14 @@ +#ifndef TURNS_UI_WIDGETS_FWD_HPP +#define TURNS_UI_WIDGETS_FWD_HPP + +namespace turns::ui::widgets +{ + struct participant_editor; + struct participant_row; + struct preferences; + struct tracker; + struct turn_order_view; + struct preferences; +} // namespace turns::ui::widgets + +#endif \ No newline at end of file diff --git a/gui/ui/include/turns/ui/init.hpp b/gui/ui/include/turns/ui/init.hpp new file mode 100644 index 0000000..77bd009 --- /dev/null +++ b/gui/ui/include/turns/ui/init.hpp @@ -0,0 +1,15 @@ +#ifndef TURNS_UI_INIT_HPP +#define TURNS_UI_INIT_HPP + +#include + +#include + +namespace turns::ui +{ + + auto register_types() -> void; + +} // namespace turns::ui + +#endif \ No newline at end of file diff --git a/gui/ui/include/turns/ui/participant_editor.hpp b/gui/ui/include/turns/ui/participant_editor.hpp new file mode 100644 index 0000000..0fbc504 --- /dev/null +++ b/gui/ui/include/turns/ui/participant_editor.hpp @@ -0,0 +1,82 @@ +#ifndef TURNS_UI_PARTICIPANT_EDITOR_HPP +#define TURNS_UI_PARTICIPANT_EDITOR_HPP + +#include "turns/core/disposition.hpp" +#include "turns/core/fwd.hpp" +#include "turns/core/participant.hpp" +#include "turns/ui/template_widget.hpp" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace turns::ui +{ + + struct ParticipantEditor : template_widget + { + using SignalFinishedType = sigc::signal; + + auto constexpr inline static children = std::array{ + "disposition", + "finish", + "name", + "priority", + }; + + explicit ParticipantEditor(Glib::RefPtr participant); + + [[nodiscard]] auto get_disposition() const -> core::Disposition; + [[nodiscard]] auto get_name() const -> Glib::ustring; + [[nodiscard]] auto get_participant() const -> Glib::RefPtr; + [[nodiscard]] auto get_priority() const -> double; + + auto set_disposition(core::Disposition value) -> void; + auto set_name(Glib::ustring const & value) -> void; + auto set_participant(Glib::RefPtr const & value) -> void; + auto set_priority(double value) -> void; + + [[nodiscard]] auto property_participant() -> Glib::PropertyProxy>; + [[nodiscard]] auto property_participant() const -> Glib::PropertyProxy_ReadOnly>; + + auto signal_finished() -> SignalFinishedType; + + private: + auto handle_finish_clicked() -> void; + auto handle_item_bind(Glib::RefPtr item) -> void; + auto handle_item_setup(Glib::RefPtr item) -> void; + auto handle_participant_changed() -> void; + + Adwaita::ComboRow * m_disposition; + Gtk::Button * m_finish; + Adwaita::EntryRow * m_name; + Adwaita::SpinRow * m_priority; + + Glib::RefPtr m_disposition_factory; + Glib::RefPtr m_disposition_model; + + Glib::Property> m_participant; + + SignalFinishedType m_signal_finished{}; + }; + +} // namespace turns::ui + +#endif \ No newline at end of file diff --git a/gui/ui/include/turns/ui/participant_row.hpp b/gui/ui/include/turns/ui/participant_row.hpp new file mode 100644 index 0000000..561214b --- /dev/null +++ b/gui/ui/include/turns/ui/participant_row.hpp @@ -0,0 +1,50 @@ +#ifndef TURNS_UI_PARTICIPANT_ROW_HPP +#define TURNS_UI_PARTICIPANT_ROW_HPP + +#include "turns/core/fwd.hpp" +#include "turns/ui/template_widget.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace turns::ui +{ + struct ParticipantRow : template_widget + { + auto constexpr inline static children = std::array{ + "delete", + "edit", + "subtitle", + "title", + "toggle_defeated", + }; + + ParticipantRow(Glib::RefPtr participant); + + auto delete_enabled() -> Glib::PropertyProxy; + auto edit_enabled() -> Glib::PropertyProxy; + + private: + auto handle_delete() -> void; + auto handle_edit() -> void; + + Gtk::Button * m_delete; + Gtk::Button * m_edit; + Gtk::Label * m_subtitle; + Gtk::Label * m_title; + Gtk::ToggleButton * m_toggle_defeated; + + Glib::Property m_delete_enabled; + Glib::Property m_edit_enabled; + }; +} // namespace turns::ui::widgets + +#endif \ No newline at end of file diff --git a/gui/ui/include/turns/ui/preferences.hpp b/gui/ui/include/turns/ui/preferences.hpp new file mode 100644 index 0000000..b68b91c --- /dev/null +++ b/gui/ui/include/turns/ui/preferences.hpp @@ -0,0 +1,53 @@ +#ifndef TURNS_UI_PREFERENCES_HPP +#define TURNS_UI_PREFERENCES_HPP + +#include "turns/ui/template_widget.hpp" + +#include +#include + +#include + +#include +#include + +#include +#include + +#include + +namespace turns::ui +{ + struct Preferences : template_widget + { + + auto constexpr inline static children = std::array{ + "friendly_reset_button", + "hostile_reset_button", + "secret_reset_button", + "friendly_color_button", + "hostile_color_button", + "secret_color_button", + "skip_defeated", + }; + + explicit Preferences(Glib::RefPtr settings = {}); + + private: + auto bind_reset(Glib::ustring const & key, Gtk::Button * button) -> void; + auto bind_setting(Glib::ustring const & key, Gtk::ColorDialogButton * button) -> void; + auto update_sensitive(Glib::ustring const & key, Gtk::Button * button) -> void; + + Glib::RefPtr m_settings; + + Gtk::Button * m_friendly_reset_button{}; + Gtk::Button * m_hostile_reset_button{}; + Gtk::Button * m_secret_reset_button{}; + Gtk::ColorDialogButton * m_friendly_color_button{}; + Gtk::ColorDialogButton * m_hostile_color_button{}; + Gtk::ColorDialogButton * m_secret_color_button{}; + Adwaita::SwitchRow * m_skip_defeated{}; + }; +} // namespace turns::ui::widgets + +#endif \ No newline at end of file diff --git a/gui/ui/include/turns/ui/template_widget.hpp b/gui/ui/include/turns/ui/template_widget.hpp new file mode 100644 index 0000000..7147560 --- /dev/null +++ b/gui/ui/include/turns/ui/template_widget.hpp @@ -0,0 +1,67 @@ +#ifndef TURNS_UI_TEMPLATE_WIDGET_HPP +#define TURNS_UI_TEMPLATE_WIDGET_HPP + +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +namespace turns::ui +{ + + template + struct template_widget : Glib::ExtraClassInit, + BaseWidgetType + { + template + template_widget(Glib::ustring && resource_path, BaseWidgetCtorArgTypes &&... base_widget_ctor_args) + : Glib::ExtraClassInit{class_init, &resource_path, instance_init} + , BaseWidgetType{std::forward(base_widget_ctor_args)...} + { + } + + protected: + template + auto get_widget(char const * name) -> WidgetType * + { + auto self = static_cast(this); + auto widget = GTK_WIDGET(Glib::unwrap(self)); + auto type = G_OBJECT_TYPE(Glib::unwrap(self)); + auto child = GTK_WIDGET(gtk_widget_get_template_child(widget, type, name)); + g_assert_nonnull(child); + return dynamic_cast(Glib::wrap(child)); + } + + private: + auto static class_init(void * g_class, void * g_class_data) -> void + { + g_return_if_fail(GTK_IS_WIDGET_CLASS(g_class)); + + auto resource_path = static_cast(g_class_data); + + gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(g_class), resource_path->c_str()); + + std::ranges::for_each(CustomWidgetType::children, [g_class](auto const & child) { + gtk_widget_class_bind_template_child_full(GTK_WIDGET_CLASS(g_class), child, false, 0); + }); + } + + auto static instance_init(GTypeInstance * instance, void * /* type_class */) -> void + { + g_return_if_fail(GTK_IS_WIDGET(instance)); + + gtk_widget_init_template(GTK_WIDGET(instance)); + } + }; + +} // namespace turns::ui::widgets + +#endif \ No newline at end of file diff --git a/gui/ui/include/turns/ui/tracker.hpp b/gui/ui/include/turns/ui/tracker.hpp new file mode 100644 index 0000000..2e3adf5 --- /dev/null +++ b/gui/ui/include/turns/ui/tracker.hpp @@ -0,0 +1,104 @@ +#ifndef TURNS_UI_TRACKER_HPP +#define TURNS_UI_TRACKER_HPP + +#include "turns/core/turn_order_model.hpp" +#include "turns/ui/template_widget.hpp" +#include "turns/ui/turn_order_view.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace turns::ui +{ + + struct Tracker : template_widget + { + auto constexpr inline static children = std::array{ + "controls", + "empty", + "overlay", + "stack", + "start", + "title", + }; + + Tracker(Glib::RefPtr const & app, Glib::RefPtr const & settings); + + auto load(Glib::RefPtr file) -> void; + + private: + friend auto register_types() -> void; + Tracker(); + + /** Setup */ + auto setup_actions() -> void; + auto setup_colors() -> void; + + /** Actions */ + auto add_participant() -> void; + auto delete_participant(Glib::VariantBase param) -> void; + auto edit_participant(Glib::VariantBase param) -> void; + auto open() -> void; + auto preferences() -> void; + auto save(bool force_ask) -> void; + auto stop() -> void; + + /** Event Handlers */ + auto on_open_response(Glib::RefPtr result, Glib::RefPtr dialog) -> void; + auto on_save_response(Glib::RefPtr result, Glib::RefPtr dialog) -> void; + auto on_load_content_done(Glib::RefPtr result) -> void; + auto on_replace_content_done(Glib::RefPtr result) -> void; + auto on_settings_changed(Glib::ustring key) -> void; + + /** Helpers */ + auto show_error(std::exception const & e) -> void; + auto show_toast(std::string const & message) -> void; + auto start_replace_content() -> void; + auto update_colors() -> void; + auto update_subtitle() -> void; + + Gtk::Revealer * m_controls; + Gtk::Widget * m_empty; + Adwaita::ToastOverlay * m_overlay; + Gtk::Stack * m_stack; + Gtk::Button * m_start; + Adwaita::WindowTitle * m_title; + Glib::RefPtr m_turn_order; + TurnOrderView * m_turn_order_view; + Glib::RefPtr m_settings{}; + Glib::PropertyProxy m_subtitle; + + Glib::RefPtr m_file{}; + std::string m_file_etag{}; + std::string m_file_buffer{}; + + Glib::RefPtr m_css{}; + }; + +} // namespace turns::ui + +#endif \ No newline at end of file diff --git a/gui/ui/include/turns/ui/turn_order_view.hpp b/gui/ui/include/turns/ui/turn_order_view.hpp new file mode 100644 index 0000000..8dae4e4 --- /dev/null +++ b/gui/ui/include/turns/ui/turn_order_view.hpp @@ -0,0 +1,40 @@ +#ifndef TURNS_UI_TURN_ORDER_VIEW_HPP +#define TURNS_UI_TURN_ORDER_VIEW_HPP + +#include "turns/core/fwd.hpp" +#include "turns/core/turn_order_model.hpp" +#include "turns/ui/template_widget.hpp" + +#include +#include + +#include +#include +#include +#include + +#include + +namespace turns::ui +{ + struct TurnOrderView : template_widget + { + using model_type = core::TurnOderModel; + + auto constexpr inline static children = std::array{ + "progress", + "view", + }; + + explicit TurnOrderView(Glib::RefPtr model = {}); + + private: + auto handle_create_row(Glib::RefPtr const item) -> Gtk::Widget *; + + Glib::RefPtr m_model; + Gtk::ProgressBar * m_progress; + Gtk::ListBox * m_view; + }; +} // namespace turns::ui::widgets + +#endif \ No newline at end of file diff --git a/gui/ui/src/init.cpp b/gui/ui/src/init.cpp new file mode 100644 index 0000000..1c0295a --- /dev/null +++ b/gui/ui/src/init.cpp @@ -0,0 +1,23 @@ +#include "turns/ui/init.hpp" + +#include "turns/ui/participant_editor.hpp" +#include "turns/ui/participant_row.hpp" +#include "turns/ui/preferences.hpp" +#include "turns/ui/tracker.hpp" +#include "turns/ui/turn_order_view.hpp" +#include +#include + +namespace turns::ui +{ + + auto register_types() -> void + { + static_cast(ParticipantEditor{{}}); + static_cast(ParticipantRow{{}}); + static_cast(Preferences{{}}); + static_cast(Tracker{}); + static_cast(TurnOrderView{{}}); + } + +} // namespace turns::ui \ No newline at end of file diff --git a/gui/ui/src/participant_editor.cpp b/gui/ui/src/participant_editor.cpp new file mode 100644 index 0000000..8c83559 --- /dev/null +++ b/gui/ui/src/participant_editor.cpp @@ -0,0 +1,162 @@ +#include "turns/ui/participant_editor.hpp" + +#include "turns/core/disposition.hpp" +#include "turns/core/participant.hpp" +#include "turns/lang/messages.hpp" +#include "turns/ui/template_widget.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace turns::ui +{ + namespace + { + auto constexpr static TYPE_NAME = "ParticipantEditor"; + auto constexpr static TEMPLATE = "/ch/arknet/Turns/participant_editor.ui"; + } // namespace + + ParticipantEditor::ParticipantEditor(Glib::RefPtr participant) + : Glib::ObjectBase{TYPE_NAME} + , template_widget{TEMPLATE} + , m_disposition{get_widget("disposition")} + , m_finish{get_widget("finish")} + , m_name{get_widget("name")} + , m_priority{get_widget("priority")} + , m_disposition_factory{Gtk::SignalListItemFactory::create()} + , m_disposition_model{Gtk::StringList::create()} + , m_participant{*this, "participant", nullptr} + { + m_finish->signal_clicked().connect(sigc::mem_fun(*this, &ParticipantEditor::handle_finish_clicked)); + + for (auto n : std::views::iota(std::uint8_t{}, static_cast(core::Disposition::END))) + { + m_disposition_model->append(presentation_name_for(core::Disposition{n})); + } + + m_disposition_factory->signal_bind().connect(sigc::mem_fun(*this, &ParticipantEditor::handle_item_bind)); + m_disposition_factory->signal_setup().connect(sigc::mem_fun(*this, &ParticipantEditor::handle_item_setup)); + + m_disposition->set_factory(m_disposition_factory); + m_disposition->set_model(m_disposition_model); + + property_participant().signal_changed().connect(sigc::mem_fun(*this, &ParticipantEditor::handle_participant_changed)); + + set_participant(participant); + } + + auto ParticipantEditor::get_disposition() const -> core::Disposition + { + return static_cast(m_disposition->get_selected()); + } + + auto ParticipantEditor::get_name() const -> Glib::ustring + { + return m_name->get_text(); + } + + auto ParticipantEditor::get_participant() const -> Glib::RefPtr + { + return m_participant.get_value(); + } + + auto ParticipantEditor::get_priority() const -> double + { + return m_priority->get_value(); + } + + auto ParticipantEditor::set_disposition(core::Disposition value) -> void + { + m_disposition->set_selected(static_cast(value)); + } + + auto ParticipantEditor::set_name(Glib::ustring const & value) -> void + { + m_name->set_text(value); + } + + auto ParticipantEditor::set_participant(Glib::RefPtr const & value) -> void + { + m_participant.set_value(value); + } + + auto ParticipantEditor::set_priority(double value) -> void + { + m_priority->set_value(value); + } + + auto ParticipantEditor::property_participant() -> Glib::PropertyProxy> + { + return m_participant.get_proxy(); + } + + auto ParticipantEditor::property_participant() const -> Glib::PropertyProxy_ReadOnly> + { + return m_participant.get_proxy(); + } + + auto ParticipantEditor::signal_finished() -> SignalFinishedType + { + return m_signal_finished; + } + + auto ParticipantEditor::handle_finish_clicked() -> void + { + m_signal_finished.emit(m_name->get_text(), m_priority->get_value(), static_cast(m_disposition->get_selected())); + close(); + } + + auto ParticipantEditor::handle_item_bind(Glib::RefPtr item) -> void + { + auto value = std::dynamic_pointer_cast(item->get_item())->get_string(); + dynamic_cast(item->get_child())->set_label(value); + } + + auto ParticipantEditor::handle_item_setup(Glib::RefPtr item) -> void + { + item->set_child(*Gtk::make_managed()); + } + + auto ParticipantEditor::handle_participant_changed() -> void + { + auto value = m_participant.get_value(); + set_title(_(value ? lang::edit_participant : lang::add_participant)); + if (value) + { + Glib::Binding::bind_property(value->property_name(), + m_name->property_text(), + Glib::Binding::Flags::BIDIRECTIONAL | Glib::Binding::Flags::SYNC_CREATE); + Glib::Binding::bind_property(value->property_priority(), + m_priority->property_value(), + Glib::Binding::Flags::BIDIRECTIONAL | Glib::Binding::Flags::SYNC_CREATE); + Glib::Binding::bind_property(value->property_disposition(), + m_disposition->property_selected(), + Glib::Binding::Flags::BIDIRECTIONAL | Glib::Binding::Flags::SYNC_CREATE, + [](auto value) { return static_cast(value); }, + [](auto value) { return static_cast(value); }); + } + } + +} // namespace turns::ui \ No newline at end of file diff --git a/gui/ui/src/participant_editor.ui b/gui/ui/src/participant_editor.ui new file mode 100644 index 0000000..6bcf83e --- /dev/null +++ b/gui/ui/src/participant_editor.ui @@ -0,0 +1,71 @@ + + + + + + + + diff --git a/gui/ui/src/participant_row.cpp b/gui/ui/src/participant_row.cpp new file mode 100644 index 0000000..7ce8e53 --- /dev/null +++ b/gui/ui/src/participant_row.cpp @@ -0,0 +1,149 @@ +#include "turns/ui/participant_row.hpp" + +#include "turns/core/disposition.hpp" +#include "turns/core/participant.hpp" +#include "turns/lang/messages.hpp" +#include "turns/ui/template_widget.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace turns::ui +{ + namespace + { + auto constexpr static TYPE_NAME = "ParticipantRow"; + auto constexpr static TEMPLATE = "/ch/arknet/Turns/participant_row.ui"; + + auto css_class_for(core::Disposition value) -> Glib::ustring + { + switch (value) + { + case core::Disposition::Friendly: + return "disposition-friendly"; + case core::Disposition::Hostile: + return "disposition-hostile"; + case core::Disposition::Secret: + return "disposition-secret"; + default: + return ""; + } + } + } // namespace + + ParticipantRow::ParticipantRow(Glib::RefPtr participant) + : Glib::ObjectBase(TYPE_NAME) + , template_widget{TEMPLATE} + , m_delete{get_widget("delete")} + , m_edit{get_widget("edit")} + , m_subtitle{get_widget("subtitle")} + , m_title{get_widget("title")} + , m_toggle_defeated{get_widget("toggle_defeated")} + , m_delete_enabled{*this, "delete-enabled", true} + , m_edit_enabled{*this, "edit-enabled", true} + + { + m_delete->signal_clicked().connect(sigc::mem_fun(*this, &ParticipantRow::handle_delete)); + m_edit->signal_clicked().connect(sigc::mem_fun(*this, &ParticipantRow::handle_edit)); + + Glib::Binding::bind_property(m_subtitle->property_label(), + m_subtitle->property_visible(), + Glib::Binding::Flags::DEFAULT, + sigc::mem_fun(&Glib::ustring::size)); + Glib::Binding::bind_property(m_title->property_label(), + m_title->property_visible(), + Glib::Binding::Flags::INVERT_BOOLEAN, + sigc::mem_fun(&Glib::ustring::size)); + Glib::Binding::bind_property(m_toggle_defeated->property_active(), + m_toggle_defeated->property_icon_name(), + Glib::Binding::Flags::SYNC_CREATE, + [](auto active) { return active ? "face-sick-symbolic" : "face-smile-symbolic"; }); + + // clang-format off + Glib::Binding::bind_property(delete_enabled(), + m_delete->property_sensitive(), + Glib::Binding::Flags::SYNC_CREATE); + Glib::Binding::bind_property(edit_enabled(), + m_edit->property_sensitive(), + Glib::Binding::Flags::SYNC_CREATE); + // clang-format on + + if (participant) + { + Glib::Binding::bind_property(participant->property_name(), m_title->property_label(), Glib::Binding::Flags::SYNC_CREATE); + + Glib::Binding::bind_property(participant->property_priority(), + m_subtitle->property_label(), + Glib::Binding::Flags::SYNC_CREATE, + [](auto n) { return std::vformat(_(lang::priority_number), std::make_format_args(n)); }); + + Glib::Binding::bind_property(participant->property_disposition(), + m_toggle_defeated->property_css_classes(), + Glib::Binding::Flags::SYNC_CREATE, + [this](auto value) { + auto classes = m_toggle_defeated->get_css_classes(); + auto removed = std::ranges::remove_if(classes, [](auto cls) { + return (cls == "disposition-friendly") | (cls == "disposition-hostile") || (cls == "disposition-secret"); + }); + classes.erase(removed.begin(), removed.end()); + classes.push_back(css_class_for(value)); + return classes; + }); + + Glib::Binding::bind_property(participant->property_is_active(), + property_css_classes(), + Glib::Binding::Flags::SYNC_CREATE, + [this](auto value) { + auto classes = get_css_classes(); + if (!value) + { + std::erase(classes, "active-participant"); + } + else + { + classes.push_back("active-participant"); + } + return classes; + }); + } + } + + auto ParticipantRow::delete_enabled() -> Glib::PropertyProxy + { + return m_delete_enabled.get_proxy(); + } + + auto ParticipantRow::edit_enabled() -> Glib::PropertyProxy + { + return m_edit_enabled.get_proxy(); + } + + auto ParticipantRow::handle_delete() -> void + { + auto index = Glib::Variant::create(get_index()); + activate_action("win.delete", index); + } + + auto ParticipantRow::handle_edit() -> void + { + auto index = Glib::Variant::create(get_index()); + activate_action("win.edit", index); + } + +} // namespace turns::ui \ No newline at end of file diff --git a/gui/ui/src/participant_row.ui b/gui/ui/src/participant_row.ui new file mode 100644 index 0000000..b53cc53 --- /dev/null +++ b/gui/ui/src/participant_row.ui @@ -0,0 +1,89 @@ + + + + + + + diff --git a/gui/ui/src/preferences.cpp b/gui/ui/src/preferences.cpp new file mode 100644 index 0000000..88e6d0f --- /dev/null +++ b/gui/ui/src/preferences.cpp @@ -0,0 +1,81 @@ +#include "turns/ui/preferences.hpp" + +#include "turns/core/settings.hpp" +#include "turns/ui/template_widget.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include + +namespace turns::ui +{ + namespace + { + auto constexpr static TYPE_NAME = "Preferences"; + auto constexpr static TEMPLATE = "/ch/arknet/Turns/preferences.ui"; + } // namespace + + Preferences::Preferences(Glib::RefPtr settings) + : Glib::ObjectBase{TYPE_NAME} + , template_widget{TEMPLATE} + , m_settings{settings} + , m_friendly_reset_button{get_widget("friendly_reset_button")} + , m_hostile_reset_button{get_widget("hostile_reset_button")} + , m_secret_reset_button{get_widget("secret_reset_button")} + , m_friendly_color_button{get_widget("friendly_color_button")} + , m_hostile_color_button{get_widget("hostile_color_button")} + , m_secret_color_button{get_widget("secret_color_button")} + , m_skip_defeated{get_widget("skip_defeated")} + { + if (!m_settings) + { + return; + } + + bind_reset(core::settings::key::disposition_friendly_color, m_friendly_reset_button); + bind_setting(core::settings::key::disposition_friendly_color, m_friendly_color_button); + bind_reset(core::settings::key::disposition_hostile_color, m_hostile_reset_button); + bind_setting(core::settings::key::disposition_hostile_color, m_hostile_color_button); + bind_reset(core::settings::key::disposition_secret_color, m_secret_reset_button); + bind_setting(core::settings::key::disposition_secret_color, m_secret_color_button); + + m_settings->bind(core::settings::key::skip_defeated, m_skip_defeated->property_active()); + } + + auto Preferences::bind_reset(Glib::ustring const & key, Gtk::Button * button) -> void + { + m_settings->signal_changed(key).connect([=, this](auto) { update_sensitive(key, button); }); + update_sensitive(key, button); + button->signal_clicked().connect(sigc::bind(sigc::mem_fun(*m_settings, &Gio::Settings::reset), key)); + } + + auto Preferences::bind_setting(Glib::ustring const & key, Gtk::ColorDialogButton * button) -> void + { + m_settings->bind(key, button->property_rgba(), Gio::Settings::BindFlags::DEFAULT, [](auto value) { + return Gdk::RGBA{value}; + }, [](auto color) { return color.to_string(); }); + } + + auto Preferences::update_sensitive(Glib::ustring const & key, Gtk::Button * button) -> void + { + auto v = Glib::Variant{}; + button->set_sensitive(m_settings->get_user_value(key, v)); + } + +} // namespace turns::ui::widgets \ No newline at end of file diff --git a/gui/ui/src/preferences.ui b/gui/ui/src/preferences.ui new file mode 100644 index 0000000..0ee2699 --- /dev/null +++ b/gui/ui/src/preferences.ui @@ -0,0 +1,104 @@ + + + + + + + + diff --git a/gui/ui/src/tracker.cpp b/gui/ui/src/tracker.cpp new file mode 100644 index 0000000..d67a6e0 --- /dev/null +++ b/gui/ui/src/tracker.cpp @@ -0,0 +1,255 @@ +#include "turns/ui/tracker.hpp" + +#include "turns/core/settings.hpp" +#include "turns/core/turn_order_model.hpp" +#include "turns/lang/messages.hpp" +#include "turns/ui/template_widget.hpp" +#include "turns/ui/turn_order_view.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace turns::ui +{ + namespace + { + auto constexpr static TYPE_NAME = "Tracker"; + auto constexpr static TEMPLATE = "/ch/arknet/Turns/tracker.ui"; + } // namespace + + Tracker::Tracker() + : Tracker{{}, core::get_settings()} + { + } + + Tracker::Tracker(Glib::RefPtr const & app, Glib::RefPtr const & settings) + : Glib::ObjectBase{TYPE_NAME} + , template_widget{TEMPLATE, app} + , m_controls{get_widget("controls")} + , m_empty{get_widget("empty")} + , m_overlay{get_widget("overlay")} + , m_stack{get_widget("stack")} + , m_start{get_widget("start")} + , m_title{get_widget("title")} + , m_turn_order{core::TurnOderModel::create()} + , m_turn_order_view{Gtk::make_managed(m_turn_order)} + , m_settings{std::move(settings)} + , m_subtitle{m_title->property_subtitle()} + , m_css{Gtk::CssProvider::create()} + { + setup_colors(); + setup_actions(); + + m_stack->add(*m_turn_order_view); + + m_turn_order->is_empty().signal_changed().connect(sigc::mem_fun(*this, &Tracker::update_subtitle)); + m_turn_order->round_number().signal_changed().connect(sigc::mem_fun(*this, &Tracker::update_subtitle)); + update_subtitle(); + + // clang-format off + Glib::Binding::bind_property(m_turn_order->is_empty(), + m_stack->property_visible_child(), + Glib::Binding::Flags::SYNC_CREATE, + [this](auto empty) { return empty ? m_empty : m_turn_order_view; }); + + Glib::Binding::bind_property(m_turn_order->is_running(), + m_controls->property_reveal_child(), + Glib::Binding::Flags::SYNC_CREATE); + // clang-format on + + m_settings->bind("skip-defeated", m_turn_order->skip_defeated()); + } + + auto Tracker::setup_actions() -> void + { + // win.add_participant + // depends-on: turn_order:state == stopped + { + auto action = add_action("add_participant", sigc::mem_fun(*this, &Tracker::add_participant)); + + Glib::Binding::bind_property(m_turn_order->is_running(), + action->property_enabled(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + } + + // win.clear + // depends-on: turn_order:is_empty == false + { + auto action = add_action("clear", sigc::mem_fun(*m_turn_order, &core::TurnOderModel::clear)); + + Glib::Binding::bind_property(m_turn_order->is_empty(), + action->property_enabled(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + } + + // win.next + // depends-on: turn_order:state == running + { + auto action = add_action("next", sigc::mem_fun(*m_turn_order, &core::TurnOderModel::next)); + + Glib::Binding::bind_property(m_turn_order->is_running(), action->property_enabled(), Glib::Binding::Flags::SYNC_CREATE); + } + + // win.previous + // depends-on: turn_order:has_previous == true + { + auto action = add_action("previous", sigc::mem_fun(*m_turn_order, &core::TurnOderModel::previous)); + + Glib::Binding::bind_property(m_turn_order->has_previous(), action->property_enabled(), Glib::Binding::Flags::SYNC_CREATE); + } + + // win.start + // depends-on: turn_order:is_empty == false + { + auto action = add_action("start", sigc::mem_fun(*m_turn_order, &core::TurnOderModel::start)); + + Glib::Binding::bind_property(m_turn_order->is_empty(), + action->property_enabled(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + + Glib::Binding::bind_property(m_turn_order->is_running(), + m_start->property_visible(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + } + + // win.stop + // depends-on: turn_order:running == true + { + auto action = add_action("stop", sigc::mem_fun(*this, &Tracker::stop)); + + Glib::Binding::bind_property(m_turn_order->is_running(), action->property_enabled(), Glib::Binding::Flags::SYNC_CREATE); + } + + // win.delete + // win.edit + // win.open + // win.preferences + { + add_action_with_parameter("delete", Glib::VARIANT_TYPE_INT32, sigc::mem_fun(*this, &Tracker::delete_participant)); + add_action_with_parameter("edit", Glib::VARIANT_TYPE_INT32, sigc::mem_fun(*this, &Tracker::edit_participant)); + add_action("open", sigc::mem_fun(*this, &Tracker::open)); + add_action("preferences", sigc::mem_fun(*this, &Tracker::preferences)); + } + + // win.save + // depends-on: turn_order:is_empty == false + { + auto action = add_action("save", sigc::bind(sigc::mem_fun(*this, &Tracker::save), false)); + + Glib::Binding::bind_property(m_turn_order->is_empty(), + action->property_enabled(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + } + + // win.save-as + // depends-on: turn_order:is_empty == false + { + auto action = add_action("save-as", sigc::bind(sigc::mem_fun(*this, &Tracker::save), true)); + + Glib::Binding::bind_property(m_turn_order->is_empty(), + action->property_enabled(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + } + } + + auto Tracker::setup_colors() -> void + { + Gtk::CssProvider::add_provider_for_display(get_display(), m_css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + m_settings->signal_changed().connect(sigc::mem_fun(*this, &Tracker::on_settings_changed)); + update_colors(); + } + + auto Tracker::start_replace_content() -> void + { + m_file_buffer = m_turn_order->serialize().dump(2); + m_file->replace_contents_async(sigc::mem_fun(*this, &Tracker::on_replace_content_done), m_file_buffer, m_file_etag); + } + + auto Tracker::show_error(std::exception const & e) -> void + { + auto error = e.what(); + show_toast(std::vformat(_(lang::saving_failed_format), std::make_format_args(error))); + } + + auto Tracker::show_toast(std::string const & message) -> void + { + m_overlay->add_toast(*Adwaita::Toast::create(message)); + } + + auto Tracker::update_colors() -> void + { + auto friendly_color = m_settings->get_string("disposition-color-friendly"); + auto hostile_color = m_settings->get_string("disposition-color-hostile"); + auto secret_color = m_settings->get_string("disposition-color-secret"); + m_css->load_from_string(std::format("@define-color friendly {};\n" + "@define-color hostile {};\n" + "@define-color secret {};\n", + friendly_color.c_str(), + hostile_color.c_str(), + secret_color.c_str())); + } + + auto Tracker::update_subtitle() -> void + { + if (m_turn_order->is_empty()) + { + m_subtitle = _(lang::no_active_turn_order); + } + else + { + auto round_number = m_turn_order->round_number() + 1; + m_subtitle = round_number == 0 ? "" : std::vformat(_(lang::round_number), std::make_format_args(round_number)); + } + } + + auto Tracker::load(Glib::RefPtr file) -> void + { + if (file->query_exists()) + { + m_file = file; + m_file->load_contents_async(sigc::mem_fun(*this, &Tracker::on_load_content_done)); + set_sensitive(false); + } + } + +} // namespace turns::ui diff --git a/gui/ui/src/tracker.ui b/gui/ui/src/tracker.ui new file mode 100644 index 0000000..c4fe324 --- /dev/null +++ b/gui/ui/src/tracker.ui @@ -0,0 +1,153 @@ + + + + + + + +