diff options
Diffstat (limited to 'gui/ui')
28 files changed, 2234 insertions, 0 deletions
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 + "$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall>" + "$<$<CXX_COMPILER_ID:GNU,Clang>:-Wextra>" + "$<$<CXX_COMPILER_ID:GNU,Clang>:-Werror>" + "$<$<CXX_COMPILER_ID:GNU,Clang>:-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" + + "$<$<CXX_COMPILER_ID:GNU,Clang>:-Wl,--whole-archive>" + "turns::ui" + "$<$<CXX_COMPILER_ID:GNU,Clang>:-Wl,--no-whole-archive>" +) + +target_link_options("ui-tests" PRIVATE + "$<$<AND:$<CXX_COMPILER_ID:GNU,Clang>,$<CONFIG:Debug>>:--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 <glibmm/refptr.h> + +#include <adwaitamm/application.hpp> + +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 <sigc++/signal.h> + +#include <glibmm/property.h> +#include <glibmm/propertyproxy.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/listitem.h> +#include <gtkmm/signallistitemfactory.h> +#include <gtkmm/stringlist.h> +#include <gtkmm/widget.h> + +#include <adwaitamm/comborow.hpp> +#include <adwaitamm/dialog.hpp> +#include <adwaitamm/entryrow.hpp> +#include <adwaitamm/spinrow.hpp> + +#include <array> + +namespace turns::ui +{ + + struct ParticipantEditor : template_widget<ParticipantEditor, Adwaita::Dialog> + { + using SignalFinishedType = sigc::signal<void(Glib::ustring, float, core::Disposition)>; + + auto constexpr inline static children = std::array{ + "disposition", + "finish", + "name", + "priority", + }; + + explicit ParticipantEditor(Glib::RefPtr<core::Participant> participant); + + [[nodiscard]] auto get_disposition() const -> core::Disposition; + [[nodiscard]] auto get_name() const -> Glib::ustring; + [[nodiscard]] auto get_participant() const -> Glib::RefPtr<core::Participant>; + [[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<core::Participant> const & value) -> void; + auto set_priority(double value) -> void; + + [[nodiscard]] auto property_participant() -> Glib::PropertyProxy<Glib::RefPtr<core::Participant>>; + [[nodiscard]] auto property_participant() const -> Glib::PropertyProxy_ReadOnly<Glib::RefPtr<core::Participant>>; + + auto signal_finished() -> SignalFinishedType; + + private: + auto handle_finish_clicked() -> void; + auto handle_item_bind(Glib::RefPtr<Gtk::ListItem> item) -> void; + auto handle_item_setup(Glib::RefPtr<Gtk::ListItem> 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<Gtk::SignalListItemFactory> m_disposition_factory; + Glib::RefPtr<Gtk::StringList> m_disposition_model; + + Glib::Property<Glib::RefPtr<core::Participant>> 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 <glibmm/property.h> +#include <glibmm/propertyproxy.h> +#include <glibmm/refptr.h> + +#include <gtkmm/button.h> +#include <gtkmm/label.h> +#include <gtkmm/listboxrow.h> +#include <gtkmm/togglebutton.h> + +#include <array> + +namespace turns::ui +{ + struct ParticipantRow : template_widget<ParticipantRow, Gtk::ListBoxRow> + { + auto constexpr inline static children = std::array{ + "delete", + "edit", + "subtitle", + "title", + "toggle_defeated", + }; + + ParticipantRow(Glib::RefPtr<core::Participant> participant); + + auto delete_enabled() -> Glib::PropertyProxy<bool>; + auto edit_enabled() -> Glib::PropertyProxy<bool>; + + 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<bool> m_delete_enabled; + Glib::Property<bool> 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 <glibmm/refptr.h> +#include <glibmm/ustring.h> + +#include <giomm/settings.h> + +#include <gtkmm/button.h> +#include <gtkmm/colordialogbutton.h> + +#include <adwaitamm/preferencespage.hpp> +#include <adwaitamm/switchrow.hpp> + +#include <array> + +namespace turns::ui +{ + struct Preferences : template_widget<Preferences, Adwaita::PreferencesPage> + { + + 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<Gio::Settings> 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<Gio::Settings> 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 <glibmm/extraclassinit.h> +#include <glibmm/ustring.h> +#include <glibmm/wrap.h> + +#include <gtkmm/widget.h> + +#include <glib-object.h> +#include <glib.h> +#include <gtk/gtk.h> + +#include <algorithm> +#include <utility> + +namespace turns::ui +{ + + template<typename CustomWidgetType, typename BaseWidgetType> + struct template_widget : Glib::ExtraClassInit, + BaseWidgetType + { + template<typename... BaseWidgetCtorArgTypes> + template_widget(Glib::ustring && resource_path, BaseWidgetCtorArgTypes &&... base_widget_ctor_args) + : Glib::ExtraClassInit{class_init, &resource_path, instance_init} + , BaseWidgetType{std::forward<BaseWidgetCtorArgTypes>(base_widget_ctor_args)...} + { + } + + protected: + template<typename WidgetType = Gtk::Widget> + auto get_widget(char const * name) -> WidgetType * + { + auto self = static_cast<CustomWidgetType *>(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<WidgetType *>(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<Glib::ustring const *>(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 <glibmm/propertyproxy.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <glibmm/variant.h> + +#include <giomm/asyncresult.h> +#include <giomm/file.h> +#include <giomm/settings.h> + +#include <gtkmm/applicationwindow.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/cssprovider.h> +#include <gtkmm/filedialog.h> +#include <gtkmm/revealer.h> +#include <gtkmm/stack.h> +#include <gtkmm/widget.h> + +#include <adwaitamm/application.hpp> +#include <adwaitamm/applicationwindow.hpp> +#include <adwaitamm/toastoverlay.hpp> +#include <adwaitamm/windowtitle.hpp> + +#include <array> +#include <exception> +#include <string> + +namespace turns::ui +{ + + struct Tracker : template_widget<Tracker, Adwaita::ApplicationWindow> + { + auto constexpr inline static children = std::array{ + "controls", + "empty", + "overlay", + "stack", + "start", + "title", + }; + + Tracker(Glib::RefPtr<Adwaita::Application> const & app, Glib::RefPtr<Gio::Settings> const & settings); + + auto load(Glib::RefPtr<Gio::File> 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<Gio::AsyncResult> result, Glib::RefPtr<Gtk::FileDialog> dialog) -> void; + auto on_save_response(Glib::RefPtr<Gio::AsyncResult> result, Glib::RefPtr<Gtk::FileDialog> dialog) -> void; + auto on_load_content_done(Glib::RefPtr<Gio::AsyncResult> result) -> void; + auto on_replace_content_done(Glib::RefPtr<Gio::AsyncResult> 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<core::TurnOderModel> m_turn_order; + TurnOrderView * m_turn_order_view; + Glib::RefPtr<Gio::Settings> m_settings{}; + Glib::PropertyProxy<Glib::ustring> m_subtitle; + + Glib::RefPtr<Gio::File> m_file{}; + std::string m_file_etag{}; + std::string m_file_buffer{}; + + Glib::RefPtr<Gtk::CssProvider> 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 <glibmm/object.h> +#include <glibmm/refptr.h> + +#include <gtkmm/box.h> +#include <gtkmm/listbox.h> +#include <gtkmm/progressbar.h> +#include <gtkmm/widget.h> + +#include <array> + +namespace turns::ui +{ + struct TurnOrderView : template_widget<TurnOrderView, Gtk::Box> + { + using model_type = core::TurnOderModel; + + auto constexpr inline static children = std::array{ + "progress", + "view", + }; + + explicit TurnOrderView(Glib::RefPtr<model_type> model = {}); + + private: + auto handle_create_row(Glib::RefPtr<Glib::Object> const item) -> Gtk::Widget *; + + Glib::RefPtr<model_type> 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 <glibmm/refptr.h> +#include <adwaitamm/application.hpp> + +namespace turns::ui +{ + + auto register_types() -> void + { + static_cast<void>(ParticipantEditor{{}}); + static_cast<void>(ParticipantRow{{}}); + static_cast<void>(Preferences{{}}); + static_cast<void>(Tracker{}); + static_cast<void>(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 <sigc++/functors/mem_fun.h> + +#include <glibmm/binding.h> +#include <glibmm/i18n.h> +#include <glibmm/objectbase.h> +#include <glibmm/propertyproxy.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> + +#include <gtkmm/button.h> +#include <gtkmm/label.h> +#include <gtkmm/listitem.h> +#include <gtkmm/object.h> +#include <gtkmm/signallistitemfactory.h> +#include <gtkmm/stringlist.h> +#include <gtkmm/stringobject.h> + +#include <adwaitamm/comborow.hpp> +#include <adwaitamm/entryrow.hpp> +#include <adwaitamm/spinrow.hpp> + +#include <cstdint> +#include <memory> +#include <ranges> + +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<core::Participant> participant) + : Glib::ObjectBase{TYPE_NAME} + , template_widget{TEMPLATE} + , m_disposition{get_widget<Adwaita::ComboRow>("disposition")} + , m_finish{get_widget<Gtk::Button>("finish")} + , m_name{get_widget<Adwaita::EntryRow>("name")} + , m_priority{get_widget<Adwaita::SpinRow>("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<std::uint8_t>(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<core::Disposition>(m_disposition->get_selected()); + } + + auto ParticipantEditor::get_name() const -> Glib::ustring + { + return m_name->get_text(); + } + + auto ParticipantEditor::get_participant() const -> Glib::RefPtr<core::Participant> + { + 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<unsigned>(value)); + } + + auto ParticipantEditor::set_name(Glib::ustring const & value) -> void + { + m_name->set_text(value); + } + + auto ParticipantEditor::set_participant(Glib::RefPtr<core::Participant> 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<Glib::RefPtr<core::Participant>> + { + return m_participant.get_proxy(); + } + + auto ParticipantEditor::property_participant() const -> Glib::PropertyProxy_ReadOnly<Glib::RefPtr<core::Participant>> + { + 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<core::Disposition>(m_disposition->get_selected())); + close(); + } + + auto ParticipantEditor::handle_item_bind(Glib::RefPtr<Gtk::ListItem> item) -> void + { + auto value = std::dynamic_pointer_cast<Gtk::StringObject>(item->get_item())->get_string(); + dynamic_cast<Gtk::Label *>(item->get_child())->set_label(value); + } + + auto ParticipantEditor::handle_item_setup(Glib::RefPtr<Gtk::ListItem> item) -> void + { + item->set_child(*Gtk::make_managed<Gtk::Label>()); + } + + 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<unsigned>(value); }, + [](auto value) { return static_cast<core::Disposition>(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 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Created with Cambalache 0.96.0 --> +<interface> + <!-- interface-name participant_editor.ui --> + <requires lib="gtk" version="4.18"/> + <requires lib="libadwaita" version="1.7"/> + <template class="gtkmm__CustomObject_ParticipantEditor" parent="AdwDialog"> + <property name="child"> + <object class="AdwToolbarView"> + <property name="content"> + <object class="AdwClamp"> + <property name="child"> + <object class="GtkBox"> + <property name="hexpand">True</property> + <property name="margin-bottom">18</property> + <property name="margin-end">12</property> + <property name="margin-start">12</property> + <property name="margin-top">18</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <property name="valign">start</property> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwEntryRow" id="name"> + <property name="title" translatable="yes">Name</property> + </object> + </child> + <child> + <object class="AdwSpinRow" id="priority"> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">-1000.0</property> + <property name="step-increment">1.0</property> + <property name="upper">1000.0</property> + </object> + </property> + <property name="digits">1</property> + <property name="numeric">True</property> + <property name="title" translatable="yes">Priority</property> + </object> + </child> + <child> + <object class="AdwComboRow" id="disposition"> + <property name="title" translatable="yes">Disposition</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkButton" id="finish"> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Finish</property> + <style> + <class name="pill"/> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </property> + </object> + </property> + <child type="top"> + <object class="AdwHeaderBar"/> + </child> + </object> + </property> + <property name="hexpand">True</property> + </template> +</interface> 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 <sigc++/functors/mem_fun.h> + +#include <glibmm/binding.h> +#include <glibmm/i18n.h> +#include <glibmm/objectbase.h> +#include <glibmm/propertyproxy.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <glibmm/variant.h> + +#include <gtkmm/button.h> +#include <gtkmm/label.h> +#include <gtkmm/listboxrow.h> +#include <gtkmm/togglebutton.h> + +#include <algorithm> +#include <format> +#include <vector> + +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<core::Participant> participant) + : Glib::ObjectBase(TYPE_NAME) + , template_widget{TEMPLATE} + , m_delete{get_widget<Gtk::Button>("delete")} + , m_edit{get_widget<Gtk::Button>("edit")} + , m_subtitle{get_widget<Gtk::Label>("subtitle")} + , m_title{get_widget<Gtk::Label>("title")} + , m_toggle_defeated{get_widget<Gtk::ToggleButton>("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<bool> + { + return m_delete_enabled.get_proxy(); + } + + auto ParticipantRow::edit_enabled() -> Glib::PropertyProxy<bool> + { + return m_edit_enabled.get_proxy(); + } + + auto ParticipantRow::handle_delete() -> void + { + auto index = Glib::Variant<int>::create(get_index()); + activate_action("win.delete", index); + } + + auto ParticipantRow::handle_edit() -> void + { + auto index = Glib::Variant<int>::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 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Created with Cambalache 0.96.0 --> +<interface> + <!-- interface-name participant_row.ui --> + <requires lib="gtk" version="4.18"/> + <template class="gtkmm__CustomObject_ParticipantRow" parent="GtkListBoxRow"> + <property name="activatable">False</property> + <property name="child"> + <object class="GtkBox"> + <child> + <object class="GtkBox"> + <child> + <object class="GtkToggleButton" id="toggle_defeated"> + <property name="halign">center</property> + <property name="icon-name">face-smile-symbolic</property> + <property name="tooltip-text" translatable="yes">Mark as defeated</property> + <property name="valign">center</property> + <style> + <class name="circular"/> + </style> + </object> + </child> + <style> + <class name="prefixes"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <property name="valign">center</property> + <child> + <object class="GtkLabel" id="title"> + <property name="wrap-mode">word-char</property> + <property name="xalign">0.0</property> + <style> + <class name="title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="subtitle"> + <property name="wrap-mode">word-char</property> + <property name="xalign">0.0</property> + <style> + <class name="subtitle"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="valign">center</property> + <child> + <object class="GtkButton" id="delete"> + <property name="icon-name">edit-delete-symbolic</property> + <property name="tooltip-text" translatable="yes">Delete participant</property> + <style> + <class name="circular"/> + <class name="destructive-action"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="edit"> + <property name="icon-name">document-edit-symbolic</property> + <property name="tooltip-text" translatable="yes">Edit participant</property> + <style> + <class name="circular"/> + <class name="suggested-action"/> + </style> + </object> + </child> + <style> + <class name="suffixes"/> + </style> + </object> + </child> + <style> + <class name="header"/> + </style> + </object> + </property> + <property name="selectable">False</property> + <property name="valign">center</property> + </template> +</interface> 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 <sigc++/adaptors/bind.h> +#include <sigc++/functors/mem_fun.h> + +#include <glibmm/binding.h> +#include <glibmm/objectbase.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <glibmm/variant.h> + +#include <giomm/settings.h> + +#include <gtkmm/button.h> +#include <gtkmm/colordialog.h> +#include <gtkmm/colordialogbutton.h> +#include <gtkmm/enums.h> + +#include <adwaitamm/switchrow.hpp> + +#include <gdkmm/rgba.h> + +namespace turns::ui +{ + namespace + { + auto constexpr static TYPE_NAME = "Preferences"; + auto constexpr static TEMPLATE = "/ch/arknet/Turns/preferences.ui"; + } // namespace + + Preferences::Preferences(Glib::RefPtr<Gio::Settings> settings) + : Glib::ObjectBase{TYPE_NAME} + , template_widget{TEMPLATE} + , m_settings{settings} + , m_friendly_reset_button{get_widget<Gtk::Button>("friendly_reset_button")} + , m_hostile_reset_button{get_widget<Gtk::Button>("hostile_reset_button")} + , m_secret_reset_button{get_widget<Gtk::Button>("secret_reset_button")} + , m_friendly_color_button{get_widget<Gtk::ColorDialogButton>("friendly_color_button")} + , m_hostile_color_button{get_widget<Gtk::ColorDialogButton>("hostile_color_button")} + , m_secret_color_button{get_widget<Gtk::ColorDialogButton>("secret_color_button")} + , m_skip_defeated{get_widget<Adwaita::SwitchRow>("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<Glib::ustring, Gdk::RGBA>(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<Glib::ustring>{}; + 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 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Created with Cambalache 0.96.0 --> +<interface> + <!-- interface-name preferences.ui --> + <requires lib="gtk" version="4.18"/> + <requires lib="libadwaita" version="1.7"/> + <template class="gtkmm__CustomObject_Preferences" parent="AdwPreferencesPage"> + <property name="icon-name">preferences-system-symbolic</property> + <property name="title" translatable="yes">Preferences</property> + <child> + <object class="AdwPreferencesGroup" id="colors"> + <property name="title" translatable="yes">Disposition Colors</property> + <child> + <object class="AdwActionRow"> + <property name="activatable-widget">friendly_color_button</property> + <property name="title" translatable="yes">Friendly</property> + <child> + <object class="GtkColorDialogButton" id="friendly_color_button"> + <property name="dialog"> + <object class="GtkColorDialog"> + <property name="with-alpha">False</property> + </object> + </property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkButton" id="friendly_reset_button"> + <property name="icon-name">process-stop-symbolic</property> + <property name="tooltip-text" translatable="yes">Reset</property> + <property name="valign">center</property> + <style> + <class name="circular"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow"> + <property name="activatable-widget">hostile_color_button</property> + <property name="title" translatable="yes">Hostile</property> + <child> + <object class="GtkColorDialogButton" id="hostile_color_button"> + <property name="dialog"> + <object class="GtkColorDialog"> + <property name="with-alpha">False</property> + </object> + </property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkButton" id="hostile_reset_button"> + <property name="icon-name">process-stop-symbolic</property> + <property name="tooltip-text" translatable="yes">Reset</property> + <property name="valign">center</property> + <style> + <class name="circular"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow"> + <property name="activatable-widget">secret_color_button</property> + <property name="title" translatable="yes">Secret</property> + <child> + <object class="GtkColorDialogButton" id="secret_color_button"> + <property name="dialog"> + <object class="GtkColorDialog"> + <property name="with-alpha">False</property> + </object> + </property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkButton" id="secret_reset_button"> + <property name="icon-name">process-stop-symbolic</property> + <property name="tooltip-text" translatable="yes">Reset</property> + <property name="valign">center</property> + <style> + <class name="circular"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="control"> + <property name="title" translatable="yes">Flow</property> + <child> + <object class="AdwSwitchRow" id="skip_defeated"> + <property name="title" translatable="yes">Skip defeated</property> + </object> + </child> + </object> + </child> + </template> +</interface> 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 <sigc++/adaptors/bind.h> +#include <sigc++/functors/mem_fun.h> + +#include <glibmm/binding.h> +#include <glibmm/i18n.h> +#include <glibmm/objectbase.h> +#include <glibmm/propertyproxy.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <glibmm/varianttype.h> +#include <glibmm/wrap.h> + +#include <giomm/file.h> +#include <giomm/liststore.h> +#include <giomm/settings.h> + +#include <gtkmm/applicationwindow.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/cssprovider.h> +#include <gtkmm/error.h> +#include <gtkmm/filedialog.h> +#include <gtkmm/object.h> +#include <gtkmm/revealer.h> +#include <gtkmm/stack.h> +#include <gtkmm/styleprovider.h> +#include <gtkmm/widget.h> + +#include <adwaitamm/application.hpp> +#include <adwaitamm/applicationwindow.hpp> +#include <adwaitamm/toast.hpp> +#include <adwaitamm/toastoverlay.hpp> +#include <adwaitamm/windowtitle.hpp> + +#include <gtk/gtk.h> +#include <nlohmann/json.hpp> + +#include <exception> +#include <format> +#include <print> +#include <string> +#include <utility> + +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<Adwaita::Application> const & app, Glib::RefPtr<Gio::Settings> const & settings) + : Glib::ObjectBase{TYPE_NAME} + , template_widget{TEMPLATE, app} + , m_controls{get_widget<Gtk::Revealer>("controls")} + , m_empty{get_widget<Gtk::Widget>("empty")} + , m_overlay{get_widget<Adwaita::ToastOverlay>("overlay")} + , m_stack{get_widget<Gtk::Stack>("stack")} + , m_start{get_widget<Gtk::Button>("start")} + , m_title{get_widget<Adwaita::WindowTitle>("title")} + , m_turn_order{core::TurnOderModel::create()} + , m_turn_order_view{Gtk::make_managed<TurnOrderView>(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<Gio::File> 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 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Created with Cambalache 0.96.0 --> +<interface> + <!-- interface-name tracker.ui --> + <requires lib="gio" version="2.0"/> + <requires lib="gtk" version="4.18"/> + <requires lib="libadwaita" version="1.7"/> + <template class="gtkmm__CustomObject_Tracker" parent="AdwApplicationWindow"> + <property name="content"> + <object class="AdwToolbarView"> + <property name="content"> + <object class="AdwToastOverlay" id="overlay"> + <property name="child"> + <object class="GtkStack" id="stack"> + <child> + <object class="AdwStatusPage" id="empty"> + <property name="child"> + <object class="GtkButton"> + <property name="action-name">win.add_participant</property> + <property name="halign">center</property> + <property name="label" translatable="yes">Add participant</property> + <style> + <class name="pill"/> + <class name="suggested-action"/> + </style> + </object> + </property> + <property name="icon-name">contact-new-symbolic</property> + </object> + </child> + </object> + </property> + </object> + </property> + <child type="top"> + <object class="AdwHeaderBar" id="header"> + <property name="centering-policy">strict</property> + <property name="title-widget"> + <object class="AdwWindowTitle" id="title"> + <property name="hexpand">True</property> + <property name="subtitle" translatable="yes">No active turn order</property> + <property name="title" translatable="yes">Turns</property> + </object> + </property> + <child type="end"> + <object class="GtkMenuButton" id="open_main_menu"> + <property name="icon-name">open-menu</property> + <property name="menu-model">main_menu</property> + <property name="tooltip-text" translatable="yes">Main Menu</property> + <property name="use-underline">True</property> + </object> + </child> + <child type="start"> + <object class="GtkButton" id="add_participant"> + <property name="action-name">win.add_participant</property> + <property name="icon-name">contact-new</property> + <property name="tooltip-text" translatable="yes">Add participant</property> + </object> + </child> + <child type="start"> + <object class="GtkButton" id="start"> + <property name="action-name">win.start</property> + <property name="icon-name">media-playback-start-symbolic</property> + <property name="tooltip-text" translatable="yes">Start turn order</property> + </object> + </child> + <style/> + </object> + </child> + <child type="bottom"> + <object class="GtkRevealer" id="controls"> + <property name="child"> + <object class="GtkActionBar"> + <child type="start"> + <object class="GtkButton"> + <property name="action-name">win.previous</property> + <property name="icon-name">media-skip-backward-symbolic</property> + <property name="tooltip-markup" translatable="yes">Previous participant</property> + <style> + <class name="pill"/> + <class name="suggested-action"/> + </style> + </object> + </child> + <child type="center"> + <object class="GtkButton"> + <property name="action-name">win.stop</property> + <property name="icon-name">media-playback-stop-symbolic</property> + <property name="tooltip-markup" translatable="yes">End turn order</property> + <style> + <class name="pill"/> + <class name="destructive-action"/> + </style> + </object> + </child> + <child type="end"> + <object class="GtkButton"> + <property name="action-name">win.next</property> + <property name="icon-name">media-skip-forward-symbolic</property> + <property name="tooltip-markup" translatable="yes">Next participant</property> + <style> + <class name="pill"/> + <class name="suggested-action"/> + </style> + </object> + </child> + <style> + <class name="toolbar"/> + </style> + </object> + </property> + <property name="transition-type">slide-up</property> + </object> + </child> + <style/> + </object> + </property> + <property name="default-height">720</property> + <property name="default-width">360</property> + <property name="height-request">480</property> + <property name="width-request">360</property> + </template> + <menu id="main_menu"> + <item> + <attribute name="action">win.clear</attribute> + <attribute name="label" translatable="yes">_Clear</attribute> + </item> + <item> + <attribute name="action">win.open</attribute> + <attribute name="label" translatable="yes">_Open...</attribute> + </item> + <item> + <attribute name="action">win.save</attribute> + <attribute name="label" translatable="yes">_Save</attribute> + </item> + <item> + <attribute name="action">win.save-as</attribute> + <attribute name="label" translatable="yes">Save as...</attribute> + </item> + <item> + <attribute name="action">win.preferences</attribute> + <attribute name="label" translatable="yes">_Preferences</attribute> + </item> + <item> + <attribute name="action">app.quit</attribute> + <attribute name="label" translatable="yes">_Quit</attribute> + </item> + <item> + <attribute name="action">app.about</attribute> + <attribute name="label">About</attribute> + </item> + </menu> +</interface> diff --git a/gui/ui/src/tracker/actions.cpp b/gui/ui/src/tracker/actions.cpp new file mode 100644 index 0000000..ba48177 --- /dev/null +++ b/gui/ui/src/tracker/actions.cpp @@ -0,0 +1,125 @@ +#include "turns/core/participant.hpp" +#include "turns/lang/messages.hpp" +#include "turns/ui/participant_editor.hpp" +#include "turns/ui/preferences.hpp" +#include "turns/ui/tracker.hpp" + +#include <sigc++/adaptors/bind.h> +#include <sigc++/functors/mem_fun.h> + +#include <glibmm/i18n.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <glibmm/variant.h> +#include <glibmm/wrap.h> + +#include <giomm/liststore.h> +#include <giomm/settings.h> + +#include <gtkmm/builder.h> +#include <gtkmm/filedialog.h> +#include <gtkmm/filefilter.h> +#include <gtkmm/object.h> + +#include <adwaitamm/alertdialog.hpp> +#include <adwaitamm/dialog.hpp> +#include <adwaitamm/enums.hpp> +#include <adwaitamm/preferencesdialog.hpp> + +#include <gio/gio.h> +#include <gtk/gtk.h> + +#include <utility> + +namespace turns::ui +{ + namespace + { + auto file_filters() + { + auto filters = Gio::ListStore<Gtk::FileFilter>::create(); + auto filter = Gtk::FileFilter::create(); + filter->set_name(_("Turns Files")); + filter->add_pattern("*.trns"); + filters->append(filter); + return filters; + } + } // namespace + + auto Tracker::add_participant() -> void + { + auto dialog = Gtk::make_managed<ParticipantEditor>(nullptr); + dialog->signal_finished().connect([this](auto n, auto p, auto d) { m_turn_order->add(n, p, d); }); + dialog->present(this); + } + + auto Tracker::delete_participant(Glib::VariantBase param) -> void + { + auto index = Glib::VariantBase::cast_dynamic<Glib::Variant<int>>(param); + m_turn_order->remove(index.get()); + } + + auto Tracker::edit_participant(Glib::VariantBase param) -> void + { + auto index = Glib::VariantBase::cast_dynamic<Glib::Variant<int>>(param); + auto participant = m_turn_order->get_typed_object<core::Participant>(index.get()); + auto dialog = Gtk::make_managed<ParticipantEditor>(participant); + dialog->present(this); + } + + auto Tracker::open() -> void + { + auto dialog = Gtk::FileDialog::create(); + dialog->set_filters(file_filters()); + dialog->open(sigc::bind(sigc::mem_fun(*this, &Tracker::on_open_response), dialog)); + } + + auto Tracker::preferences() -> void + { + auto preferences = Gtk::make_managed<struct Preferences>(m_settings); + auto dialog = Gtk::make_managed<Adwaita::PreferencesDialog>(); + dialog->add(*preferences); + dialog->set_visible_page(*preferences); + dialog->present(this); + } + + auto Tracker::save(bool force_ask) -> void + { + if (m_file && !force_ask) + { + start_replace_content(); + } + else + { + auto dialog = Gtk::FileDialog::create(); + m_file ? dialog->set_initial_file(m_file) : dialog->set_initial_name(_(lang::new_turn_order_file_name)); + dialog->set_filters(file_filters()); + dialog->save(*this, sigc::bind(sigc::mem_fun(*this, &Tracker::on_save_response), dialog)); + } + } + + auto Tracker::stop() -> void + { + auto dialog = Adwaita::AlertDialog::create(_(lang::stop_turn_order), _(lang::question_clear_turn_order)); + dialog->add_response("stop", _(lang::stop)); + dialog->set_response_appearance("stop", Adwaita::ResponseAppearance::Suggested); + dialog->add_response("clear", _(lang::stop_and_clear)); + dialog->set_response_appearance("clear", Adwaita::ResponseAppearance::Destructive); + dialog->add_response("cancel", _(lang::cancel)); + dialog->set_response_appearance("cancel", Adwaita::ResponseAppearance::Default); + dialog->set_close_response("cancel"); + dialog->set_default_response("cancel"); + dialog->choose(*this, nullptr, [dialog = std::move(dialog), this](auto const & result) { + auto response = dialog->choose_finish(result); + if (response == "cancel") + { + return; + } + if (response == "clear") + { + m_turn_order->clear(); + } + m_turn_order->stop(); + }); + } +} // namespace turns::ui
\ No newline at end of file diff --git a/gui/ui/src/tracker/event_handlers.cpp b/gui/ui/src/tracker/event_handlers.cpp new file mode 100644 index 0000000..78eb82b --- /dev/null +++ b/gui/ui/src/tracker/event_handlers.cpp @@ -0,0 +1,105 @@ +#include "turns/lang/messages.hpp" +#include "turns/ui/tracker.hpp" + +#include <sigc++/functors/mem_fun.h> + +#include <glibmm/i18n.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> + +#include <giomm/asyncresult.h> +#include <giomm/error.h> + +#include <gtkmm/filedialog.h> + +#include <nlohmann/json.hpp> + +#include <cstddef> +#include <exception> +#include <format> +#include <string_view> + +namespace turns::ui +{ + + auto Tracker::on_load_content_done(Glib::RefPtr<Gio::AsyncResult> result) -> void + { + set_sensitive(); + char * data{}; + auto size = std::size_t{}; + + try + { + if (!m_file->load_contents_finish(result, data, size, m_file_etag)) + { + m_file.reset(); + m_file_etag.clear(); + return; + } + + m_turn_order->load(nlohmann::json::parse(std::string_view{data, size})); + } + catch (std::exception const & e) + { + return show_error(e); + } + + auto name = m_file->get_basename(); + show_toast(std::vformat(_(lang::successfully_opened_format), std::make_format_args(name))); + set_title(std::format("{} - {}", _(lang::turns), name)); + } + + auto Tracker::on_replace_content_done(Glib::RefPtr<Gio::AsyncResult> result) -> void + { + set_sensitive(); + + try + { + m_file->replace_contents_finish(result, m_file_etag); + } + catch (Gio::Error const & e) + { + return show_error(e); + } + + auto name = m_file->get_basename(); + show_toast(std::vformat(_(lang::successfully_saved_format), std::make_format_args(name))); + set_title(std::format("{} - {}", _(lang::turns), name)); + } + + auto Tracker::on_open_response(Glib::RefPtr<Gio::AsyncResult> result, Glib::RefPtr<Gtk::FileDialog> dialog) -> void + { + try + { + m_file = dialog->open_finish(result); + } + catch (std::exception const & e) + { + return show_error(e); + } + + m_file->load_contents_async(sigc::mem_fun(*this, &Tracker::on_load_content_done)); + set_sensitive(false); + } + + auto Tracker::on_save_response(Glib::RefPtr<Gio::AsyncResult> result, Glib::RefPtr<Gtk::FileDialog> dialog) -> void + { + try + { + m_file = dialog->save_finish(result); + } + catch (std::exception const & e) + { + show_error(e); + } + + start_replace_content(); + set_sensitive(false); + } + + auto Tracker::on_settings_changed(Glib::ustring) -> void + { + update_colors(); + } + +} // namespace turns::ui
\ No newline at end of file diff --git a/gui/ui/src/turn_order_view.cpp b/gui/ui/src/turn_order_view.cpp new file mode 100644 index 0000000..08fdf5d --- /dev/null +++ b/gui/ui/src/turn_order_view.cpp @@ -0,0 +1,64 @@ +#include "turns/ui/turn_order_view.hpp" + +#include "turns/core/participant.hpp" +#include "turns/ui/participant_row.hpp" +#include "turns/ui/template_widget.hpp" + +#include <sigc++/functors/mem_fun.h> + +#include <glibmm/binding.h> +#include <glibmm/object.h> +#include <glibmm/objectbase.h> +#include <glibmm/refptr.h> + +#include <gtkmm/enums.h> +#include <gtkmm/listbox.h> +#include <gtkmm/object.h> +#include <gtkmm/progressbar.h> +#include <gtkmm/widget.h> + +#include <memory> + +namespace turns::ui +{ + namespace + { + auto constexpr static TYPE_NAME = "TurnOrderView"; + auto constexpr static TEMPLATE = "/ch/arknet/Turns/turn_order_view.ui"; + } // namespace + + TurnOrderView::TurnOrderView(Glib::RefPtr<model_type> model) + : Glib::ObjectBase(TYPE_NAME) + , template_widget{TEMPLATE} + , m_model{model} + , m_progress{get_widget<Gtk::ProgressBar>("progress")} + , m_view{get_widget<Gtk::ListBox>("view")} + { + if (!model) + { + return; + } + + set_orientation(Gtk::Orientation::VERTICAL); + + m_view->bind_model(m_model, sigc::mem_fun(*this, &TurnOrderView::handle_create_row)); + Glib::Binding::bind_property(m_model->progress(), m_progress->property_fraction(), Glib::Binding::Flags::SYNC_CREATE); + } + + auto TurnOrderView::handle_create_row(Glib::RefPtr<Glib::Object> const item) -> Gtk::Widget * + { + auto participant = std::dynamic_pointer_cast<core::Participant>(item); + auto row = Gtk::make_managed<ParticipantRow>(participant); + + Glib::Binding::bind_property(m_model->is_running(), + row->delete_enabled(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + + Glib::Binding::bind_property(m_model->is_running(), + row->edit_enabled(), + Glib::Binding::Flags::SYNC_CREATE | Glib::Binding::Flags::INVERT_BOOLEAN); + + return row; + } + +} // namespace turns::ui::widgets
\ No newline at end of file diff --git a/gui/ui/src/turn_order_view.ui b/gui/ui/src/turn_order_view.ui new file mode 100644 index 0000000..28a4bd0 --- /dev/null +++ b/gui/ui/src/turn_order_view.ui @@ -0,0 +1,38 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Created with Cambalache 0.96.0 --> +<interface> + <!-- interface-name turn_order_view.ui --> + <requires lib="gtk" version="4.18"/> + <requires lib="libadwaita" version="1.7"/> + <template class="gtkmm__CustomObject_TurnOrderView" parent="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkProgressBar" id="progress"> + <style> + <class name="osd"/> + </style> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="child"> + <object class="AdwClamp"> + <property name="child"> + <object class="GtkListBox" id="view"> + <property name="valign">start</property> + <style> + <class name="boxed-list"/> + </style> + </object> + </property> + <property name="margin-bottom">18</property> + <property name="margin-end">12</property> + <property name="margin-start">12</property> + <property name="margin-top">12</property> + </object> + </property> + <property name="vexpand">True</property> + </object> + </child> + </template> +</interface> diff --git a/gui/ui/tests/gtk_test_init.cpp b/gui/ui/tests/gtk_test_init.cpp new file mode 100644 index 0000000..bfa885f --- /dev/null +++ b/gui/ui/tests/gtk_test_init.cpp @@ -0,0 +1,41 @@ +#include "turns/core/init.hpp" +#include "turns/ui/init.hpp" + +#include <catch2/reporters/catch_reporter_event_listener.hpp> +#include <catch2/reporters/catch_reporter_registrars.hpp> + +#include <glibmm/i18n.h> + +#include <gtkmm/init.h> + +#include <adwaitamm/application.hpp> +#include <adwaitamm/wrap_init.hpp> + +#include <libintl.h> + +#include <clocale> + +namespace turns::ui::tests +{ + + struct gtk_test_init : Catch::EventListenerBase + { + using Catch::EventListenerBase::EventListenerBase; + + auto testRunStarting(Catch::TestRunInfo const &) -> void override + { + setlocale(LC_ALL, ""); + bindtextdomain("turns", TESTLOCALEDIR); + bind_textdomain_codeset("turns", "UTF-8"); + textdomain("turns"); + + [[maybe_unused]] auto app = Adwaita::Application::create("ch.arknet.turns.tests.ui"); + + core::register_types(); + ui::register_types(); + } + }; + + CATCH_REGISTER_LISTENER(gtk_test_init); + +} // namespace turns::ui::tests
\ No newline at end of file diff --git a/gui/ui/tests/participant_editor.cpp b/gui/ui/tests/participant_editor.cpp new file mode 100644 index 0000000..fee0313 --- /dev/null +++ b/gui/ui/tests/participant_editor.cpp @@ -0,0 +1,150 @@ +#include "turns/ui/participant_editor.hpp" + +#include "turns/core/disposition.hpp" +#include "turns/core/participant.hpp" +#include "turns/lang/messages.hpp" + +#include <catch2/catch_test_macros.hpp> +#include <catch2/generators/catch_generators.hpp> + +#include <glibmm/i18n.h> +#include <glibmm/ustring.h> +#include <glibmm/wrap.h> + +#include <gtkmm/builder.h> +#include <gtkmm/listboxrow.h> +#include <gtkmm/window.h> + +#include <adwaitamm/entryrow.hpp> +#include <adwaitamm/spinrow.hpp> + +#include <gtk/gtk.h> + +#include <clocale> +#include <memory> + +namespace turns::ui::tests +{ + + TEST_CASE("A freshly constructed participant editor without a participant", "[windows]") + { + auto locale = GENERATE("en_US.UTF-8", "de_CH.UTF-8"); + setlocale(LC_ALL, locale); + + auto participant = core::Participant::create("Frederick Thumblewackle", 7.2, core::Disposition::Friendly); + auto instance = std::make_shared<ParticipantEditor>(nullptr); + auto window = Gtk::Window{}; + + SECTION("was successfully constructed") + { + REQUIRE(instance); + } + + SECTION("has a non-empty title") + { + REQUIRE_FALSE(instance->get_title().empty()); + } + + SECTION("has its title set according to the active language") + { + REQUIRE(instance->get_title() == _(lang::add_participant)); + } + + SECTION("has an empty name") + { + REQUIRE(instance->get_name().empty()); + } + + SECTION("has a zero priority") + { + REQUIRE(instance->get_priority() == 0); + } + + SECTION("has neutral disposition") + { + REQUIRE(instance->get_disposition() == core::Disposition::Neutral); + } + + SECTION("has a null participant") + { + REQUIRE_FALSE(instance->get_participant()); + } + + WHEN("setting a new participant") + { + instance->set_participant(participant); + + THEN("getting the participant returns the new one") + { + REQUIRE(instance->get_participant() == participant); + } + + THEN("changes to the name propagate to the participant") + { + CHECK(participant->get_name() != "REPLACED"); + instance->set_name("REPLACED"); + REQUIRE(participant->get_name() == "REPLACED"); + } + + THEN("changes to the priority propagate to the participant") + { + CHECK(participant->get_priority() != 0); + instance->set_priority(0); + REQUIRE(participant->get_priority() == 0); + } + + THEN("changes to the disposition propagate to the participant") + { + CHECK(participant->get_disposition() != core::Disposition::Secret); + instance->set_disposition(core::Disposition::Secret); + REQUIRE(participant->get_disposition() == core::Disposition::Secret); + } + } + + SECTION("allows binding to the finished signal") + { + REQUIRE((instance->signal_finished().connect([](auto, auto, auto) {})).connected()); + } + } + + TEST_CASE("A freshly constructed participant editor with a participant", "[windows]") + { + auto locale = GENERATE("en_US.UTF-8", "de_CH.UTF-8"); + setlocale(LC_ALL, locale); + + auto participant = core::Participant::create("Qibi Babblebranch", 12, core::Disposition::Neutral); + auto instance = std::make_shared<ParticipantEditor>(participant); + auto window = Gtk::Window{}; + + SECTION("was successfully constructed") + { + REQUIRE(instance); + } + + SECTION("has a non-empty title") + { + REQUIRE_FALSE(instance->get_title().empty()); + } + + SECTION("has its title set according to the active language") + { + REQUIRE(instance->get_title() == _(lang::edit_participant)); + } + + SECTION("has its name field set according to its participant") + { + REQUIRE(instance->get_name() == participant->property_name().get_value()); + } + + SECTION("has its priority field set according to its participant") + { + REQUIRE(instance->get_priority() == participant->property_priority()); + } + + SECTION("allows binding to the finished signal") + { + REQUIRE((instance->signal_finished().connect([](auto, auto, auto) {})).connected()); + } + } + +} // namespace turns::ui::tests
\ No newline at end of file diff --git a/gui/ui/tests/participant_row.cpp b/gui/ui/tests/participant_row.cpp new file mode 100644 index 0000000..20eaa5c --- /dev/null +++ b/gui/ui/tests/participant_row.cpp @@ -0,0 +1,30 @@ +#include "turns/ui/participant_row.hpp" + +#include "turns/core/disposition.hpp" +#include "turns/core/participant.hpp" + +#include <catch2/catch_test_macros.hpp> + +#include <glibmm/refptr.h> + +#include <gtkmm/object.h> + +#include <memory> + +namespace turns::ui::tests +{ + + TEST_CASE("A freshly constructed participant row") + { + SECTION("can be created without a participant") + { + REQUIRE(std::make_shared<ParticipantRow>(Glib::RefPtr<core::Participant>{})); + } + + SECTION("can be created with a participant") + { + REQUIRE(std::make_shared<ParticipantRow>(core::Participant::create("Tazmyla Fireforge", 13, core::Disposition::Secret))); + } + } + +} // namespace turns::ui::widgets::tests
\ No newline at end of file diff --git a/gui/ui/tests/resources.cpp b/gui/ui/tests/resources.cpp new file mode 100644 index 0000000..a091266 --- /dev/null +++ b/gui/ui/tests/resources.cpp @@ -0,0 +1,21 @@ +#include <catch2/catch_test_macros.hpp> +#include <catch2/generators/catch_generators.hpp> + +#include <giomm/resource.h> + +#include <format> + +TEST_CASE("UI resources") +{ + + auto file = GENERATE("/ch/arknet/Turns/participant_editor.ui", + "/ch/arknet/Turns/participant_row.ui", + "/ch/arknet/Turns/preferences.ui", + "/ch/arknet/Turns/tracker.ui", + "/ch/arknet/Turns/turn_order_view.ui"); + + SECTION(std::format("contains {}", file)) + { + REQUIRE(Gio::Resource::get_file_exists_global_nothrow(file)); + } +} diff --git a/gui/ui/tests/tracker.cpp b/gui/ui/tests/tracker.cpp new file mode 100644 index 0000000..0d5e983 --- /dev/null +++ b/gui/ui/tests/tracker.cpp @@ -0,0 +1,79 @@ +#include "turns/ui/tracker.hpp" + +#include "turns/core/settings.hpp" +#include "turns/lang/messages.hpp" + +#include <catch2/catch_test_macros.hpp> +#include <catch2/generators/catch_generators.hpp> + +#include <glibmm/i18n.h> +#include <glibmm/wrap.h> + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/widget.h> + +#include <adwaitamm/application.hpp> +#include <adwaitamm/windowtitle.hpp> + +#include <clocale> +#include <memory> + +namespace turns::ui::tests +{ + + TEST_CASE("A freshly constructed tracker window", "[windows]") + { + auto locale = GENERATE("en_US.UTF-8", "de_CH.UTF-8"); + setlocale(LC_ALL, locale); + + auto app = Adwaita::Application::create("ch.arknet.Turns.test"); + auto instance = std::make_shared<Tracker>(app, core::get_settings()); + + SECTION("was successfully constructed") + { + REQUIRE(instance); + } + + // SECTION("has a non-empty subtitle") + // { + // auto widget = instance->get_ builder->get_widget<Adwaita::WindowTitle>("title"); + // REQUIRE_FALSE(widget->get_subtitle().empty()); + // } + + // SECTION("has its subtitle set according to the active language") + // { + // auto widget = builder->get_widget<Adwaita::WindowTitle>("title"); + // REQUIRE(widget->get_subtitle() == _(lang::no_active_turn_order)); + // } + + // SECTION("has a non-empty title") + // { + // auto widget = builder->get_widget<Adwaita::WindowTitle>("title"); + // REQUIRE_FALSE(widget->get_title().empty()); + // } + + // SECTION("has its title set according to the active language") + // { + // auto widget = builder->get_widget<Adwaita::WindowTitle>("title"); + // REQUIRE(widget->get_title() == _(lang::turns)); + // } + + // SECTION("has its add_participant button's tooltip set according to the active language") + // { + // auto widget = builder->get_widget<Gtk::Button>("add_participant"); + // REQUIRE(widget->get_tooltip_text() == _(lang::add_participant)); + // } + + // SECTION("as its open_main_menu button's tooltip set according to the active language") + // { + // auto widget = builder->get_widget<Gtk::MenuButton>("open_main_menu"); + // REQUIRE(widget->get_tooltip_text() == _(lang::main_menu)); + // } + + // instance->destroy(); + // delete instance; + } + +} // namespace turns::ui::tests
\ No newline at end of file diff --git a/gui/ui/ui.cmb b/gui/ui/ui.cmb new file mode 100644 index 0000000..b246448 --- /dev/null +++ b/gui/ui/ui.cmb @@ -0,0 +1,10 @@ +<?xml version='1.0' encoding='UTF-8' standalone='no'?> +<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd"> +<!-- Created with Cambalache 0.96.0 --> +<cambalache-project version="0.96.0" target_tk="gtk-4.0"> + <ui template-class="gtkmm__CustomObject_TurnOrderView" filename="src/tracker.ui" sha256="5f89d446490f94ec306b6c6e78769daab156f01acf66694a9f2fd4b0b713858b"/> + <ui template-class="gtkmm__CustomObject_ParticipantEditor" filename="src/participant_editor.ui" sha256="175e5445abfe35525885929739b998e9b5d5379bc01dfbef798c52ef8870cf96"/> + <ui template-class="gtkmm__CustomObject_ParticipantRow" filename="src/participant_row.ui" sha256="ab4db80068f811a2b77608fca128ba72c3e753ff33748822afd7a0f74c955dcd"/> + <ui template-class="gtkmm__CustomObject_TurnOrderView" filename="src/turn_order_view.ui" sha256="1a71db6bcf70d48123f1bd876b344f64f3e3d0c7f9fe12c6daefb326763cbef7"/> + <ui template-class="gtkmm__CustomObject_Preferences" filename="src/preferences.ui" sha256="3c47beaa2297fa45f8c29ac7aa410227b7f9d43971d6b6c31fa4278f2bb43f6a"/> +</cambalache-project> |
