#include "turns/ui/windows/tracker.hpp" #include "turns/core/participant.hpp" #include "turns/core/turn_order.hpp" #include "turns/lang/messages.hpp" #include "turns/ui/windows/participant_editor.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace turns::ui::windows { namespace { auto editor_for(Glib::RefPtr participant) { auto builder = Gtk::Builder::create_from_resource("/windows/participant_editor.ui"); return std::pair{builder, Gtk::Builder::get_widget_derived(builder, "participant_editor", participant)}; } auto stop_dialog_callback(AdwAlertDialog * dialog, GAsyncResult * result, core::turn_order * order) { auto response = Glib::ustring{adw_alert_dialog_choose_finish(dialog, result)}; if (response == "cancel") { return; } if (response == "clear") { order->clear(); } order->stop(); } } // namespace tracker::tracker(BaseObjectType * base, Glib::RefPtr const builder) : Gtk::ApplicationWindow{base} , m_adw{ADW_APPLICATION_WINDOW(gobj())} , m_controls{builder->get_widget("controls")} , m_empty(builder->get_widget("empty")) , m_overlay{ADW_TOAST_OVERLAY(builder->get_widget("overlay")->gobj())} , m_stack{builder->get_widget("stack")} , m_start{builder->get_widget("start")} , m_title(ADW_WINDOW_TITLE(builder->get_widget("title")->gobj())) , m_turn_order{core::turn_order::create()} , m_turn_order_view{Gtk::make_managed(m_turn_order)} , m_subtitle{Glib::wrap(GTK_WIDGET(m_title)), "subtitle"} { 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 } auto tracker::do_save() -> void { m_buffer = m_turn_order->serialize().dump(2); m_file->replace_contents_async(sigc::mem_fun(*this, &tracker::handle_save_done), m_buffer, m_file_tag); } auto tracker::handle_add_participant() -> void { auto [lifeline, dialog] = editor_for(nullptr); dialog->present(this); dialog->signal_finished().connect([this](auto n, auto p, auto d) { m_turn_order->add(n, p, d); }); } auto tracker::handle_delete_participant(Glib::VariantBase param) -> void { auto index = Glib::VariantBase::cast_dynamic>(param); m_turn_order->remove(index.get()); } auto tracker::handle_edit_participant(Glib::VariantBase param) -> void { static_cast(param); auto index = Glib::VariantBase::cast_dynamic>(param); auto participant = m_turn_order->get_typed_object(index.get()); auto [lifeline, dialog] = editor_for(participant); dialog->present(this); } auto tracker::handle_open() -> void { auto filters = Gio::ListStore::create(); auto filter = Gtk::FileFilter::create(); filter->set_name(_("Turns Files")); filter->add_pattern("*.trns"); filters->append(filter); auto dialog = Gtk::FileDialog::create(); dialog->set_filters(filters); dialog->open(sigc::bind(sigc::mem_fun(*this, &tracker::handle_open_response), dialog)); } auto tracker::handle_open_done(Glib::RefPtr result) -> void try { set_sensitive(); char * data{}; auto size = std::size_t{}; if (!m_file->load_contents_finish(result, data, size, m_file_tag)) { m_file.reset(); m_file_tag.clear(); return; } auto deserialized = nlohmann::json::parse(std::string_view{data, size}); m_turn_order->load(deserialized); auto name = m_file->get_basename(); auto message = std::vformat(_(lang::successfully_opened_format), std::make_format_args(name)); auto toast = adw_toast_new(message.c_str()); adw_toast_overlay_add_toast(m_overlay, toast); set_title(std::format("{} - {}", _(lang::turns), name)); } catch (std::exception const & e) { show_error_toast(e); } auto tracker::handle_open_response(Glib::RefPtr result, Glib::RefPtr dialog) -> void try { m_file = dialog->open_finish(result); m_file->load_contents_async(sigc::mem_fun(*this, &tracker::handle_open_done)); set_sensitive(false); } catch (std::exception const & e) { set_sensitive(); show_error_toast(e); } auto tracker::handle_save(bool force_ask) -> void { if (m_file && !force_ask) { do_save(); } else { auto filters = Gio::ListStore::create(); auto filter = Gtk::FileFilter::create(); filter->set_name(_("Turns Files")); filter->add_pattern("*.trns"); filters->append(filter); auto dialog = Gtk::FileDialog::create(); if (m_file) { dialog->set_initial_file(m_file); } else { dialog->set_initial_name(_(lang::new_turn_order_file_name)); } dialog->set_filters(filters); dialog->save(*this, sigc::bind(sigc::mem_fun(*this, &tracker::handle_save_response), dialog)); } } auto tracker::handle_save_done(Glib::RefPtr result) -> void try { set_sensitive(); m_file->replace_contents_finish(result, m_file_tag); auto name = m_file->get_basename(); auto message = std::vformat(_(lang::successfully_saved_format), std::make_format_args(name)); auto toast = adw_toast_new(message.c_str()); adw_toast_overlay_add_toast(m_overlay, toast); set_title(std::format("{} - {}", _(lang::turns), name)); } catch (Gio::Error const & e) { show_error_toast(e); } auto tracker::handle_save_response(Glib::RefPtr result, Glib::RefPtr dialog) -> void try { m_file = dialog->save_finish(result); do_save(); set_sensitive(false); } catch (Gtk::DialogError const & e) { if (e.code() == Gtk::DialogError::FAILED) { show_error_toast(e); } } auto tracker::handle_stop() -> void { auto dialog = ADW_ALERT_DIALOG(adw_alert_dialog_new(_(lang::stop_turn_order), _(lang::question_clear_turn_order))); adw_alert_dialog_add_response(dialog, "stop", _(lang::stop)); adw_alert_dialog_set_response_appearance(dialog, "stop", ADW_RESPONSE_SUGGESTED); adw_alert_dialog_add_response(dialog, "clear", _(lang::stop_and_clear)); adw_alert_dialog_set_response_appearance(dialog, "clear", ADW_RESPONSE_DESTRUCTIVE); adw_alert_dialog_add_response(dialog, "cancel", _(lang::cancel)); adw_alert_dialog_set_response_appearance(dialog, "cancel", ADW_RESPONSE_DEFAULT); adw_alert_dialog_set_close_response(dialog, "cancel"); adw_alert_dialog_set_default_response(dialog, "cancel"); adw_alert_dialog_choose(dialog, GTK_WIDGET(this->gobj()), NULL, reinterpret_cast(stop_dialog_callback), m_turn_order.get()); } 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::handle_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::turn_order::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::turn_order::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::turn_order::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::turn_order::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::handle_stop)); Glib::Binding::bind_property(m_turn_order->is_running(), action->property_enabled(), Glib::Binding::Flags::SYNC_CREATE); } // win.delete // win.edit // win.open { add_action_with_parameter("delete", Glib::VARIANT_TYPE_INT32, sigc::mem_fun(*this, &tracker::handle_delete_participant)); add_action_with_parameter("edit", Glib::VARIANT_TYPE_INT32, sigc::mem_fun(*this, &tracker::handle_edit_participant)); add_action("open", sigc::mem_fun(*this, &tracker::handle_open)); } // win.save // depends-on: turn_order:is_empty == false { auto action = add_action("save", sigc::bind(sigc::mem_fun(*this, &tracker::handle_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::handle_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::show_error_toast(std::exception const & e) -> void { auto error = e.what(); auto message = std::vformat(_(lang::saving_failed_format), std::make_format_args(error)); auto toast = adw_toast_new(message.c_str()); adw_toast_overlay_add_toast(m_overlay, toast); } 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)); } } } // namespace turns::ui::windows