// -*- mode: c++; c-basic-offset: 2; -*- #include "common.hh" #include #include #include #include #include #include #include "gui_about.hh" #include "gui_config.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 { size_t operator()(AttributedText::Attribute const& attr) const { return attr.hash(); } }; } // namespace std namespace { template 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(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(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(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(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(row) >= model->model_->rows()) return false; g_return_val_if_fail(iter, false); iter->stamp = model->iter_stamp_; iter->user_data = reinterpret_cast(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(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(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(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(iter->user_data) + 1; if (row >= model->model_->rows()) return false; iter->user_data = reinterpret_cast(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(iter->user_data); if (row == 0) return false; iter->user_data = reinterpret_cast(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(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(n) < model->model_->rows(), false); g_return_val_if_fail(iter, false); iter->stamp = model->iter_stamp_; iter->user_data = reinterpret_cast(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(list_model_class_init), nullptr, // class_finalize nullptr, // class_data sizeof(ListModel), 0, // n_preallocs reinterpret_cast(list_model_init) }; static const GInterfaceInfo tree_model_info = { reinterpret_cast(list_tree_model_init), nullptr, nullptr }; type = g_type_register_static(G_TYPE_OBJECT, "ListModel", &list_model_type_info, static_cast(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(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; } void reset() override { GtkTextIter begin, end; gtk_text_buffer_get_start_iter(buffer_.get(), &begin); gtk_text_buffer_get_end_iter(buffer_.get(), &end); gtk_text_buffer_delete(buffer_.get(), &begin, &end); } 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((color >> 16) & 0xff), static_cast((color >> 8) & 0xff), static_cast(color & 0xff), static_cast(color >> 24)); g_value_set_static_string(value, tmp); } shared_gobject buffer_; std::unordered_map tags_; }; class GtkConfig : public GuiConfig { public: GtkConfig() : settings_(g_settings_new("org.the_jk.tp.Monitor")) { } ~GtkConfig() override { g_settings_sync(); } using Config::get; std::string const& get(std::string const& key, std::string const& fallback) override { auto variant = g_settings_get_user_value(settings_.get(), key.c_str()); if (!variant) return fallback; memory_[key] = g_variant_get_string(variant, nullptr); g_variant_unref(variant); return memory_[key]; } char const* get(std::string const& key, char const* fallback) override { auto variant = g_settings_get_user_value(settings_.get(), key.c_str()); if (!variant) return fallback; memory_[key] = g_variant_get_string(variant, nullptr); g_variant_unref(variant); return memory_[key].c_str(); } bool is_set(std::string const& key) override { auto variant = g_settings_get_user_value(settings_.get(), key.c_str()); if (!variant) return false; g_variant_unref(variant); return true; } void set(std::string const& key, std::string const& value) override { g_settings_set_string(settings_.get(), key.c_str(), value.c_str()); } private: shared_gobject settings_; std::unordered_map memory_; }; 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), config_(new GtkConfig()), 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&& 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(user_data); gtk_selection_data_set(selection, gtk_selection_data_get_target(selection), 8, reinterpret_cast(data->data()), data->size()); }, [](GtkClipboard* UNUSED(clipboard), gpointer user_data) -> void { auto data = reinterpret_cast(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; } Config* config() override { return config_.get(); } std::string title_; uint32_t width_; uint32_t height_; mutable double split_; shared_gobject app_; shared_gobject listmodel_; std::unique_ptr package_; std::unique_ptr config_; Observers 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 menu_; shared_gobject section_; std::unordered_map> action_; GActionMap* map_; std::vector> submenus_; Observers 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 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 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 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 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(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(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(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(value.get())->value_.c_str()); break; case NUMBER: { char tmp[20]; snprintf(tmp, sizeof(tmp), "%llu", static_cast( static_cast(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(value.get())->value_ = gtk_entry_get_text(GTK_ENTRY(value->entry_)); break; case NUMBER: get_number(value->entry_, &static_cast(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> values_; Observers 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(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(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(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(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(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&& text) { package_.swap(text); auto wnd = reinterpret_cast(window()); if (wnd) { gtk_text_view_set_buffer(GTK_TEXT_VIEW(wnd->bottom_), static_cast(package_.get())->buffer()); } } void GtkGuiMain::set_split(double split) { split_ = std::max(0.0, std::min(split, 1.0)); auto wnd = reinterpret_cast(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(window()); if (wnd) { GtkAllocation alloc; gtk_widget_get_allocation(wnd->paned_, &alloc); split_ = static_cast(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(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_) { if (handle->delayed_remove_) { g_source_remove(handle->watch_); } else { 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); 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(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(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(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> 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 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(); }