summaryrefslogtreecommitdiff
path: root/src/gui_gtk.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui_gtk.cc')
-rw-r--r--src/gui_gtk.cc1610
1 files changed, 1610 insertions, 0 deletions
diff --git a/src/gui_gtk.cc b/src/gui_gtk.cc
new file mode 100644
index 0000000..99172e3
--- /dev/null
+++ b/src/gui_gtk.cc
@@ -0,0 +1,1610 @@
+// -*- mode: c++; c-basic-offset: 2; -*-
+
+#include "common.hh"
+
+#include <functional>
+#include <gtk/gtk.h>
+#include <math.h>
+#include <memory>
+#include <unordered_map>
+#include <vector>
+
+#include "gui_about.hh"
+#include "gui_form.hh"
+#include "gui_formapply.hh"
+#include "gui_listmodel.hh"
+#include "gui_main.hh"
+#include "gui_menu.hh"
+#include "gui_statusbar.hh"
+#include "looper.hh"
+#include "observers.hh"
+
+namespace std {
+template<>
+struct hash<AttributedText::Attribute> {
+ size_t operator()(AttributedText::Attribute const& attr) const {
+ return attr.hash();
+ }
+};
+} // namespace std
+
+namespace {
+
+template<typename T>
+class shared_gobject {
+public:
+ shared_gobject()
+ : ptr_(nullptr) {
+ }
+ shared_gobject(std::nullptr_t)
+ : ptr_(nullptr) {
+ }
+ explicit shared_gobject(T* ptr)
+ : ptr_(ptr) {
+ }
+ shared_gobject(shared_gobject const& obj)
+ : ptr_(obj.ptr_) {
+ if (ptr_) g_object_ref(ptr_);
+ }
+ shared_gobject(shared_gobject&& obj)
+ : ptr_(obj.ptr_) {
+ obj.ptr = nullptr;
+ }
+
+ ~shared_gobject() {
+ reset();
+ }
+
+ shared_gobject& operator=(shared_gobject const& obj) {
+ reset(obj.ptr_);
+ if (ptr_) g_object_ref(ptr_);
+ return *this;
+ }
+ shared_gobject& operator=(shared_gobject&& obj) {
+ ptr_ = obj.ptr_;
+ obj.ptr_ = nullptr;
+ return *this;
+ }
+
+ void swap(shared_gobject& obj) {
+ auto x = ptr_;
+ ptr_ = obj.ptr_;
+ obj.ptr_ = x;
+ }
+
+ void reset() {
+ if (ptr_) {
+ g_object_unref(ptr_);
+ ptr_ = nullptr;
+ }
+ }
+
+ void reset(T* ptr) {
+ if (ptr_) g_object_unref(ptr_);
+ ptr_ = ptr;
+ }
+
+ T* get() const {
+ return ptr_;
+ }
+
+ T& operator*() const {
+ return *ptr_;
+ }
+
+ T* operator->() const {
+ return ptr_;
+ }
+
+ explicit operator bool() const {
+ return ptr_ != nullptr;
+ }
+
+private:
+ T* ptr_;
+};
+
+#define MAIN_APP_TYPE main_app_get_type()
+
+G_DECLARE_FINAL_TYPE(MainApp, main_app, MAIN, APP, GtkApplication)
+
+#define MAIN_APP_WINDOW_TYPE main_app_window_get_type()
+G_DECLARE_FINAL_TYPE(MainAppWindow, main_app_window, MAIN, APP_WINDOW,
+ GtkApplicationWindow)
+
+class GtkGuiWindow : public GuiWindow {
+public:
+ GtkWindow* window() const {
+ return reinterpret_cast<GtkWindow*>(impl());
+ }
+
+ void set_title(std::string const& title) override;
+};
+
+typedef struct _ListModel ListModel;
+
+class ListModelListener : public virtual GuiListModel::Listener {
+public:
+ ListModelListener(ListModel* model)
+ : model_(model) {
+ }
+
+ void rows_added(GuiListModel* model, size_t first, size_t last) override;
+ void rows_changed(GuiListModel* model, size_t first, size_t last) override;
+ void rows_removed(GuiListModel* model, size_t first, size_t last) override;
+
+private:
+ ListModel* model_;
+};
+
+struct _ListModel {
+ GObject parent_;
+ GuiListModel* model_;
+ gint iter_stamp_;
+
+ ListModelListener* listener_;
+};
+
+typedef struct _ListModelClass {
+ GObjectClass parent_class_;
+} ListModelClass;
+
+GType list_model_get_type();
+
+#define LIST_MODEL_TYPE (list_model_get_type())
+#define LIST_MODEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LIST_MODEL_TYPE, ListModel))
+#define IS_LIST_MODEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), LIST_MODEL_TYPE))
+
+void list_model_init(ListModel* model) {
+ model->model_ = nullptr;
+ model->iter_stamp_ = g_random_int();
+}
+
+void list_model_finalize(GObject* obj) {
+ auto model = LIST_MODEL(obj);
+ model->model_->remove_listener(model->listener_);
+ delete model->listener_;
+ auto clazz = g_type_class_ref(LIST_MODEL_TYPE);
+ reinterpret_cast<GObjectClass*>(g_type_class_peek_parent(clazz))->finalize(obj);
+ g_type_class_unref(clazz);
+}
+
+void list_model_class_init(ListModelClass* clazz) {
+ clazz->parent_class_.finalize = list_model_finalize;
+}
+
+GtkTreeModelFlags list_model_get_flags(GtkTreeModel* tree_model) {
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), static_cast<GtkTreeModelFlags>(0));
+ return GTK_TREE_MODEL_LIST_ONLY;
+}
+
+gint list_model_get_n_columns(GtkTreeModel* tree_model) {
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), 0);
+ return LIST_MODEL(tree_model)->model_->columns();
+}
+
+GType list_model_get_column_type(GtkTreeModel* tree_model, gint index) {
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), G_TYPE_INVALID);
+ auto model = LIST_MODEL(tree_model);
+ g_return_val_if_fail(index >= 0 && static_cast<size_t>(index) < model->model_->columns(), G_TYPE_INVALID);
+ return G_TYPE_STRING;
+}
+
+gboolean list_model_get_iter(GtkTreeModel* tree_model, GtkTreeIter* iter, GtkTreePath* path) {
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), false);
+ auto model = LIST_MODEL(tree_model);
+ g_return_val_if_fail(gtk_tree_path_get_depth(path) == 1, false);
+ auto row = gtk_tree_path_get_indices(path)[0];
+ g_return_val_if_fail(row >= 0, false);
+ if (static_cast<size_t>(row) >= model->model_->rows()) return false;
+
+ g_return_val_if_fail(iter, false);
+ iter->stamp = model->iter_stamp_;
+ iter->user_data = reinterpret_cast<gpointer>(row);
+ iter->user_data2 = nullptr;
+ iter->user_data3 = nullptr;
+ return true;
+}
+
+GtkTreePath* list_model_get_path(GtkTreeModel* tree_model, GtkTreeIter* iter) {
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), nullptr);
+ auto model = LIST_MODEL(tree_model);
+ g_return_val_if_fail(iter, nullptr);
+ g_return_val_if_fail(iter->stamp == model->iter_stamp_, nullptr);
+ auto path = gtk_tree_path_new();
+ gtk_tree_path_append_index(path, reinterpret_cast<size_t>(iter->user_data));
+ return path;
+}
+
+void list_model_get_value(GtkTreeModel* tree_model, GtkTreeIter* iter, gint column, GValue* value) {
+ g_return_if_fail(IS_LIST_MODEL(tree_model));
+ auto model = LIST_MODEL(tree_model);
+ g_return_if_fail(column >= 0 && static_cast<size_t>(column) < model->model_->columns());
+ g_return_if_fail(iter);
+ g_return_if_fail(iter->stamp == model->iter_stamp_);
+ g_value_init(value, G_TYPE_STRING);
+ auto row = reinterpret_cast<size_t>(iter->user_data);
+ g_return_if_fail(row < model->model_->rows());
+ auto str = model->model_->data(row, column);
+ g_value_set_string(value, str.c_str());
+}
+
+gboolean list_model_iter_next(GtkTreeModel* tree_model, GtkTreeIter* iter) {
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), false);
+ auto model = LIST_MODEL(tree_model);
+ g_return_val_if_fail(iter, false);
+ g_return_val_if_fail(iter->stamp == model->iter_stamp_, false);
+ auto row = reinterpret_cast<size_t>(iter->user_data) + 1;
+ if (row >= model->model_->rows()) return false;
+ iter->user_data = reinterpret_cast<gpointer>(row);
+ return true;
+}
+
+gboolean list_model_iter_previous(GtkTreeModel* tree_model, GtkTreeIter* iter) {
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), false);
+ auto model = LIST_MODEL(tree_model);
+ g_return_val_if_fail(iter, false);
+ g_return_val_if_fail(iter->stamp == model->iter_stamp_, false);
+ auto row = reinterpret_cast<size_t>(iter->user_data);
+ if (row == 0) return false;
+ iter->user_data = reinterpret_cast<gpointer>(row - 1);
+ return true;
+}
+
+gboolean list_model_iter_children(GtkTreeModel* tree_model, GtkTreeIter* iter, GtkTreeIter* parent) {
+ if (parent) return false;
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), false);
+ auto model = LIST_MODEL(tree_model);
+ if (model->model_->rows() == 0) return false;
+ g_return_val_if_fail(iter, false);
+ iter->stamp = model->iter_stamp_;
+ iter->user_data = static_cast<gpointer>(0);
+ iter->user_data2 = nullptr;
+ iter->user_data3 = nullptr;
+ return true;
+}
+
+gboolean list_model_iter_has_child(GtkTreeModel*, GtkTreeIter*) {
+ return false;
+}
+
+gint list_model_iter_n_children(GtkTreeModel* tree_model, GtkTreeIter* iter) {
+ if (iter) return 0;
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), false);
+ auto model = LIST_MODEL(tree_model);
+ return model->model_->rows();
+}
+
+gint list_model_iter_nth_child(GtkTreeModel* tree_model, GtkTreeIter* iter, GtkTreeIter* parent, gint n) {
+ if (parent) return 0;
+ g_return_val_if_fail(IS_LIST_MODEL(tree_model), false);
+ auto model = LIST_MODEL(tree_model);
+ g_return_val_if_fail(n >= 0 && static_cast<size_t>(n) < model->model_->rows(), false);
+ g_return_val_if_fail(iter, false);
+ iter->stamp = model->iter_stamp_;
+ iter->user_data = reinterpret_cast<gpointer>(n);
+ iter->user_data2 = nullptr;
+ iter->user_data3 = nullptr;
+ return true;
+}
+
+gboolean list_model_iter_parent(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*) {
+ return false;
+}
+
+void list_tree_model_init(GtkTreeModelIface* iface) {
+ iface->get_flags = list_model_get_flags;
+ iface->get_n_columns = list_model_get_n_columns;
+ iface->get_column_type = list_model_get_column_type;
+ iface->get_iter = list_model_get_iter;
+ iface->get_path = list_model_get_path;
+ iface->get_value = list_model_get_value;
+ iface->iter_next = list_model_iter_next;
+ iface->iter_previous = list_model_iter_previous;
+ iface->iter_children = list_model_iter_children;
+ iface->iter_has_child = list_model_iter_has_child;
+ iface->iter_n_children = list_model_iter_n_children;
+ iface->iter_nth_child = list_model_iter_nth_child;
+ iface->iter_parent = list_model_iter_parent;
+}
+
+GType list_model_get_type() {
+ static GType type;
+
+ if (!type) {
+ static const GTypeInfo list_model_type_info = {
+ sizeof(ListModelClass),
+ nullptr, // base_init
+ nullptr, // base_finalize
+ reinterpret_cast<GClassInitFunc>(list_model_class_init),
+ nullptr, // class_finalize
+ nullptr, // class_data
+ sizeof(ListModel),
+ 0, // n_preallocs
+ reinterpret_cast<GInstanceInitFunc>(list_model_init)
+ };
+ static const GInterfaceInfo tree_model_info = {
+ reinterpret_cast<GInterfaceInitFunc>(list_tree_model_init),
+ nullptr,
+ nullptr
+ };
+
+ type = g_type_register_static(G_TYPE_OBJECT, "ListModel", &list_model_type_info, static_cast<GTypeFlags>(0));
+ g_type_add_interface_static(type, GTK_TYPE_TREE_MODEL, &tree_model_info);
+ }
+
+ return type;
+}
+
+ListModel* list_model_new(GuiListModel* model) {
+ auto list = reinterpret_cast<ListModel*>(g_object_new(LIST_MODEL_TYPE, nullptr));
+ list->model_ = model;
+ list->listener_ = new ListModelListener(list);
+ model->add_listener(list->listener_);
+ return list;
+}
+
+GuiListModel* list_model_get_model(ListModel const* model) {
+ return model->model_;
+}
+
+void ListModelListener::rows_added(GuiListModel*, size_t first, size_t last) {
+ GtkTreeIter iter;
+ auto path = gtk_tree_path_new();
+
+ auto tree_model = GTK_TREE_MODEL(model_);
+ for (size_t row = first; row <= last; ++row) {
+ gtk_tree_path_append_index(path, row);
+ list_model_get_iter(tree_model, &iter, path);
+ gtk_tree_model_row_inserted(tree_model, path, &iter);
+ gtk_tree_path_up(path);
+ }
+ gtk_tree_path_free(path);
+}
+
+void ListModelListener::rows_changed(GuiListModel*, size_t first, size_t last) {
+ GtkTreeIter iter;
+ auto path = gtk_tree_path_new();
+
+ auto tree_model = GTK_TREE_MODEL(model_);
+ for (size_t row = first; row <= last; ++row) {
+ gtk_tree_path_append_index(path, row);
+ list_model_get_iter(tree_model, &iter, path);
+ gtk_tree_model_row_changed(tree_model, path, &iter);
+ gtk_tree_path_up(path);
+ }
+ gtk_tree_path_free(path);
+}
+
+void ListModelListener::rows_removed(GuiListModel*, size_t first, size_t last) {
+ auto path = gtk_tree_path_new();
+
+ auto tree_model = GTK_TREE_MODEL(model_);
+ for (size_t row = last; row >= first; --row) {
+ gtk_tree_path_append_index(path, row);
+ gtk_tree_model_row_deleted(tree_model, path);
+ gtk_tree_path_up(path);
+ }
+ gtk_tree_path_free(path);
+}
+
+class GtkAttributedText : public AttributedText {
+public:
+ GtkAttributedText()
+ : buffer_(gtk_text_buffer_new(nullptr)) {
+ }
+
+ ~GtkAttributedText() override {
+ }
+
+ void append(const char* str, size_t len, Attribute const& attr, size_t start, size_t length) override {
+ if (len == 0 || length == 0) return;
+ if (start >= len) return;
+ auto max = len - start;
+ length = std::min(max, length);
+ GtkTextIter end;
+ gtk_text_buffer_get_end_iter(buffer_.get(), &end);
+ if (attr == EMPTY) {
+ gtk_text_buffer_insert(buffer_.get(), &end, str + start, length);
+ } else {
+ gtk_text_buffer_insert_with_tags(buffer_.get(), &end, str + start, length, get_tag(attr), nullptr);
+ }
+ }
+
+ void add(Attribute const& attr, size_t start, size_t length) override {
+ if (length == 0) return;
+ if (attr == EMPTY) return;
+ GtkTextIter begin, end;
+ init_iter(&begin, &end, start, length);
+ gtk_text_buffer_apply_tag(buffer_.get(), get_tag(attr), &begin, &end);
+ }
+ void set(Attribute const& attr, size_t start, size_t length) override {
+ if (length == 0) return;
+ GtkTextIter begin, end;
+ init_iter(&begin, &end, start, length);
+ gtk_text_buffer_remove_all_tags(buffer_.get(), &begin, &end);
+ if (attr != EMPTY) {
+ gtk_text_buffer_apply_tag(buffer_.get(), get_tag(attr), &begin, &end);
+ }
+ }
+ void clear(size_t start, size_t length) override {
+ if (length == 0) return;
+ GtkTextIter begin, end;
+ init_iter(&begin, &end, start, length);
+ gtk_text_buffer_remove_all_tags(buffer_.get(), &begin, &end);
+ }
+
+ std::string text() const override {
+ GtkTextIter begin, end;
+ gtk_text_buffer_get_start_iter(buffer_.get(), &begin);
+ gtk_text_buffer_get_end_iter(buffer_.get(), &end);
+ auto text = gtk_text_buffer_get_text(buffer_.get(), &begin, &end, true);
+ std::string ret(text);
+ g_free(text);
+ return ret;
+ }
+
+ GtkTextBuffer* buffer() const {
+ return buffer_.get();
+ }
+
+private:
+ void init_iter(GtkTextIter* begin, GtkTextIter* end,
+ size_t start, size_t length) {
+ gtk_text_buffer_get_iter_at_offset(buffer_.get(), begin, start);
+ gtk_text_buffer_get_iter_at_offset(
+ buffer_.get(), end, length == std::string::npos ? -1 : start + length);
+ }
+
+ GtkTextTag* get_tag(Attribute const& attr) {
+ auto it = tags_.find(attr);
+ if (it != tags_.end()) return it->second;
+ auto tag = gtk_text_buffer_create_tag(buffer_.get(), nullptr, nullptr);
+ GValue val = G_VALUE_INIT;
+ gchar tmp[50];
+ if (attr.has_background()) {
+ color_value(&val, tmp, sizeof(tmp), attr.background());
+ g_object_set_property(G_OBJECT(tag), "background", &val);
+ g_value_unset(&val);
+ }
+ if (attr.has_foreground()) {
+ color_value(&val, tmp, sizeof(tmp), attr.foreground());
+ g_object_set_property(G_OBJECT(tag), "foreground", &val);
+ g_value_unset(&val);
+ }
+ if (attr.strike()) {
+ g_value_init(&val, G_TYPE_BOOLEAN);
+ g_value_set_boolean(&val, true);
+ g_object_set_property(G_OBJECT(tag), "strikethrough", &val);
+ g_value_unset(&val);
+ }
+ g_value_init(&val, G_TYPE_INT);
+ if (attr.underline()) {
+ g_value_set_int(&val, PANGO_UNDERLINE_SINGLE);
+ g_object_set_property(G_OBJECT(tag), "underline", &val);
+ }
+ if (attr.bold()) {
+ g_value_set_int(&val, PANGO_WEIGHT_BOLD);
+ g_object_set_property(G_OBJECT(tag), "weight", &val);
+ }
+ if (attr.italic()) {
+ g_value_set_int(&val, PANGO_STYLE_ITALIC);
+ g_object_set_property(G_OBJECT(tag), "style", &val);
+ }
+ g_value_unset(&val);
+ tags_.emplace(attr, tag);
+ return tag;
+ }
+
+ static void color_value(GValue* value, gchar* tmp, size_t size, uint32_t color) {
+ g_value_init(value, G_TYPE_STRING);
+ snprintf(tmp, size, "rgba(%u, %u, %u, %u)",
+ static_cast<unsigned>((color >> 16) & 0xff),
+ static_cast<unsigned>((color >> 8) & 0xff),
+ static_cast<unsigned>(color & 0xff),
+ static_cast<unsigned>(color >> 24));
+ g_value_set_static_string(value, tmp);
+ }
+
+ shared_gobject<GtkTextBuffer> buffer_;
+ std::unordered_map<Attribute, GtkTextTag*> tags_;
+};
+
+class GtkGuiMain : public virtual GuiMain, public GtkGuiWindow {
+public:
+ GtkGuiMain(std::string const& title, uint32_t width, uint32_t height)
+ : title_(title), width_(width), height_(height), split_(0.5),
+ menu_(nullptr), statusbar_(nullptr) {
+ }
+
+ void set_menu(GuiMenu* menu) override {
+ assert(menu);
+ menu_ = menu;
+ }
+
+ GuiMenu* menu() const override {
+ return menu_;
+ }
+
+ void set_statusbar(GuiStatusBar* statusbar) override {
+ assert(statusbar);
+ statusbar_ = statusbar;
+ }
+
+ GuiStatusBar* statusbar() const override {
+ return statusbar_;
+ }
+
+ void set_split(double split) override;
+ double split() const override;
+
+ void set_listmodel(GuiListModel* model) override {
+ if (model) {
+ listmodel_.reset(list_model_new(model));
+ } else {
+ listmodel_.reset();
+ }
+ }
+ GuiListModel* listmodel() const override {
+ return listmodel_ ? list_model_get_model(listmodel_.get()) : nullptr;
+ }
+
+ void set_package(std::unique_ptr<AttributedText>&& text) override;
+
+ AttributedText* package() const {
+ return package_.get();
+ }
+
+ ListModel* gtklistmodel() const {
+ return listmodel_.get();
+ }
+
+ bool run(int argc, char** argv) override;
+
+ bool exit() override;
+
+ void set_title(std::string const& title) override;
+
+ void show(GuiWindow* window) override;
+
+ uint32_t default_width() const {
+ return width_;
+ }
+
+ uint32_t default_height() const {
+ return height_;
+ }
+
+ void add_listener(GuiMain::Listener* listener) override {
+ observers_.insert(listener);
+ }
+
+ void remove_listener(GuiMain::Listener* listener) override {
+ observers_.erase(listener);
+ }
+
+ void* impl() const override;
+
+ void sync_split() {
+ set_split(split_);
+ }
+
+ void notify_selected_row(size_t row) {
+ auto it = observers_.notify();
+ while (it.has_next()) {
+ it.next()->selected_row(this, row);
+ }
+ }
+
+ void notify_lost_selection() {
+ auto it = observers_.notify();
+ while (it.has_next()) {
+ it.next()->lost_selection(this);
+ }
+ }
+
+ void add_to_clipboard(std::string const& data,
+ std::string const& mimetype) override {
+ auto clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
+ if (mimetype.empty() || g_utf8_validate(data.data(), data.size(), nullptr)) {
+ gtk_clipboard_set_text(clipboard, data.data(), data.size());
+ return;
+ }
+ auto target = gtk_target_entry_new(mimetype.c_str(), 0, 0);
+ auto ptr = new std::string(data);
+ gtk_clipboard_set_with_data(
+ clipboard, target, 1,
+ [](GtkClipboard* UNUSED(clipboard),
+ GtkSelectionData* selection,
+ guint UNUSED(info),
+ gpointer user_data) -> void {
+ auto data = reinterpret_cast<std::string*>(user_data);
+ gtk_selection_data_set(selection,
+ gtk_selection_data_get_target(selection),
+ 8,
+ reinterpret_cast<guchar const*>(data->data()),
+ data->size());
+ },
+ [](GtkClipboard* UNUSED(clipboard),
+ gpointer user_data) -> void {
+ auto data = reinterpret_cast<std::string*>(user_data);
+ delete data;
+ },
+ ptr);
+ gtk_target_entry_free(target);
+ }
+
+private:
+ bool notify_about_to_exit() {
+ auto it = observers_.notify();
+ while (it.has_next()) {
+ if (!it.next()->about_to_exit(this)) return false;
+ }
+ return true;
+ }
+
+ std::string title_;
+ uint32_t width_;
+ uint32_t height_;
+ mutable double split_;
+
+ shared_gobject<MainApp> app_;
+ shared_gobject<ListModel> listmodel_;
+ std::unique_ptr<AttributedText> package_;
+ Observers<GuiMain::Listener*> observers_;
+ GuiMenu* menu_;
+ GuiStatusBar* statusbar_;
+};
+
+class GtkGuiMenu : public GuiMenu {
+public:
+ GtkGuiMenu()
+ : GtkGuiMenu(nullptr) {
+ }
+
+ ~GtkGuiMenu() override {
+ }
+
+ void add_listener(Listener* listener) override {
+ if (parent_) {
+ parent_->add_listener(listener);
+ return;
+ }
+ observers_.insert(listener);
+ }
+
+ void remove_listener(Listener* listener) override {
+ if (parent_) {
+ parent_->remove_listener(listener);
+ return;
+ }
+ observers_.erase(listener);
+ }
+
+ GMenuModel* model() const {
+ return G_MENU_MODEL(menu_.get());
+ }
+
+ void set_map(GActionMap* map);
+ void activate(GSimpleAction* action);
+
+ void add_item(std::string const& id, std::string const& label) override;
+ GuiMenu* add_menu(std::string const& label) override;
+ void add_separator() override;
+ bool enable_item(std::string const& id, bool enable) override;
+
+private:
+ explicit GtkGuiMenu(GtkGuiMenu* parent)
+ : parent_(parent), menu_(g_menu_new()), section_(g_menu_new()),
+ map_(nullptr) {
+ g_menu_append_section(menu_.get(), nullptr, G_MENU_MODEL(section_.get()));
+ }
+
+ void notify_item_activated(std::string const& id) {
+ auto it = observers_.notify();
+ while (it.has_next()) {
+ it.next()->item_activated(id);
+ }
+ }
+
+ std::string add_action(std::string const& id);
+ std::string escape(std::string const& id);
+ std::string unescape(std::string const& action);
+
+ GtkGuiMenu* const parent_;
+ shared_gobject<GMenu> menu_;
+ shared_gobject<GMenu> section_;
+ std::unordered_map<std::string, shared_gobject<GSimpleAction>> action_;
+ GActionMap* map_;
+ std::vector<std::unique_ptr<GtkGuiMenu>> submenus_;
+ Observers<Listener*> observers_;
+};
+
+class GtkGuiStatusBar : public GuiStatusBar {
+public:
+ GtkGuiStatusBar()
+ : statusbar_(nullptr) {
+ }
+
+ GtkWidget* widget() {
+ if (!statusbar_) {
+ statusbar_ = gtk_statusbar_new();
+ std_ctx_ = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusbar_), "");
+ ovr_ctx_ = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusbar_),
+ "override");
+ gtk_statusbar_push(GTK_STATUSBAR(statusbar_), std_ctx_, status_.c_str());
+ if (!override_.empty()) {
+ gtk_statusbar_push(GTK_STATUSBAR(statusbar_), ovr_ctx_,
+ override_.c_str());
+ }
+ }
+ return statusbar_;
+ }
+
+ void set_status(std::string const& str) override {
+ status_ = str;
+ if (statusbar_) {
+ gtk_statusbar_remove_all(GTK_STATUSBAR(statusbar_), std_ctx_);
+ gtk_statusbar_push(GTK_STATUSBAR(statusbar_), std_ctx_, status_.c_str());
+ }
+ }
+ void set_override(std::string const& str) override {
+ override_ = str;
+ if (statusbar_) {
+ gtk_statusbar_remove_all(GTK_STATUSBAR(statusbar_), ovr_ctx_);
+ if (!override_.empty()) {
+ gtk_statusbar_push(GTK_STATUSBAR(statusbar_), ovr_ctx_,
+ override_.c_str());
+ }
+ }
+ }
+
+private:
+ std::string status_;
+ std::string override_;
+ GtkWidget* statusbar_;
+ guint std_ctx_;
+ guint ovr_ctx_;
+};
+
+void close_about_dialog(GtkAboutDialog* about) {
+ // TODO: Restore dialog state
+ gtk_widget_hide(GTK_WIDGET(about));
+}
+
+class GtkGuiAbout : public virtual GuiAbout, public GtkGuiWindow {
+public:
+ GtkGuiAbout(std::string const& title, std::string const& version,
+ std::vector<std::string> const& authors)
+ : about_(GTK_ABOUT_DIALOG(gtk_about_dialog_new())) {
+ gtk_about_dialog_set_program_name(about_, title.c_str());
+ gtk_about_dialog_set_version(about_, version.c_str());
+ std::vector<std::string> tmp;
+ tmp.reserve(authors.size() / 2);
+ for (size_t i = 0; i < authors.size(); i += 2) {
+ if (authors[i + 1].empty()) {
+ tmp.push_back(authors[i]);
+ } else {
+ tmp.push_back(authors[i] + " <" + authors[i + 1] + ">");
+ }
+ }
+ std::vector<gchar const*> tmp2;
+ tmp2.reserve(tmp.size() + 1);
+ for (auto const& str : tmp) tmp2.push_back(str.c_str());
+ tmp2.push_back(nullptr);
+ gtk_about_dialog_set_authors(about_, tmp2.data());
+
+ g_signal_connect(about_, "delete-event",
+ G_CALLBACK(gtk_widget_hide_on_delete), nullptr);
+ g_signal_connect(about_, "response",
+ G_CALLBACK(close_about_dialog), nullptr);
+ }
+
+ ~GtkGuiAbout() override {
+ gtk_widget_destroy(GTK_WIDGET(about_));
+ }
+
+ void* impl() const override {
+ return about_;
+ }
+
+ void set_title(std::string const& title) override {
+ GtkGuiWindow::set_title(title);
+ }
+
+ void add_listener(GuiAbout::Listener* listener) override {
+ observers_.insert(listener);
+ }
+
+ void remove_listener(GuiAbout::Listener* listener) override {
+ observers_.erase(listener);
+ }
+
+private:
+ GtkAboutDialog* about_;
+ Observers<GuiAbout::Listener*> observers_;
+};
+
+class GtkGuiForm : public virtual GuiForm, public GtkGuiWindow {
+public:
+ GtkGuiForm(std::string const& title, std::string const& text)
+ : title_(title), text_(text), dialog_(nullptr), error_(nullptr) {
+ }
+
+ ~GtkGuiForm() override {
+ assert(!dialog_);
+ }
+
+ void set_title(std::string const& title) override {
+ title_ = title;
+ GtkGuiWindow::set_title(title);
+ }
+
+ void* impl() const override {
+ return dialog_;
+ }
+
+ void add_listener(GuiForm::Listener* listener) override {
+ observers_.insert(listener);
+ }
+
+ void remove_listener(GuiForm::Listener* listener) override {
+ observers_.erase(listener);
+ }
+
+ void add_string(std::string const& id, std::string const& label,
+ std::string const& value) override {
+ values_.emplace_back(new StringValue(id, label, value));
+ }
+
+ void add_number(std::string const& id, std::string const& label,
+ uint64_t value) override {
+ values_.emplace_back(new NumberValue(id, label, value));
+ }
+
+ std::string get_string(std::string const& id) const override {
+ for (auto const& value : values_) {
+ if (value->id_ == id && value->type_ == STRING) {
+ if (value->entry_) {
+ return gtk_entry_get_text(GTK_ENTRY(value->entry_));
+ }
+ return static_cast<StringValue*>(value.get())->value_;
+ }
+ }
+ assert(false);
+ return "";
+ }
+
+ uint64_t get_number(std::string const& id) const override {
+ for (auto const& value : values_) {
+ if (value->id_ == id && value->type_ == NUMBER) {
+ if (value->entry_) {
+ uint64_t i;
+ if (get_number(value->entry_, &i)) {
+ return i;
+ }
+ }
+ return static_cast<NumberValue*>(value.get())->value_;
+ }
+ }
+ assert(false);
+ return 0;
+ }
+
+ void set_error(std::string const& error) override {
+ if (!error_) {
+ assert(false);
+ return;
+ }
+ if (error.empty()) {
+ gtk_widget_hide(error_);
+ } else {
+ gtk_widget_show(error_);
+ gtk_label_set_text(GTK_LABEL(error_), error.c_str());
+ }
+ }
+
+ bool show(GuiWindow* parent) override {
+ if (dialog_) {
+ assert(false);
+ return false;
+ }
+ dialog_ = gtk_dialog_new_with_buttons(
+ title_.c_str(),
+ reinterpret_cast<GtkWindow*>(parent->impl()),
+ GTK_DIALOG_MODAL,
+ accept_button(),
+ GTK_RESPONSE_ACCEPT,
+ "_Cancel",
+ GTK_RESPONSE_REJECT,
+ nullptr);
+ auto content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog_));
+ auto spacing = 5;
+ if (!text_.empty()) {
+ auto label = gtk_label_new(text_.c_str());
+ gtk_box_pack_start(GTK_BOX(content_area), label, true, true, spacing);
+ }
+ for (auto& value : values_) {
+ auto label = gtk_label_new(value->label_.c_str());
+ value->entry_ = gtk_entry_new();
+ gtk_entry_set_activates_default(GTK_ENTRY(value->entry_), true);
+ switch (value->type_) {
+ case STRING:
+ gtk_entry_set_text(GTK_ENTRY(value->entry_),
+ static_cast<StringValue*>(value.get())->value_.c_str());
+ break;
+ case NUMBER: {
+ char tmp[20];
+ snprintf(tmp, sizeof(tmp), "%llu", static_cast<unsigned long long>(
+ static_cast<NumberValue*>(value.get())->value_));
+ gtk_entry_set_text(GTK_ENTRY(value->entry_), tmp);
+ gtk_entry_set_input_purpose(GTK_ENTRY(value->entry_),
+ GTK_INPUT_PURPOSE_DIGITS);
+ break;
+ }
+ }
+ auto box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, spacing);
+ gtk_box_pack_start(GTK_BOX(box), label, false, false, 0);
+ gtk_box_pack_start(GTK_BOX(box), value->entry_, true, true, 0);
+ gtk_box_pack_start(GTK_BOX(content_area), box, false, false, spacing);
+ }
+ error_ = gtk_label_new("");
+ gtk_label_set_xalign(GTK_LABEL(error_), 0.0);
+ gtk_box_pack_start(GTK_BOX(content_area), error_, true, true, spacing);
+ gtk_widget_show_all(dialog_);
+ add_extra_widgets(GTK_BOX(content_area), spacing);
+ gtk_widget_hide(error_);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog_), GTK_RESPONSE_ACCEPT);
+ gint result;
+ do {
+ result = gtk_dialog_run(GTK_DIALOG(dialog_));
+ if (result != GTK_RESPONSE_ACCEPT) break;
+ if (notify_about_to_close()) break;
+ } while (true);
+ for (auto& value : values_) {
+ switch (value->type_) {
+ case STRING:
+ static_cast<StringValue*>(value.get())->value_ =
+ gtk_entry_get_text(GTK_ENTRY(value->entry_));
+ break;
+ case NUMBER:
+ get_number(value->entry_,
+ &static_cast<NumberValue*>(value.get())->value_);
+ break;
+ }
+ value->entry_ = nullptr;
+ }
+ gtk_widget_destroy(dialog_);
+ dialog_ = nullptr;
+ error_ = nullptr;
+ reset_extra_widgets();
+ return result == GTK_RESPONSE_ACCEPT;
+ }
+
+protected:
+ enum Type {
+ STRING,
+ NUMBER,
+ };
+
+ struct Value {
+ Type const type_;
+ std::string const id_;
+ std::string const label_;
+ GtkWidget* entry_;
+
+ Value(Type type, std::string const& id, std::string const& label)
+ : type_(type), id_(id), label_(label), entry_(nullptr) {
+ }
+ };
+
+ struct StringValue : public Value {
+ std::string value_;
+
+ StringValue(std::string const& id, std::string const& label,
+ std::string const& value)
+ : Value(STRING, id, label), value_(value) {
+ }
+ };
+
+ struct NumberValue : public Value {
+ uint64_t value_;
+
+ NumberValue(std::string const& id, std::string const& label,
+ uint64_t value)
+ : Value(NUMBER, id, label), value_(value) {
+ }
+ };
+
+ virtual bool notify_about_to_close() {
+ auto it = observers_.notify();
+ while (it.has_next()) {
+ if (!it.next()->about_to_close(this)) return false;
+ }
+ return true;
+ }
+
+ static bool get_number(GtkWidget* entry, uint64_t* number) {
+ assert(entry && number);
+ auto text = gtk_entry_get_text(GTK_ENTRY(entry));
+ errno = 0;
+ char* end = nullptr;
+ auto tmp = strtoull(text, &end, 10);
+ if (errno && end && !*end) {
+ if (number) *number = tmp;
+ return true;
+ }
+ return false;
+ }
+
+ virtual void add_extra_widgets(GtkBox*, gint) {
+ }
+
+ virtual void reset_extra_widgets() {
+ }
+
+ virtual char const* accept_button() {
+ return "_OK";
+ }
+
+ std::string title_;
+ std::string text_;
+ std::vector<std::unique_ptr<Value>> values_;
+ Observers<GuiForm::Listener*> observers_;
+ GtkWidget* dialog_;
+ GtkWidget* error_;
+};
+
+class GtkGuiFormApply : public GtkGuiForm, public virtual GuiFormApply {
+public:
+ GtkGuiFormApply(std::string const& title, std::string const& text,
+ std::string const& apply_button, Delegate* delegate)
+ : GtkGuiForm(title, text), apply_button_(apply_button),
+ delegate_(delegate), spinner_(nullptr), applied_(false) {
+ }
+
+ void applied(bool success) override {
+ assert(spinner_);
+ if (spinner_) {
+ gtk_spinner_stop(GTK_SPINNER(spinner_));
+ gtk_widget_hide(spinner_);
+ }
+ assert(dialog_);
+ if (dialog_) {
+ gtk_dialog_set_response_sensitive(GTK_DIALOG(dialog_),
+ GTK_RESPONSE_ACCEPT, true);
+ }
+ if (success) {
+ applied_ = true;
+ if (dialog_) {
+ gtk_dialog_response(GTK_DIALOG(dialog_), GTK_RESPONSE_ACCEPT);
+ }
+ }
+ }
+
+private:
+ void add_extra_widgets(GtkBox* content_area, gint spacing) override {
+ assert(!spinner_);
+ spinner_ = gtk_spinner_new();
+ gtk_box_pack_start(content_area, spinner_, true, true, spacing);
+ }
+
+ void reset_extra_widgets() override {
+ spinner_ = nullptr;
+ }
+
+ char const* accept_button() override {
+ return apply_button_.c_str();
+ }
+
+ bool notify_about_to_close() override {
+ if (applied_) {
+ applied_ = false;
+ return true;
+ }
+ if (!GtkGuiForm::notify_about_to_close()) return false;
+ assert(spinner_);
+ if (spinner_) {
+ gtk_widget_show(spinner_);
+ gtk_spinner_start(GTK_SPINNER(spinner_));
+ }
+ assert(dialog_);
+ if (dialog_) {
+ gtk_dialog_set_response_sensitive(GTK_DIALOG(dialog_),
+ GTK_RESPONSE_ACCEPT, false);
+ }
+ if (error_) {
+ gtk_widget_hide(error_);
+ }
+ delegate_->apply(this);
+ if (applied_) {
+ applied_ = false;
+ return true;
+ }
+ return false;
+ }
+
+ std::string apply_button_;
+ Delegate* const delegate_;
+ GtkWidget* spinner_;
+ bool applied_;
+};
+
+
+struct _MainApp
+{
+ GtkApplication parent_;
+ GtkGuiMain* main_;
+};
+
+struct _MainAppWindow
+{
+ GtkApplicationWindow parent_;
+ GtkWidget* paned_;
+ GtkWidget* top_;
+ GtkWidget* bottom_;
+};
+
+G_DEFINE_TYPE(MainApp, main_app, GTK_TYPE_APPLICATION);
+
+G_DEFINE_TYPE(MainAppWindow, main_app_window, GTK_TYPE_APPLICATION_WINDOW);
+
+void top_selection_changed(GtkTreeSelection* selection, gpointer data) {
+ auto main = reinterpret_cast<GtkGuiMain*>(data);
+ GtkTreeIter iter;
+ GtkTreeModel* model;
+ if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+ auto path = gtk_tree_model_get_path(model, &iter);
+ if (path) {
+ main->notify_selected_row(gtk_tree_path_get_indices(path)[0]);
+ gtk_tree_path_free(path);
+ return;
+ }
+ }
+ main->notify_lost_selection();
+}
+
+MainAppWindow* main_app_window_new(MainApp *app) {
+ auto ret = MAIN_APP_WINDOW(g_object_new(MAIN_APP_WINDOW_TYPE,
+ "application", app, nullptr));
+ gtk_window_set_default_size(GTK_WINDOW(ret), app->main_->default_width(),
+ app->main_->default_height());
+ auto box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ ret->paned_ = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
+ auto listmodel = app->main_->listmodel();
+ if (listmodel) {
+ ret->top_ = gtk_tree_view_new_with_model(GTK_TREE_MODEL(app->main_->gtklistmodel()));
+ auto renderer = gtk_cell_renderer_text_new();
+ for (size_t column = 0; column < listmodel->columns(); ++column) {
+ gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(ret->top_), -1,
+ listmodel->header(column).c_str(), renderer,
+ "text", column,
+ nullptr);
+ gtk_tree_view_column_set_resizable(gtk_tree_view_get_column(GTK_TREE_VIEW(ret->top_), column), true);
+ }
+ gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(ret->top_), true);
+ auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(ret->top_));
+ gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
+ g_signal_connect(G_OBJECT(selection), "changed", G_CALLBACK(top_selection_changed), app->main_);
+ auto top_scroll = gtk_scrolled_window_new(nullptr, nullptr);
+ gtk_container_add(GTK_CONTAINER(top_scroll), ret->top_);
+ gtk_paned_add1(GTK_PANED(ret->paned_), top_scroll);
+ }
+ ret->bottom_ = gtk_text_view_new();
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(ret->bottom_), false);
+ gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(ret->bottom_), false);
+ gtk_text_view_set_monospace(GTK_TEXT_VIEW(ret->bottom_), true);
+ if (app->main_->package()) {
+ gtk_text_view_set_buffer(GTK_TEXT_VIEW(ret->bottom_),
+ static_cast<GtkAttributedText*>(app->main_->package())->buffer());
+ }
+ auto bottom_scroll = gtk_scrolled_window_new(nullptr, nullptr);
+ gtk_container_add(GTK_CONTAINER(bottom_scroll), ret->bottom_);
+ gtk_paned_add2(GTK_PANED(ret->paned_), bottom_scroll);
+ gtk_box_pack_start(GTK_BOX(box), ret->paned_, true, true, 0);
+ auto statusbar = static_cast<GtkGuiStatusBar*>(app->main_->statusbar());
+ if (statusbar) {
+ gtk_box_pack_start(GTK_BOX(box), statusbar->widget(), false, false, 0);
+ }
+ gtk_widget_show_all(box);
+ gtk_container_add(GTK_CONTAINER(ret), box);
+ return ret;
+}
+
+void main_app_activate(GApplication* g_app) {
+ auto app = MAIN_APP(g_app);
+ auto menu = static_cast<GtkGuiMenu*>(app->main_->menu());
+ if (menu) {
+ menu->set_map(G_ACTION_MAP(app));
+ gtk_application_set_menubar(GTK_APPLICATION(app), menu->model());
+ }
+ auto win = main_app_window_new(app);
+ gtk_window_present(GTK_WINDOW(win));
+
+ app->main_->sync_split();
+}
+
+void main_app_window_open(MainAppWindow* UNUSED(win), GFile* UNUSED(file)) {
+}
+
+void main_app_open(GApplication* app, GFile** files, gint n_files,
+ gchar const* UNUSED(hint)) {
+ auto windows = gtk_application_get_windows(GTK_APPLICATION(app));
+ auto win = windows ? MAIN_APP_WINDOW(windows->data)
+ : main_app_window_new(MAIN_APP(app));
+ for (auto i = 0; i < n_files; i++) {
+ main_app_window_open(win, files[i]);
+ }
+
+ gtk_window_present(GTK_WINDOW(win));
+
+ MAIN_APP(app)->main_->sync_split();
+}
+
+void main_app_init(MainApp* UNUSED(app)) {
+}
+
+void main_app_class_init(MainAppClass* clazz) {
+ G_APPLICATION_CLASS(clazz)->activate = main_app_activate;
+ G_APPLICATION_CLASS(clazz)->open = main_app_open;
+}
+
+void main_app_window_init(MainAppWindow* UNUSED(app)) {
+}
+
+void main_app_window_class_init(MainAppWindowClass* UNUSED(clazz)) {
+}
+
+MainApp* main_app_new(GtkGuiMain* main) {
+ auto ret = MAIN_APP(g_object_new(MAIN_APP_TYPE,
+ "application-id", "org.jk.tp",
+ "flags", G_APPLICATION_HANDLES_OPEN,
+ nullptr));
+ ret->main_ = main;
+ return ret;
+}
+
+void GtkGuiWindow::set_title(std::string const& title) {
+ auto win = window();
+ if (!win) return;
+ gtk_window_set_title(win, title.c_str());
+}
+
+bool GtkGuiMain::run(int argc, char** argv) {
+ app_.reset(main_app_new(this));
+ return g_application_run(G_APPLICATION(app_.get()), argc, argv);
+}
+
+void* GtkGuiMain::impl() const {
+ if (!app_) return nullptr;
+ auto windows = gtk_application_get_windows(GTK_APPLICATION(app_.get()));
+ if (!windows) return nullptr;
+ return windows->data;
+}
+
+void GtkGuiMain::set_title(std::string const& title) {
+ title_ = title;
+ GtkGuiWindow::set_title(title);
+}
+
+bool GtkGuiMain::exit() {
+ if (!app_) {
+ assert(false);
+ return true;
+ }
+ if (!notify_about_to_exit()) return false;
+ g_application_quit(G_APPLICATION(app_.get()));
+ return true;
+}
+
+void GtkGuiMain::show(GuiWindow* window) {
+ auto wnd = reinterpret_cast<GtkWindow*>(window->impl());
+ if (!wnd) {
+ assert(false);
+ return;
+ }
+ gtk_window_set_transient_for(wnd, this->window());
+ gtk_window_present(wnd);
+}
+
+void GtkGuiMain::set_package(std::unique_ptr<AttributedText>&& text) {
+ package_.swap(text);
+ auto wnd = reinterpret_cast<MainAppWindow*>(window());
+ if (wnd) {
+ gtk_text_view_set_buffer(GTK_TEXT_VIEW(wnd->bottom_),
+ static_cast<GtkAttributedText*>(package_.get())->buffer());
+ }
+}
+
+void GtkGuiMain::set_split(double split) {
+ split_ = std::max(0.0, std::min(split, 1.0));
+ auto wnd = reinterpret_cast<MainAppWindow*>(window());
+ if (wnd) {
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(wnd->paned_, &alloc);
+ gtk_paned_set_position(GTK_PANED(wnd->paned_), round(alloc.height * split));
+ }
+}
+
+double GtkGuiMain::split() const {
+ auto wnd = reinterpret_cast<MainAppWindow*>(window());
+ if (wnd) {
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(wnd->paned_, &alloc);
+ split_ = static_cast<double>(alloc.height)
+ / gtk_paned_get_position(GTK_PANED(wnd->paned_));
+ }
+ return split_;
+}
+
+void GtkGuiMenu::add_item(std::string const& id, std::string const& label) {
+ auto action = add_action(id);
+ g_menu_append(section_.get(), label.c_str(), action.c_str());
+}
+
+GuiMenu* GtkGuiMenu::add_menu(std::string const& label) {
+ auto ret = new GtkGuiMenu(this);
+ g_menu_append_submenu(section_.get(), label.c_str(), ret->model());
+ submenus_.emplace_back(ret);
+ return ret;
+}
+
+void GtkGuiMenu::add_separator() {
+ section_.reset(g_menu_new());
+ g_menu_append_section(menu_.get(), nullptr, G_MENU_MODEL(section_.get()));
+}
+
+void GtkGuiMenu::set_map(GActionMap* map) {
+ assert(!parent_);
+ assert(!map_);
+ assert(map);
+ map_ = map;
+ for (auto const& pair : action_) {
+ g_action_map_add_action(map_, G_ACTION(pair.second.get()));
+ }
+}
+
+std::string GtkGuiMenu::escape(std::string const& action) {
+ // TODO
+ return action;
+}
+
+std::string GtkGuiMenu::unescape(std::string const& action) {
+ return action;
+}
+
+void menu_item_activate(GSimpleAction* action, GVariant* UNUSED(parameter),
+ gpointer data) {
+ auto menu = reinterpret_cast<GtkGuiMenu*>(data);
+ menu->activate(action);
+}
+
+void GtkGuiMenu::activate(GSimpleAction* action) {
+ notify_item_activated(unescape(g_action_get_name(G_ACTION(action))));
+}
+
+std::string GtkGuiMenu::add_action(std::string const& id) {
+ if (parent_) return parent_->add_action(id);
+ std::string name = escape(id);
+ auto action = g_simple_action_new(name.c_str(), nullptr);
+ action_.emplace(id, action);
+ g_signal_connect(action, "activate", G_CALLBACK(menu_item_activate), this);
+ if (map_) g_action_map_add_action(map_, G_ACTION(action));
+ return "app." + name;
+}
+
+bool GtkGuiMenu::enable_item(std::string const& id, bool enable) {
+ if (parent_) return parent_->enable_item(id, enable);
+ auto pair = action_.find(id);
+ if (pair == action_.end()) return false;
+ g_simple_action_set_enabled(pair->second.get(), enable);
+ return true;
+}
+
+class GtkLooper : public Looper {
+public:
+ GtkLooper() {
+ }
+
+ void add(int fd, uint8_t events, FdCallback const& callback) override {
+ auto channel = g_io_channel_unix_new(fd);
+ auto handle = new Fd(channel, callback);
+ handle->watch_ = g_io_add_watch(channel, events2cond(events),
+ &GtkLooper::event, handle);
+ fds_.emplace(fd, handle);
+ }
+
+ void modify(int fd, uint8_t events) override {
+ auto it = fds_.find(fd);
+ if (it == fds_.end()) {
+ assert(false);
+ return;
+ }
+ auto& handle = it->second;
+ if (handle->in_callback_) {
+ handle->delayed_remove_ = true;
+ } else {
+ g_source_remove(handle->watch_);
+ }
+ handle->watch_ = g_io_add_watch(handle->channel_, events2cond(events),
+ &GtkLooper::event, handle.get());
+ }
+
+ void remove(int fd) override {
+ auto it = fds_.find(fd);
+ if (it == fds_.end()) {
+ assert(false);
+ return;
+ }
+ auto& handle = it->second;
+ if (handle->in_callback_) {
+ handle->delayed_remove_ = true;
+ handle->watch_ = 0;
+ handle.release(); // will be removed in event() and not by fds_.erase
+ } else {
+ g_source_remove(handle->watch_);
+ }
+ fds_.erase(it);
+ }
+
+ void* schedule(float delay_s, ScheduleCallback const& callback) override {
+ auto handle = new Handle(callback);
+ handle->id_ = g_timeout_add(delay_s * 1000, &GtkLooper::timeout, handle);
+ return handle;
+ }
+
+ void cancel(void* handle) override {
+ auto h = reinterpret_cast<Handle*>(handle);
+ if (h->id_) {
+ g_source_remove(h->id_);
+ delete h;
+ }
+ }
+
+ bool run() override {
+ assert(false);
+ return false;
+ }
+ void quit() override {
+ assert(false);
+ }
+
+ clock::time_point now() const override {
+ return clock::now();
+ }
+
+private:
+ struct Handle {
+ guint id_;
+ ScheduleCallback callback_;
+
+ Handle(ScheduleCallback const& callback)
+ : id_(0), callback_(callback) {
+ }
+ };
+
+ struct Fd {
+ GIOChannel* channel_;
+ FdCallback callback_;
+ guint watch_;
+ bool in_callback_;
+ bool delayed_remove_;
+
+ Fd(GIOChannel* channel, FdCallback const& callback)
+ : channel_(channel), callback_(callback), watch_(0),
+ in_callback_(false), delayed_remove_(false) {
+ }
+
+ Fd(Fd const&) = delete;
+
+ ~Fd() {
+ g_io_channel_unref(channel_);
+ }
+ };
+
+ static gboolean timeout(gpointer userdata) {
+ auto handle = reinterpret_cast<Handle*>(userdata);
+ handle->id_ = 0;
+ handle->callback_(handle);
+ delete handle;
+ return false; // All timers are one-shot
+ }
+
+ static GIOCondition events2cond(uint8_t events) {
+ gint cond = 0;
+ if (events & EVENT_READ) cond |= G_IO_IN | G_IO_PRI;
+ if (events & EVENT_WRITE) cond |= G_IO_OUT;
+ return static_cast<GIOCondition>(cond);
+ }
+
+ static uint8_t cond2events(GIOCondition cond) {
+ uint8_t events = 0;
+ if (cond & (G_IO_IN | G_IO_PRI)) events |= EVENT_READ;
+ if (cond & G_IO_OUT) events |= EVENT_WRITE;
+ if (cond & G_IO_HUP) events |= EVENT_HUP;
+ if (cond & (G_IO_ERR | G_IO_NVAL)) events |= EVENT_ERROR;
+ return events;
+ }
+
+ static gboolean event(GIOChannel* source, GIOCondition cond, gpointer data) {
+ auto handle = reinterpret_cast<Fd*>(data);
+ assert(handle->channel_ == source);
+ assert(!handle->in_callback_);
+ auto fd = g_io_channel_unix_get_fd(handle->channel_);
+ handle->in_callback_ = true;
+ handle->callback_(fd, cond2events(cond));
+ handle->in_callback_ = false;
+ if (handle->delayed_remove_) {
+ handle->delayed_remove_ = false;
+ if (!handle->watch_) delete handle;
+ return false;
+ }
+ return true;
+ }
+
+ std::unordered_map<int, std::unique_ptr<Fd>> fds_;
+};
+
+} // namespace
+
+// static
+GuiMain* GuiMain::create(std::string const& title, uint32_t width,
+ uint32_t height) {
+ return new GtkGuiMain(title, width, height);
+}
+
+// static
+GuiMenu* GuiMenu::create() {
+ return new GtkGuiMenu();
+}
+
+// static
+GuiStatusBar* GuiStatusBar::create() {
+ return new GtkGuiStatusBar();
+}
+
+// static
+GuiAbout* GuiAbout::create(std::string const& title,
+ std::string const& version,
+ char const* author_name,
+ char const* author_email,
+ ...) {
+ std::vector<std::string> authors;
+ authors.push_back(author_name);
+ authors.push_back(author_email);
+ va_list args;
+ va_start(args, author_email);
+ while (true) {
+ auto name = va_arg(args, char const*);
+ if (!name) break;
+ auto email = va_arg(args, char const*);
+ authors.push_back(name);
+ authors.push_back(email);
+ }
+ va_end(args);
+ return new GtkGuiAbout(title, version, authors);
+}
+
+// static
+GuiForm* GuiForm::create(std::string const& title,
+ std::string const& text) {
+ return new GtkGuiForm(title, text);
+}
+
+// static
+GuiFormApply* GuiFormApply::create(std::string const& title,
+ std::string const& text,
+ std::string const& apply_button,
+ GuiFormApply::Delegate* delegate) {
+ return new GtkGuiFormApply(title, text, apply_button, delegate);
+}
+
+// static
+Looper* GuiMain::createLooper() {
+ return new GtkLooper();
+}
+
+// static
+AttributedText* AttributedText::create() {
+ return new GtkAttributedText();
+}