From cb17c3035bbd80bd8ea6718bae4c57cfb2555653 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Thu, 15 Jun 2017 23:20:00 +0200 Subject: Initial monitor GUI Basic monitor functionality, GTK-3.0 and QT5 backends --- src/gui_gtk.cc | 1610 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1610 insertions(+) create mode 100644 src/gui_gtk.cc (limited to 'src/gui_gtk.cc') diff --git a/src/gui_gtk.cc b/src/gui_gtk.cc new file mode 100644 index 0000000..99172e3 --- /dev/null +++ b/src/gui_gtk.cc @@ -0,0 +1,1610 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include +#include +#include +#include +#include +#include + +#include "gui_about.hh" +#include "gui_form.hh" +#include "gui_formapply.hh" +#include "gui_listmodel.hh" +#include "gui_main.hh" +#include "gui_menu.hh" +#include "gui_statusbar.hh" +#include "looper.hh" +#include "observers.hh" + +namespace std { +template<> +struct hash { + 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; + } + + 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 GtkGuiMain : public virtual GuiMain, public GtkGuiWindow { +public: + GtkGuiMain(std::string const& title, uint32_t width, uint32_t height) + : title_(title), width_(width), height_(height), split_(0.5), + menu_(nullptr), statusbar_(nullptr) { + } + + void set_menu(GuiMenu* menu) override { + assert(menu); + menu_ = menu; + } + + GuiMenu* menu() const override { + return menu_; + } + + void set_statusbar(GuiStatusBar* statusbar) override { + assert(statusbar); + statusbar_ = statusbar; + } + + GuiStatusBar* statusbar() const override { + return statusbar_; + } + + void set_split(double split) override; + double split() const override; + + void set_listmodel(GuiListModel* model) override { + if (model) { + listmodel_.reset(list_model_new(model)); + } else { + listmodel_.reset(); + } + } + GuiListModel* listmodel() const override { + return listmodel_ ? list_model_get_model(listmodel_.get()) : nullptr; + } + + void set_package(std::unique_ptr&& 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; + } + + std::string title_; + uint32_t width_; + uint32_t height_; + mutable double split_; + + shared_gobject app_; + shared_gobject listmodel_; + std::unique_ptr package_; + 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_) { + 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(); +} -- cgit v1.2.3-70-g09d2