// -*- 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_message.hh" #include "gui_statusbar.hh" #include "gui_textwnd.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 + 1; row > first; --row) { gtk_tree_path_append_index(path, row - 1); 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(); } 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 get(std::string const& key, bool fallback) override { auto variant = g_settings_get_user_value(settings_.get(), key.c_str()); if (!variant) return fallback; bool ret = g_variant_get_boolean(variant); g_variant_unref(variant); return ret; } 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()); } void set(std::string const& key, bool value) override { g_settings_set_boolean(settings_.get(), key.c_str(), value); } void remove(std::string const& key) override { g_settings_reset(settings_.get(), key.c_str()); } private: shared_gobject settings_; std::unordered_map memory_; }; void set_filter(GtkFileChooser* chooser, std::vector const& filters) { for (auto const& filter : filters) { auto f = gtk_file_filter_new(); gtk_file_filter_set_name(f, filter.name.c_str()); for (auto const& mask : filter.masks) { gtk_file_filter_add_pattern(f, mask.c_str()); } gtk_file_chooser_add_filter(chooser, f); } } 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; std::string const& title() const override { return title_; } 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 notify_open(std::string const& file) { auto it = observers_.notify(); while (it.has_next()) { it.next()->open(this, file); } } 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*, GtkSelectionData* selection, guint, 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*, gpointer user_data) -> void { auto data = reinterpret_cast(user_data); delete data; }, ptr); gtk_target_entry_free(target); } std::string file_dialog(std::string const& title, std::string const& file, uint8_t flags, std::vector const& filter) override { shared_gobject chooser(gtk_file_chooser_native_new( title.c_str(), reinterpret_cast(impl()), flags & GuiFile::FILE_SAVE ? GTK_FILE_CHOOSER_ACTION_SAVE : GTK_FILE_CHOOSER_ACTION_OPEN, nullptr, nullptr)); if (flags & GuiFile::FILE_SAVE) { gtk_file_chooser_set_do_overwrite_confirmation( GTK_FILE_CHOOSER(chooser.get()), true); } if (file.empty()) { gtk_file_chooser_set_filename( GTK_FILE_CHOOSER(chooser.get()), file.c_str()); } set_filter(GTK_FILE_CHOOSER(chooser.get()), filter); std::string ret_file; auto ret = gtk_native_dialog_run(GTK_NATIVE_DIALOG(chooser.get())); if (ret == GTK_RESPONSE_ACCEPT) { ret_file = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser.get())); } return ret_file; } 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) : title_(title), 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 { title_ = title; GtkGuiWindow::set_title(title); } std::string const& title() const override { return title_; } void add_listener(GuiAbout::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiAbout::Listener* listener) override { observers_.erase(listener); } private: std::string title_; 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); } std::string const& title() const override { return 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, std::string const& description) override { values_.emplace_back(new StringValue(this, id, label, description, value)); } void add_number(std::string const& id, std::string const& label, uint64_t value, std::string const& description) override { values_.emplace_back(new NumberValue(this, id, label, description, value)); } void add_file(std::string const& id, std::string const& label, std::string const& value, std::string const& description, uint8_t flags, std::vector const& filter) override { values_.emplace_back(new FileValue(this, id, label, description, value, flags, filter)); } void add_bool(std::string const& id, std::string const& label, bool value, std::string const& description) override { values_.emplace_back(new BoolValue(this, id, label, description, value)); } void enable(std::string const& id, bool enable) override { for (auto const& value : values_) { if (value->id_ == id) { value->enable_ = enable; if (value->entry_) { gtk_widget_set_sensitive(value->entry_, enable); } switch (value->type_) { case STRING: case NUMBER: case BOOLEAN: break; case FILE: { auto v = static_cast(value.get()); if (v->button_) { gtk_widget_set_sensitive(GTK_WIDGET(v->button_), enable); } else if (v->chooser_) { gtk_widget_set_sensitive(GTK_WIDGET(v->chooser_), enable); } break; } } } } } 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; } std::string get_file(std::string const& id) const override { for (auto const& value : values_) { if (value->id_ == id && value->type_ == FILE) { auto v = static_cast(value.get()); if ((v->flags_ & GuiFile::FILE_SAVE) == 0 && v->chooser_) { std::string ret; auto file = gtk_file_chooser_get_filename(v->chooser_); if (file) { ret.assign(file); g_free(file); } return ret; } return v->value_; } } assert(false); return ""; } bool get_bool(std::string const& id) const override { for (auto const& value : values_) { if (value->id_ == id && value->type_ == BOOLEAN) { if (value->entry_) { return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(value->entry_)); } return static_cast(value.get())->value_; } } assert(false); return false; } 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 grid = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(grid), 5); gtk_grid_set_row_spacing(GTK_GRID(grid), 5); gint row = 0; if (!text_.empty()) { auto label = gtk_label_new(text_.c_str()); gtk_label_set_width_chars(GTK_LABEL(label), 35); gtk_label_set_line_wrap(GTK_LABEL(label), true); gtk_grid_attach(GTK_GRID(grid), label, 0, row++, 3, 1); } for (auto& value : values_) { GtkWidget* label; if (value->type_ != BOOLEAN) { label = gtk_label_new(value->label_.c_str()); } else { label = nullptr; } if (value->type_ != FILE || (static_cast(value.get()) ->flags_ & GuiFile::FILE_SAVE)) { value->entry_ = gtk_entry_new(); gtk_entry_set_activates_default(GTK_ENTRY(value->entry_), true); } GtkWidget* extra = nullptr; switch (value->type_) { case STRING: gtk_entry_set_text(GTK_ENTRY(value->entry_), static_cast(value.get())->value_.c_str()); g_signal_connect(G_OBJECT(value->entry_), "changed", G_CALLBACK(changed), value.get()); 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); g_signal_connect(G_OBJECT(value->entry_), "changed", G_CALLBACK(changed), value.get()); break; } case FILE: { auto v = static_cast(value.get()); if ((v->flags_ & GuiFile::FILE_SAVE) == 0) { assert(!value->entry_); value->entry_ = gtk_file_chooser_button_new( v->label_.c_str(), GTK_FILE_CHOOSER_ACTION_OPEN); v->chooser_ = GTK_FILE_CHOOSER(value->entry_); g_signal_connect(G_OBJECT(v->chooser_), "file-set", G_CALLBACK(file_set), value.get()); } else { auto chooser = gtk_file_chooser_native_new( v->label_.c_str(), GTK_WINDOW(dialog_), GTK_FILE_CHOOSER_ACTION_SAVE, nullptr, nullptr); extra = gtk_button_new_with_label("..."); g_signal_connect(G_OBJECT(extra), "clicked", G_CALLBACK(show_filechooser), v); v->chooser_ = GTK_FILE_CHOOSER(chooser); v->button_ = extra; gtk_editable_set_editable(GTK_EDITABLE(value->entry_), false); gtk_file_chooser_set_do_overwrite_confirmation(v->chooser_, true); g_signal_connect(G_OBJECT(value->entry_), "changed", G_CALLBACK(changed), value.get()); } if (!v->value_.empty()) { gtk_file_chooser_set_filename(v->chooser_, v->value_.c_str()); } set_filter(v->chooser_, v->filter_); break; } case BOOLEAN: assert(!label); gtk_widget_destroy(value->entry_); value->entry_ = gtk_check_button_new_with_label(value->label_.c_str()); gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(value->entry_), static_cast(value.get())->value_); g_signal_connect(G_OBJECT(value->entry_), "toggled", G_CALLBACK(toggled), value.get()); break; } gint col = 0; if (label) { gtk_grid_attach(GTK_GRID(grid), label, col++, row, 1, 1); } gtk_widget_set_sensitive(value->entry_, value->enable_); if (extra) { gtk_widget_set_sensitive(extra, value->enable_); gtk_grid_attach(GTK_GRID(grid), value->entry_, col, row, 2 - col, 1); gtk_grid_attach(GTK_GRID(grid), extra, ++col, row, 1, 1); } else { gtk_grid_attach(GTK_GRID(grid), value->entry_, col, row, 3 - col, 1); } row++; if (!value->description_.empty()) { auto desc = gtk_label_new(NULL); gtk_label_set_markup(GTK_LABEL(desc), ("" + value->description_ + "").c_str()); gtk_widget_set_halign(GTK_WIDGET(desc), GTK_ALIGN_END); gtk_label_set_line_wrap(GTK_LABEL(desc), true); gtk_label_set_max_width_chars(GTK_LABEL(desc), 35); gtk_grid_attach(GTK_GRID(grid), desc, 0, row++, 3, 1); } } error_ = gtk_label_new(""); gtk_label_set_xalign(GTK_LABEL(error_), 0.0); gtk_grid_attach(GTK_GRID(grid), error_, 0, row++, 3, 1); gtk_container_add(GTK_CONTAINER(content_area), grid); gtk_widget_show_all(dialog_); row = add_extra_widgets(GTK_GRID(grid), row, 3); 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; case FILE: { auto v = static_cast(value.get()); if ((v->flags_ & GuiFile::FILE_SAVE) == 0) { auto file = gtk_file_chooser_get_filename(v->chooser_); if (file) { v->value_ = file; g_free(file); } else { v->value_.clear(); } } else { g_object_unref(v->chooser_); } v->chooser_ = nullptr; break; } case BOOLEAN: static_cast(value.get())->value_ = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(value->entry_)); 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, FILE, BOOLEAN, }; struct Value { GtkGuiForm* me_; Type const type_; std::string const id_; std::string const label_; std::string const description_; bool enable_; GtkWidget* entry_; Value(GtkGuiForm* me, Type type, std::string const& id, std::string const& label, std::string const& description) : me_(me), type_(type), id_(id), label_(label), description_(description), enable_(true), entry_(nullptr) { } }; struct StringValue : public Value { std::string value_; StringValue(GtkGuiForm* me, std::string const& id, std::string const& label, std::string const& description, std::string const& value) : Value(me, STRING, id, label, description), value_(value) { } }; struct NumberValue : public Value { uint64_t value_; NumberValue(GtkGuiForm* me, std::string const& id, std::string const& label, std::string const& description, uint64_t value) : Value(me, NUMBER, id, label, description), value_(value) { } }; struct FileValue : public Value { std::string value_; uint8_t flags_; std::vector filter_; GtkFileChooser* chooser_; GtkWidget* button_; FileValue(GtkGuiForm* me, std::string const& id, std::string const& label, std::string const& description, std::string const& value, uint8_t flags, std::vector const& filter) : Value(me, FILE, id, label, description), value_(value), flags_(flags), filter_(filter), chooser_(nullptr), button_(nullptr) { } }; struct BoolValue : public Value { bool value_; BoolValue(GtkGuiForm* me, std::string const& id, std::string const& label, std::string const& description, bool value) : Value(me, BOOLEAN, id, label, description), 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; } void notify_changed(std::string const& id) { auto it = observers_.notify(); while (it.has_next()) { it.next()->changed(this, id); } } 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; } static void safe_entry_from_filename(GtkWidget* widget, std::string const& filename) { auto str = g_filename_to_utf8(filename.data(), filename.size(), nullptr, nullptr, nullptr); if (str) { gtk_entry_set_text(GTK_ENTRY(widget), str); g_free(str); } else { gtk_entry_set_text(GTK_ENTRY(widget), ""); } } static void show_filechooser(GtkButton*, gpointer user_data) { auto value = reinterpret_cast(user_data); auto ret = gtk_native_dialog_run(GTK_NATIVE_DIALOG(value->chooser_)); if (ret == GTK_RESPONSE_ACCEPT) { auto file = gtk_file_chooser_get_filename(value->chooser_); value->value_ = file; g_free(file); safe_entry_from_filename(value->entry_, value->value_); } } static void changed(GtkEditable*, gpointer user_data) { auto value = reinterpret_cast(user_data); value->me_->notify_changed(value->id_); } static void file_set(GtkFileChooserButton*, gpointer user_data) { auto value = reinterpret_cast(user_data); value->me_->notify_changed(value->id_); } static void toggled(GtkToggleButton*, gpointer user_data) { auto value = reinterpret_cast(user_data); value->me_->notify_changed(value->id_); } virtual gint add_extra_widgets(GtkGrid*, gint row, gint) { return row; } 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: gint add_extra_widgets(GtkGrid* layout, gint row, gint colspan) override { assert(!spinner_); spinner_ = gtk_spinner_new(); gtk_grid_attach(layout, spinner_, 0, row++, colspan, 1); return row; } 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_; }; class GtkGuiMessage : public virtual GuiMessage, public GtkGuiWindow { public: GtkGuiMessage(Type type, std::string const& title, std::string const& text) : type_(type), title_(title), text_(text), dialog_(nullptr) { } ~GtkGuiMessage() override { assert(!dialog_); } void set_title(std::string const& title) override { title_ = title; GtkGuiWindow::set_title(title); } std::string const& title() const override { return title_; } void* impl() const override { return dialog_; } void add_listener(GuiMessage::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiMessage::Listener* listener) override { observers_.erase(listener); } void show(GuiWindow* parent) override { if (dialog_) { assert(false); return; } GtkMessageType type = GTK_MESSAGE_OTHER; switch (type_) { case ERROR: type = GTK_MESSAGE_ERROR; break; case OTHER: break; } dialog_ = gtk_message_dialog_new( reinterpret_cast(parent->impl()), GTK_DIALOG_MODAL, type, GTK_BUTTONS_CLOSE, "%s", text_.c_str()); gtk_dialog_run(GTK_DIALOG(dialog_)); gtk_widget_destroy(dialog_); dialog_ = nullptr; } protected: Type type_; std::string title_; std::string text_; Observers observers_; GtkWidget* dialog_; }; class GtkGuiTextWindow : public virtual GuiTextWindow, public GtkGuiWindow { public: GtkGuiTextWindow(std::string const& title, uint32_t width, uint32_t height, AttributedText const* text) : title_(title), width_(width), height_(height), text_(text), wnd_(nullptr), view_(nullptr), end_mark_(nullptr) { } ~GtkGuiTextWindow() override { disconnect_buffer(); if (wnd_) { gtk_widget_destroy(wnd_); } } void set_title(std::string const& title) override { title_ = title; GtkGuiWindow::set_title(title); } std::string const& title() const override { return title_; } void set_text(AttributedText const* text) override { if (!text) { assert(false); if (text_own_) text_own_->reset(); return; } disconnect_buffer(); text_ = text; connect_buffer(); text_own_.reset(); } void set_text(std::unique_ptr&& text) override { if (!text) { assert(false); if (text_own_) text_own_->reset(); return; } disconnect_buffer(); text_ = text.get(); connect_buffer(); text_own_.swap(text); } AttributedText const* text() const override { return text_; } void* impl() const override { return wnd_; } void add_listener(GuiTextWindow::Listener* listener) override { observers_.insert(listener); } void remove_listener(GuiTextWindow::Listener* listener) override { observers_.erase(listener); } void show(GuiWindow*) override { if (wnd_) { assert(false); focus(); return; } wnd_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); g_signal_connect(G_OBJECT(wnd_), "delete-event", G_CALLBACK(delete_event), this); gtk_window_set_default_size(GTK_WINDOW(wnd_), width_, height_); gtk_window_set_title(GTK_WINDOW(wnd_), title_.c_str()); view_ = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(view_), false); gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(view_), false); connect_buffer(); auto scroll = gtk_scrolled_window_new(nullptr, nullptr); gtk_container_add(GTK_CONTAINER(scroll), view_); gtk_container_add(GTK_CONTAINER(wnd_), scroll); gtk_widget_show_all(wnd_); } void focus() override { if (!wnd_) { assert(false); return; } gtk_window_present(GTK_WINDOW(wnd_)); } protected: bool notify_about_to_close() { auto it = observers_.notify(); while (it.has_next()) { if (!it.next()->about_to_close(this)) return false; } return true; } void disconnect_buffer() { if (!view_ || !text_) return; auto buf = static_cast(text_)->buffer(); g_signal_handler_disconnect(G_OBJECT(buf), changed_handler_); gtk_text_buffer_delete_mark(buf, end_mark_); end_mark_ = nullptr; } void scroll_to_bottom() { gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(view_), end_mark_, 0.0, true, 0.0, 1.0); } void connect_buffer() { if (!view_ || !text_) return; auto buf = static_cast(text_)->buffer(); changed_handler_ = g_signal_connect(G_OBJECT(buf), "changed", G_CALLBACK(buf_changed), this); GtkTextIter end; gtk_text_buffer_get_end_iter(buf, &end); end_mark_ = gtk_text_buffer_create_mark(buf, nullptr, &end, false); gtk_text_view_set_buffer(GTK_TEXT_VIEW(view_), buf); } static void buf_changed(GtkTextBuffer*, gpointer user_data) { auto me = reinterpret_cast(user_data); me->scroll_to_bottom(); } static gboolean delete_event(GtkWidget* widget, GdkEvent*, gpointer user_data) { auto me = reinterpret_cast(user_data); assert(me->wnd_ == widget); me->disconnect_buffer(); me->wnd_ = nullptr; auto view = me->view_; me->view_ = nullptr; if (me->notify_about_to_close()) { gtk_widget_destroy(widget); return false; } me->wnd_ = widget; me->view_ = view; me->connect_buffer(); return true; } std::string title_; uint32_t width_; uint32_t height_; AttributedText const* text_; std::unique_ptr text_own_; Observers observers_; GtkWidget* wnd_; GtkWidget* view_; GtkTextMark* end_mark_; gulong changed_handler_; }; 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_set_title(GTK_WINDOW(win), app->main_->title().c_str()); gtk_window_present(GTK_WINDOW(win)); app->main_->sync_split(); } bool main_app_window_open(MainApp* app, MainAppWindow*, GFile* file) { auto path = g_file_get_path(file); if (!path) return false; app->main_->notify_open(path); g_free(path); return true; } void main_app_open(GApplication* app, GFile** files, gint n_files, gchar const*) { auto menu = static_cast(MAIN_APP(app)->main_->menu()); if (menu) { menu->set_map(G_ACTION_MAP(app)); gtk_application_set_menubar(GTK_APPLICATION(app), menu->model()); } auto windows = gtk_application_get_windows(GTK_APPLICATION(app)); auto win = windows ? MAIN_APP_WINDOW(windows->data) : main_app_window_new(MAIN_APP(app)); gtk_window_set_title(GTK_WINDOW(win), MAIN_APP(app)->main_->title().c_str()); for (gint i = 0; i < n_files; ++i) { if (main_app_window_open(MAIN_APP(app), win, files[i])) break; } gtk_window_present(GTK_WINDOW(win)); MAIN_APP(app)->main_->sync_split(); } void main_app_init(MainApp*) { } 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*) { } void main_app_window_class_init(MainAppWindowClass*) { } 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) { GtkTextBuffer* buf; if (package_) { buf = static_cast(package_.get())->buffer(); } else { buf = gtk_text_buffer_new(NULL); } gtk_text_view_set_buffer(GTK_TEXT_VIEW(wnd->bottom_), buf); if (!package_) g_object_unref(buf); } } 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*, 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 uint8_t const GuiFile::FILE_OPEN = 0; uint8_t const GuiFile::FILE_SAVE = 1; // 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 GuiMessage* GuiMessage::create(Type type, std::string const& title, std::string const& text) { return new GtkGuiMessage(type, title, text); } // static Looper* GuiMain::createLooper() { return new GtkLooper(); } // static AttributedText* AttributedText::create() { return new GtkAttributedText(); } // static GuiTextWindow* GuiTextWindow::create(std::string const& title, uint32_t width, uint32_t height, AttributedText const* text) { return new GtkGuiTextWindow(title, width, height, text); }