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 --- configure.ac | 19 + src/.gitignore | 4 + src/Makefile.am | 30 +- src/common.hh | 2 +- src/gui_about.hh | 33 + src/gui_attrtext.cc | 37 ++ src/gui_attrtext.hh | 155 +++++ src/gui_form.hh | 43 ++ src/gui_formapply.hh | 30 + src/gui_gtk.cc | 1610 +++++++++++++++++++++++++++++++++++++++++++++ src/gui_gtk.ccbak | 490 ++++++++++++++ src/gui_hexdump.cc | 136 ++++ src/gui_hexdump.hh | 23 + src/gui_htmlattrtext.cc | 218 ++++++ src/gui_htmlattrtext.hh | 18 + src/gui_listmodel.hh | 38 ++ src/gui_main.hh | 66 ++ src/gui_menu.hh | 42 ++ src/gui_qt.cc | 1277 +++++++++++++++++++++++++++++++++++ src/gui_statusbar.hh | 25 + src/gui_window.hh | 32 + src/monitor-gui.cc | 487 ++++++++++++++ test/.gitignore | 1 + test/Makefile.am | 5 +- test/test-htmlattrtext.cc | 38 ++ 25 files changed, 4855 insertions(+), 4 deletions(-) create mode 100644 src/gui_about.hh create mode 100644 src/gui_attrtext.cc create mode 100644 src/gui_attrtext.hh create mode 100644 src/gui_form.hh create mode 100644 src/gui_formapply.hh create mode 100644 src/gui_gtk.cc create mode 100644 src/gui_gtk.ccbak create mode 100644 src/gui_hexdump.cc create mode 100644 src/gui_hexdump.hh create mode 100644 src/gui_htmlattrtext.cc create mode 100644 src/gui_htmlattrtext.hh create mode 100644 src/gui_listmodel.hh create mode 100644 src/gui_main.hh create mode 100644 src/gui_menu.hh create mode 100644 src/gui_qt.cc create mode 100644 src/gui_statusbar.hh create mode 100644 src/gui_window.hh create mode 100644 src/monitor-gui.cc create mode 100644 test/test-htmlattrtext.cc diff --git a/configure.ac b/configure.ac index 7564309..73270bc 100644 --- a/configure.ac +++ b/configure.ac @@ -116,6 +116,25 @@ AM_CONDITIONAL([HAVE_SSL],[test "x$have_ssl" = "x1"]) AM_CONDITIONAL([HAVE_MBEDTLS],[test "x$ssl_mbedtls" = "x1"]) AM_CONDITIONAL([HAVE_OPENSSL],[test "x$ssl_openssl" = "x1"]) +# GTK +have_gtk=0 +gtk_need="gtk+-3.0 >= 3.20" +PKG_CHECK_EXISTS([$gtk_need], + [PKG_CHECK_MODULES([GTK], [$gtk_need]) + have_gtk=1]) +AM_CONDITIONAL([HAVE_GTK],[test "x$have_gtk" = "x1"]) + +# QT +have_qt=0 +qt_need="Qt5Widgets >= 5.8.0" +PKG_CHECK_EXISTS([$qt_need], + [PKG_CHECK_MODULES([QT], [$qt_need]) + PKG_CHECK_VAR([QT_CONFIG], [Qt5Core], [qt_config]) + AS_IF([echo $QT_CONFIG | grep -q reduce_relocations], + [QT_CFLAGS="$QT_CFLAGS -fPIC"]) + have_qt=1]) +AM_CONDITIONAL([HAVE_QT],[test "x$have_qt" = "x1"]) + # Finish up AC_CONFIG_HEADERS([src/config.h]) diff --git a/src/.gitignore b/src/.gitignore index 7045943..6b82970 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,8 +1,12 @@ /config.h /config.h.in~ +/libattrstr.a /libmonitor.a +/libmonitor_gui.a /libmitm.a /libtp.a /tp /tp-genca /tp-monitor +/tp-monitor-gtk +/tp-monitor-qt diff --git a/src/Makefile.am b/src/Makefile.am index c050770..37d29fa 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -6,11 +6,19 @@ AM_CXXFLAGS = @DEFINES@ ARFLAGS = cr bin_PROGRAMS = tp tp-monitor -noinst_LIBRARIES = libtp.a libmonitor.a +noinst_LIBRARIES = libtp.a libmonitor.a libattrstr.a if HAVE_SSL bin_PROGRAMS += tp-genca noinst_LIBRARIES += libmitm.a endif +if HAVE_GTK +bin_PROGRAMS += tp-monitor-gtk +noinst_LIBRARIES += libmonitor_gui.a +endif +if HAVE_QT +bin_PROGRAMS += tp-monitor-qt +noinst_LIBRARIES += libmonitor_gui.a +endif tp_SOURCES = main.cc proxy.cc logger.cc resolver.cc tp_LDADD = libtp.a @THREAD_LIBS@ @@ -41,8 +49,26 @@ tp_genca_LDADD = libmitm.a libtp.a @SSL_LIBS@ tp_genca_CXXFLAGS = $(AM_CXXFLAGS) -DVERSION='"@VERSION@"' libmonitor_a_SOURCES = monitor.cc resolver.cc -libmonitor_CXXFLAGS = $(AM_CXXFLAGS) -DVERSION='"@VERSION@"' @THREAD_CFLAGS@ +libmonitor_a_CXXFLAGS = $(AM_CXXFLAGS) -DVERSION='"@VERSION@"' @THREAD_CFLAGS@ tp_monitor_SOURCES = monitor-cmd.cc tp_monitor_LDADD = libmonitor.a libtp.a @THREAD_LIBS@ tp_monitor_CXXFLAGS = $(AM_CXXFLAGS) -DVERSION='"@VERSION@"' @THREAD_CFLAGS@ + +libmonitor_gui_a_SOURCES = monitor-gui.cc gui_hexdump.cc +libmonitor_gui_a_CXXFLAGS = $(AM_CXXFLAGS) -DVERSION='"@VERSION@"' \ + @THREAD_CFLAGS@ + +libattrstr_a_SOURCES = gui_attrtext.cc gui_htmlattrtext.cc + +tp_monitor_gtk_SOURCES = gui_gtk.cc +tp_monitor_gtk_LDADD = libmonitor_gui.a libattrstr.a libmonitor.a libtp.a \ + @GTK_LIBS@ @THREAD_LIBS@ +tp_monitor_gtk_CXXFLAGS = $(AM_CXXFLAGS) -DVERSION='"@VERSION@"' \ + @GTK_CFLAGS@ @THREAD_CFLAGS@ -Wno-unused-function + +tp_monitor_qt_SOURCES = gui_qt.cc +tp_monitor_qt_LDADD = libmonitor_gui.a libattrstr.a libmonitor.a libtp.a \ + @QT_LIBS@ @THREAD_LIBS@ +tp_monitor_qt_CXXFLAGS = $(AM_CXXFLAGS) -DVERSION='"@VERSION@"' \ + @QT_CFLAGS@ @THREAD_CFLAGS@ diff --git a/src/common.hh b/src/common.hh index 95abcaa..53045a9 100644 --- a/src/common.hh +++ b/src/common.hh @@ -13,6 +13,6 @@ # define UNUSED(x) /* x ## _unused */ #endif -#include +#include #endif // COMMON_HH diff --git a/src/gui_about.hh b/src/gui_about.hh new file mode 100644 index 0000000..5f592f0 --- /dev/null +++ b/src/gui_about.hh @@ -0,0 +1,33 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_ABOUT_HH +#define GUI_ABOUT_HH + +#include + +#include "gui_window.hh" + +class GuiAbout : public GuiWindow { +public: + class Listener : public GuiWindow::Listener { + public: + virtual ~Listener() {} + + protected: + Listener() {} + }; + + static GuiAbout* create(std::string const& title, + std::string const& version, + char const* author_name, + char const* author_email, + ...); + + virtual void add_listener(Listener* listener) = 0; + virtual void remove_listener(Listener* listener) = 0; + +protected: + GuiAbout() {} +}; + +#endif // GUI_ABOUT_HH diff --git a/src/gui_attrtext.cc b/src/gui_attrtext.cc new file mode 100644 index 0000000..ebb2f32 --- /dev/null +++ b/src/gui_attrtext.cc @@ -0,0 +1,37 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include + +#include "gui_attrtext.hh" + +void AttributedText::Attribute::add(Attribute const& attr) { + flags_ |= attr.flags_; + if (attr.has_foreground()) fg_ = attr.foreground(); + if (attr.has_background()) bg_ = attr.background(); +} + +void AttributedText::append(std::string const& str, Attribute const& attr, size_t start, size_t length) { + append(str.data(), str.size(), attr, start, length); +} + +void AttributedText::append(const char* str, Attribute const& attr, size_t start, size_t length) { + if (!str) { + assert(false); + return; + } + append(str, strlen(str), attr, start, length); +} + +// static +const uint8_t AttributedText::Attribute::BOLD = 1 << 0; +// static +const uint8_t AttributedText::Attribute::ITALIC = 1 << 1; +// static +const uint8_t AttributedText::Attribute::UNDERLINE = 1 << 2; +// static +const uint8_t AttributedText::Attribute::STRIKE = 1 << 3; + +// static +const AttributedText::Attribute AttributedText::EMPTY; diff --git a/src/gui_attrtext.hh b/src/gui_attrtext.hh new file mode 100644 index 0000000..129f905 --- /dev/null +++ b/src/gui_attrtext.hh @@ -0,0 +1,155 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_ATTRTEXT_HH +#define GUI_ATTRTEXT_HH + +#include + +class AttributedText { +public: + class Attribute { + private: + static const uint8_t BOLD; + static const uint8_t ITALIC; + static const uint8_t UNDERLINE; + static const uint8_t STRIKE; + public: + Attribute() + : fg_(0), bg_(0), flags_(0) { + } + Attribute(uint32_t foreground, uint32_t background) + : fg_(foreground), bg_(background), flags_(0) { + } + Attribute(uint8_t r, uint8_t g, uint8_t b) + : bg_(0), flags_(0) { + set_foreground(r, g, b); + } + Attribute(uint8_t fg_r, uint8_t fg_g, uint8_t fg_b, uint8_t bg_r, uint8_t bg_g, uint8_t bg_b) + : flags_(0) { + set_foreground(fg_r, fg_g, fg_b); + set_background(bg_r, bg_g, bg_b); + } + + bool operator==(Attribute const& attr) const { + return flags_ == attr.flags_ && fg_ == attr.fg_ && bg_ == attr.bg_; + } + bool operator!=(Attribute const& attr) const { + return !(*this == attr); + } + + bool bold() const { + return flags_ & BOLD; + } + void set_bold(bool value) { + set_flag(value, BOLD); + } + bool italic() const { + return flags_ & ITALIC; + } + void set_italic(bool value) { + set_flag(value, ITALIC); + } + bool underline() const { + return flags_ & UNDERLINE; + } + void set_underline(bool value) { + set_flag(value, UNDERLINE); + } + bool strike() const { + return flags_ & STRIKE; + } + void set_strike(bool value) { + set_flag(value, STRIKE); + } + + uint32_t foreground() const { + return fg_; + } + uint32_t has_foreground() const { + return has(fg_); + } + + void set_foreground(uint32_t fg) { + fg_ = fg; + if (!has_foreground()) fg_ = 0; + } + void set_foreground(uint8_t r, uint8_t g, uint8_t b) { + fg_ = set(r, g, b); + } + void clear_foreground() { + fg_ = 0; + } + + uint32_t background() const { + return bg_; + } + uint32_t has_background() const { + return has(bg_); + } + + void set_background(uint32_t bg) { + bg_ = bg; + if (!has_background()) bg_ = 0; + } + void set_background(uint8_t r, uint8_t g, uint8_t b) { + bg_ = set(r, g, b); + } + void clear_background() { + bg_ = 0; + } + + size_t hash() const { + return flags_ + ((fg_ | bg_) >> 8); + } + + void add(Attribute const& attr); + + protected: + void set_flag(bool value, uint8_t flag) { + if (value) { + flags_ |= flag; + } else { + flags_ &= ~flag; + } + } + + static bool has(uint32_t clr) { + return clr & 0xff000000; + } + + static uint32_t set(uint8_t r, uint8_t g, uint8_t b) { + return 0xff000000 | (r << 16) | (g << 8) | b; + } + + uint32_t fg_, bg_; + uint8_t flags_; + }; + + static const Attribute EMPTY; + + virtual ~AttributedText() {} + + static AttributedText* create(); + + void append(std::string const& str, Attribute const& attr = EMPTY, + size_t start = 0, size_t length = std::string::npos); + void append(const char* str, Attribute const& attr = EMPTY, + size_t start = 0, size_t length = std::string::npos); + virtual void append(const char* str, size_t len, Attribute const& attr = EMPTY, + size_t start = 0, size_t length = std::string::npos) = 0; + + virtual void add(Attribute const& attr, size_t start = 0, size_t length = std::string::npos) = 0; + virtual void set(Attribute const& attr, size_t start = 0, size_t length = std::string::npos) = 0; + virtual void clear(size_t start = 0, size_t length = std::string::npos) = 0; + + virtual std::string text() const = 0; + +protected: + AttributedText() {} + +private: + AttributedText(AttributedText const&) = delete; + AttributedText& operator=(AttributedText const&) = delete; +}; + +#endif // GUI_ATTRTEXT_HH diff --git a/src/gui_form.hh b/src/gui_form.hh new file mode 100644 index 0000000..95ec221 --- /dev/null +++ b/src/gui_form.hh @@ -0,0 +1,43 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_FORM_HH +#define GUI_FORM_HH + +#include + +#include "gui_window.hh" + +class GuiForm : public GuiWindow { +public: + class Listener : public GuiWindow::Listener { + public: + virtual ~Listener() {} + + virtual bool about_to_close(GuiForm* form); + + protected: + Listener() {} + }; + + static GuiForm* create(std::string const& title, std::string const& text); + + virtual void add_string(std::string const& id, std::string const& label, + std::string const& value) = 0; + virtual void add_number(std::string const& id, std::string const& label, + uint64_t value) = 0; + + virtual void add_listener(Listener* listener) = 0; + virtual void remove_listener(Listener* listener) = 0; + + virtual bool show(GuiWindow* parent) = 0; + + virtual void set_error(std::string const& error) = 0; + + virtual std::string get_string(std::string const& id) const = 0; + virtual uint64_t get_number(std::string const& id) const = 0; + +protected: + GuiForm() {} +}; + +#endif // GUI_FORM_HH diff --git a/src/gui_formapply.hh b/src/gui_formapply.hh new file mode 100644 index 0000000..6e4af11 --- /dev/null +++ b/src/gui_formapply.hh @@ -0,0 +1,30 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_FORMAPPLY_HH +#define GUI_FORMAPPLY_HH + +#include "gui_form.hh" + +class GuiFormApply : public virtual GuiForm { +public: + class Delegate { + public: + virtual ~Delegate() {} + + virtual void apply(GuiFormApply* connect) = 0; + + protected: + Delegate() {} + }; + + static GuiFormApply* create(std::string const& title, std::string const& text, + std::string const& apply_button, + Delegate* delegate); + + virtual void applied(bool success) = 0; + +protected: + GuiFormApply() {} +}; + +#endif // GUI_FORMAPPLY_HH 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(); +} diff --git a/src/gui_gtk.ccbak b/src/gui_gtk.ccbak new file mode 100644 index 0000000..fac8886 --- /dev/null +++ b/src/gui_gtk.ccbak @@ -0,0 +1,490 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include +#include +#include +#include + +#include "gui_about.hh" +#include "gui_main.hh" +#include "gui_menu.hh" +#include "observers.hh" + +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; +}; + +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) { + } + + void set_menu(GuiMenu* menu) override { + assert(menu); + menu_ = menu; + } + + GuiMenu* menu() const override { + return menu_; + } + + 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; + +private: + bool notify_about_to_exit() { + auto it = observers_.notify(); + while (it.has_next()) { + if (!it.next()->about_to_exit()) return false; + } + return true; + } + + std::string title_; + uint32_t width_; + uint32_t height_; + + shared_gobject app_; + Observers observers_; + GuiMenu* menu_; +}; + +class GtkGuiMenu : public GuiMenu { +public: + GtkGuiMenu() + : parent_(nullptr), menu_(g_menu_new()), map_(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; + bool enable_item(std::string const& id, bool enable) override; + +private: + explicit GtkGuiMenu(GtkGuiMenu* parent) + : parent_(parent), menu_(g_menu_new()), map_(nullptr) { + } + + 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_; + std::unordered_map> action_; + GActionMap* map_; + std::vector> submenus_; + Observers observers_; +}; + +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_; +}; + +struct _MainApp +{ + GtkApplication parent; + GtkGuiMain* main; +}; + +struct _MainAppWindow +{ + GtkApplicationWindow parent; +}; + +G_DEFINE_TYPE(MainApp, main_app, GTK_TYPE_APPLICATION); + +G_DEFINE_TYPE(MainAppWindow, main_app_window, GTK_TYPE_APPLICATION_WINDOW); + +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()); + 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)); +} + +void main_app_window_open(MainAppWindow* win, GFile* file) { +} + +void main_app_open(GApplication* app, GFile** files, gint n_files, + gchar const* 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)); +} + +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* app) { +} + +void main_app_window_class_init(MainAppWindowClass* 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 GtkGuiMenu::add_item(std::string const& id, std::string const& label) { + auto action = add_action(id); + g_menu_append(menu_.get(), label.c_str(), action.c_str()); +} + +GuiMenu* GtkGuiMenu::add_menu(std::string const& label) { + auto ret = new GtkGuiMenu(this); + g_menu_append_submenu(menu_.get(), label.c_str(), ret->model()); + submenus_.emplace_back(ret); + return ret; +} + +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* 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; +} + +} // 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 +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); +} diff --git a/src/gui_hexdump.cc b/src/gui_hexdump.cc new file mode 100644 index 0000000..fbda64d --- /dev/null +++ b/src/gui_hexdump.cc @@ -0,0 +1,136 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include + +#include "gui_attrtext.hh" +#include "gui_hexdump.hh" + +namespace { + +AttributedText::Attribute const addr(0x90, 0x90, 0x90); + +inline size_t append(char* out, size_t max, char const* in, size_t len) { + if (len <= max) { + memcpy(out, in, len); + return len; + } + return 0; +} + +inline size_t safe(char* out, size_t max, char const* in, size_t len) { + size_t ret = 0; + for (; len--; ++in) { + if (*in >= ' ' && *in < '\x7f') { + ret += append(out + ret, max - ret, in, 1); + continue; + } + switch (*in) { + case '\r': + ret += append(out + ret, max - ret, "\xe2\x86\xb5", 3); // U+21B5 + break; + case '\n': + ret += append(out + ret, max - ret, "\xc2\xb6", 2); // U+00B6 + break; + case '\x8': + ret += append(out + ret, max - ret, "\xe2\x8c\xab", 3); // U+232B + break; + case '\t': + ret += append(out + ret, max - ret, "\xe2\x86\xb9", 3); // U+21B9 + break; + default: + ret += append(out + ret, max - ret, "\xef\xbf\xbd", 3); // U+FFFD + break; + } + } + return ret; +} + +} // namespace + + +// static +void HexDump::write(AttributedText* text, uint8_t flags, std::string const& str, + size_t start, size_t length) { + if (start >= str.size()) return; + length = std::min(length, str.size() - start); + if (length == 0) return; + + AttributedText::Attribute box(0x90, 0x90, 0x90); + box.set_bold(true); + + size_t i = 0; + auto data = str.data() + start; + char tmp[80]; + char tmp2[64]; + int len; + while (i + 16 <= length) { + if (flags & ADDRESS) { + len = snprintf(tmp, sizeof(tmp), "%08lx ", static_cast(i)); + text->append(tmp, len, addr); + } + auto x = tmp2; + for (char c = 0; c < 2; ++c) { + len = snprintf(tmp, sizeof(tmp), "%02x %02x %02x %02x %02x %02x %02x %02x", + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], data[i + 5], data[i + 6], + data[i + 7]); + if (flags & CHARS) { + x += safe(x, sizeof(tmp2) - (x - tmp2), data + i, 8); + } + i += 8; + text->append(tmp, len); + if (c == 0) text->append(" ", 2); + } + if (flags & CHARS) { + text->append(" |", 3, box); + text->append(tmp2, x - tmp2); + text->append("|\n", 2, box); + } else { + text->append("\n", 1); + } + } + if (i < length) { + if (flags & ADDRESS) { + len = snprintf(tmp, sizeof(tmp), "%08lx ", static_cast(i)); + text->append(tmp, len, addr); + } + auto x = tmp2; + if (i + 8 <= length) { + len = snprintf(tmp, sizeof(tmp), "%02x %02x %02x %02x %02x %02x %02x %02x", + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], data[i + 5], data[i + 6], + data[i + 7]); + if (flags & CHARS) { + x += safe(x, sizeof(tmp2) - (x - tmp2), data + i, 8); + } + i += 8; + text->append(tmp, len); + text->append(" "); + } + for (; i < length; ++i) { + len = snprintf(tmp, sizeof(tmp), "%02x ", data[i]); + if (flags & CHARS) x += safe(x, sizeof(tmp2) - (x - tmp2), data + i, 1); + text->append(tmp, len); + } + if (flags & CHARS) { + auto extra = 16 - length % 16; + extra = extra * 3 + (extra > 8 ? 1 : 0); + memset(tmp, ' ', extra); + text->append(tmp, extra); + text->append(" |", 2, box); + text->append(tmp2, x - tmp2); + text->append("|\n", 2, box); + } else { + text->append("\n", 1); + } + if (flags & ADDRESS) { + len = snprintf(tmp, sizeof(tmp), "%08lx\n", static_cast(length)); + text->append(tmp, len, addr); + } + } +} + +// static +uint8_t const HexDump::ADDRESS = 1 << 0; +// static +uint8_t const HexDump::CHARS = 1 << 1; diff --git a/src/gui_hexdump.hh b/src/gui_hexdump.hh new file mode 100644 index 0000000..d3c41cf --- /dev/null +++ b/src/gui_hexdump.hh @@ -0,0 +1,23 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_HEXDUMP_HH +#define GUI_HEXDUMP_HH + +#include + +class AttributedText; + +class HexDump { +public: + static uint8_t const ADDRESS; + static uint8_t const CHARS; + + static void write(AttributedText* text, uint8_t flags, std::string const& data, + size_t start = 0, size_t length = std::string::npos); + +private: + ~HexDump() {} + HexDump() {} +}; + +#endif // GUI_HEXDUMP_HH diff --git a/src/gui_htmlattrtext.cc b/src/gui_htmlattrtext.cc new file mode 100644 index 0000000..24380af --- /dev/null +++ b/src/gui_htmlattrtext.cc @@ -0,0 +1,218 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include +#include +#include +#include + +#include "gui_htmlattrtext.hh" + +namespace { + +char const* ESCAPE = "&<>\"\n "; + +void append_escaped(std::string* out, char const* in, size_t len) { + size_t last = 0; + for (size_t i = 0; i < len; ++i) { + auto tmp = reinterpret_cast(memchr(ESCAPE, in[i], 6)); + if (!tmp) continue; + if (last < i) { + out->append(in + last, i - last); + } + last = i + 1; + switch (tmp - ESCAPE) { + case 0: + out->append("&"); + break; + case 1: + out->append("<"); + break; + case 2: + out->append(">"); + break; + case 3: + out->append("""); + break; + case 4: + out->append("
"); + break; + case 5: + out->append(" "); + break; + } + } + if (last < len) { + out->append(in + last, len - last); + } +} + +void color(std::string* out, uint32_t color) { + char tmp[30]; + if (color >> 24 == 0xff) { + out->append(tmp, snprintf(tmp, sizeof(tmp), "rgb(%u, %u, %u)", + static_cast((color >> 16) & 0xff), + static_cast((color >> 8) & 0xff), + static_cast(color & 0xff))); + } else { + out->append(tmp, snprintf(tmp, sizeof(tmp), "rgba(%u, %u, %u, %u)", + static_cast((color >> 16) & 0xff), + static_cast((color >> 8) & 0xff), + static_cast(color & 0xff), + static_cast(color >> 24))); + } +} + +class HtmlAttributedTextImpl : public HtmlAttributedText { +public: + void append(const char* str, size_t len, Attribute const& attr, size_t start, size_t length) override { + if (!str || start >= len) return; + length = std::min(len - start, length); + if (length == 0) return; + auto offset = text_.size(); + text_.append(str + start, length); + if (attr != EMPTY) ranges_.emplace_back(attr, offset, offset + length); + } + + void add(Attribute const& attr, size_t start, size_t length) override { + if (attr == EMPTY) return; + auto end = check_end(start, length); + if (end == 0) return; + Range r(attr, start, end); + auto it = std::lower_bound(ranges_.begin(), ranges_.end(), r); + if (it != ranges_.end() && it->start_ == r.start_ && it->end_ == r.end_) { + it->attr_.add(r.attr_); + return; + } + ranges_.insert(it, r); + } + void set(Attribute const& attr, size_t start, size_t length) override { + clear(start, length); + add(attr, start, length); + } + void clear(size_t start, size_t length) override { + auto end = check_end(start, length); + if (end == 0) return; + auto it = ranges_.begin(); + while (it != ranges_.end()) { + if (it->start_ >= end) break; + if (it->end_ <= start) { + ++it; + continue; + } + if (it->start_ == start && it->end_ == end) { + it = ranges_.erase(it); + continue; + } + if (start > it->start_ && end < it->end_) { + auto const e = it->end_; + it->end_ = start; + it = ranges_.emplace(it + 1, it->attr_, end, e); + } else if (start <= it->start_) { + it->start_ = end; + } else { + assert(end >= it->end_); + it->end_ = start; + } + ++it; + } + } + + std::string html() const override { + std::string ret; + std::deque stack; + size_t last = 0; + for (auto const& range : ranges_) { + while (!stack.empty() && stack.front()->end_ <= range.start_) { + if (stack.front()->end_ > last) { + append_escaped(&ret, text_.data() + last, stack.front()->end_ - last); + last = stack.front()->end_; + } + end_tag(&ret, stack.front()->attr_); + stack.pop_front(); + } + if (range.start_ > last) { + append_escaped(&ret, text_.data() + last, range.start_ - last); + last = range.start_; + } + start_tag(&ret, range.attr_); + auto it = std::lower_bound(stack.begin(), stack.end(), &range, [](Range const* r1, Range const* r2) -> bool { + return r1->end_ < r2->end_; + }); + stack.emplace(it, &range); + } + while (!stack.empty()) { + if (stack.front()->end_ > last) { + append_escaped(&ret, text_.data() + last, stack.front()->end_ - last); + last = stack.front()->end_; + } + end_tag(&ret, stack.front()->attr_); + stack.pop_front(); + } + if (last < text_.size()) { + append_escaped(&ret, text_.data() + last, text_.size() - last); + } + return ret; + } + + std::string text() const override { + return text_; + } + +private: + struct Range { + size_t start_; + size_t end_; + Attribute attr_; + + Range(Attribute const& attr, size_t start, size_t end) + : start_(start), end_(end), attr_(attr) { + assert(start_ < end_); + } + + bool operator<(Range const& range) const { + return start_ < range.start_; + } + }; + + size_t check_end(size_t start, size_t length) const { + if (start >= text_.size()) return 0; + length = std::min(text_.size() - start, length); + if (length == 0) return 0; + return start + length; + } + + static void start_tag(std::string* out, Attribute const& attr) { + out->append("append("font-weight: bold; "); + if (attr.italic()) out->append("font-style: italic; "); + if (attr.underline()) out->append("text-decoration: underline; "); + if (attr.strike()) out->append("text-decoration: line-through; "); + if (attr.has_foreground()) { + out->append("color: "); + color(out, attr.foreground()); + out->append("; "); + } + if (attr.has_background()) { + out->append("background-color: "); + color(out, attr.background()); + out->append("; "); + } + out->append("\">"); + } + + static void end_tag(std::string* out, Attribute const& UNUSED(attr)) { + out->append(""); + } + + std::string text_; + std::vector ranges_; +}; + +} // namespace + +// static +HtmlAttributedText* HtmlAttributedText::create() { + return new HtmlAttributedTextImpl(); +} diff --git a/src/gui_htmlattrtext.hh b/src/gui_htmlattrtext.hh new file mode 100644 index 0000000..91e1cc1 --- /dev/null +++ b/src/gui_htmlattrtext.hh @@ -0,0 +1,18 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_HTMLATTRTEXT_HH +#define GUI_HTMLATTRTEXT_HH + +#include "gui_attrtext.hh" + +class HtmlAttributedText : public AttributedText { +public: + static HtmlAttributedText* create(); + + virtual std::string html() const = 0; + +protected: + HtmlAttributedText() {} +}; + +#endif // GUI_HTMLATTRTEXT_HH diff --git a/src/gui_listmodel.hh b/src/gui_listmodel.hh new file mode 100644 index 0000000..571924c --- /dev/null +++ b/src/gui_listmodel.hh @@ -0,0 +1,38 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_LISTMODEL_HH +#define GUI_LISTMODEL_HH + +#include + +class GuiListModel { +public: + class Listener { + public: + virtual ~Listener() {} + + virtual void rows_added(GuiListModel* model, size_t first, size_t last) = 0; + virtual void rows_changed(GuiListModel* model, size_t first, size_t last) = 0; + virtual void rows_removed(GuiListModel* model, size_t first, size_t last) = 0; + + protected: + Listener() {} + }; + + virtual ~GuiListModel() {} + + virtual size_t rows() const = 0; + virtual size_t columns() const = 0; + + virtual std::string header(size_t column) const = 0; + virtual std::string data(size_t row, size_t column) const = 0; + + virtual void add_listener(Listener* listener) = 0; + virtual void remove_listener(Listener* listener) = 0; + +protected: + GuiListModel() {} + GuiListModel(GuiListModel const&) = delete; +}; + +#endif // GUI_LISTMODEL_HH diff --git a/src/gui_main.hh b/src/gui_main.hh new file mode 100644 index 0000000..af35106 --- /dev/null +++ b/src/gui_main.hh @@ -0,0 +1,66 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_MAIN_HH +#define GUI_MAIN_HH + +#include +#include +#include + +#include "gui_attrtext.hh" +#include "gui_window.hh" + +class GuiListModel; +class GuiMenu; +class GuiStatusBar; +class Looper; + +class GuiMain : public GuiWindow { +public: + class Listener : public GuiWindow::Listener { + public: + virtual ~Listener() {} + + virtual bool about_to_exit(GuiMain* main) = 0; + virtual void selected_row(GuiMain* main, size_t index) = 0; + virtual void lost_selection(GuiMain* main) = 0; + + protected: + Listener() {} + }; + + static GuiMain* create(std::string const& title, + uint32_t width, uint32_t height); + static Looper* createLooper(); + + virtual void set_menu(GuiMenu* menu) = 0; + virtual GuiMenu* menu() const = 0; + + virtual void set_statusbar(GuiStatusBar* statusbar) = 0; + virtual GuiStatusBar* statusbar() const = 0; + + virtual void set_split(double split) = 0; + virtual double split() const = 0; + + virtual void set_listmodel(GuiListModel* model) = 0; + virtual GuiListModel* listmodel() const = 0; + + virtual void set_package(std::unique_ptr&& data) = 0; + + virtual bool run(int argc, char** argv) = 0; + + virtual bool exit() = 0; + + virtual void show(GuiWindow* window) = 0; + + virtual void add_listener(Listener* listener) = 0; + virtual void remove_listener(Listener* listener) = 0; + + virtual void add_to_clipboard(std::string const& data, + std::string const& mimetype = "") = 0; + +protected: + GuiMain() {} +}; + +#endif // GUI_MAIN_HH diff --git a/src/gui_menu.hh b/src/gui_menu.hh new file mode 100644 index 0000000..e2af274 --- /dev/null +++ b/src/gui_menu.hh @@ -0,0 +1,42 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_MENU_HH +#define GUI_MENU_HH + +#include + +class GuiMenu { +public: + class Listener { + public: + virtual ~Listener() {} + + virtual void item_activated(std::string const& id) = 0; + + protected: + Listener() {} + }; + + virtual ~GuiMenu() {} + + static GuiMenu* create(); + + virtual void add_item(std::string const& id, std::string const& label) = 0; + // The returned menu lives as long as the root-menu, do not free + // the pointer yourself + virtual GuiMenu* add_menu(std::string const& label) = 0; + virtual void add_separator() = 0; + + // Call to enable/disable menu item, searches sub menues if no such + // item is found in this menu. Returns true if an item was found. + virtual bool enable_item(std::string const& id, bool enable = true) = 0; + + virtual void add_listener(Listener* listener) = 0; + virtual void remove_listener(Listener* listener) = 0; + +protected: + GuiMenu() {} + GuiMenu(GuiMenu const&) = delete; +}; + +#endif // GUI_MENU_HH diff --git a/src/gui_qt.cc b/src/gui_qt.cc new file mode 100644 index 0000000..dcfd320 --- /dev/null +++ b/src/gui_qt.cc @@ -0,0 +1,1277 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gui_about.hh" +#include "gui_form.hh" +#include "gui_formapply.hh" +#include "gui_htmlattrtext.hh" +#include "gui_listmodel.hh" +#include "gui_main.hh" +#include "gui_menu.hh" +#include "gui_statusbar.hh" +#include "looper.hh" +#include "observers.hh" + +namespace { + +class SizeHintLayout : public QLayout { +public: + SizeHintLayout(QWidget* parent, QSize hint) + : QLayout(parent), hint_(hint), item_(nullptr) { + } + + ~SizeHintLayout() override { + while (true) { + auto l = takeAt(0); + if (!l) break; + delete l; + } + } + + void addItem(QLayoutItem *item) override { + assert(!item_); + item_ = item; + } + + int count() const override { + return item_ ? 1 : 0; + } + + QLayoutItem *itemAt(int index) const override { + if (item_ && index == 0) { + return item_; + } + return nullptr; + } + + Qt::Orientations expandingDirections() const override { + return item_ ? item_->expandingDirections() + : QLayout::expandingDirections(); + } + + QSize minimumSize() const override { + return item_ ? item_->minimumSize() : QLayout::minimumSize(); + } + + bool hasHeightForWidth() const override { + return item_ ? item_->hasHeightForWidth() : QLayout::hasHeightForWidth(); + } + + void setGeometry(const QRect &rect) override { + QLayout::setGeometry(rect); + if (item_) { + item_->setGeometry(rect); + } + } + + QSize sizeHint() const override { + return hint_; + } + + QLayoutItem *takeAt(int index) override { + if (item_ && index == 0) { + auto ret = item_; + item_ = nullptr; + return ret; + } + return nullptr; + } + +private: + QSize const hint_; + QLayoutItem* item_; +}; + +class QtGuiWindow : public GuiWindow { +public: + void set_title(std::string const& title) override { + auto w = widget(); + if (w) { + w->setWindowTitle(QString::fromStdString(title)); + } + } + + virtual QWidget* widget() const { + return nullptr; + } + + virtual bool showWidget() { + return false; + } + + void* impl() const override { + return const_cast(this); + } +}; + +class QtGuiStatusBar : public GuiStatusBar { +public: + QtGuiStatusBar(); + + void set_status(std::string const& str) override; + void set_override(std::string const& str) override; + + void setup(QStatusBar* statusbar); + +private: + std::string status_; + std::string override_; + QStatusBar* statusbar_; + QLabel* label_; +}; + +class QtChildGuiMenu; + +class QtCommonMenu : public GuiMenu { +public: + QtCommonMenu(QtCommonMenu* parent, std::string const& label) + : parent_(parent), label_(label) { + } + + QtCommonMenu() + : parent_(nullptr) { + } + + void add_item(std::string const& id, std::string const& label) override { + item_.emplace_back(id, label); + } + + void add_separator() override { + item_.emplace_back("", ""); + } + + GuiMenu* add_menu(std::string const& label) override; + + bool enable_item(const std::string& id, bool enable) override { + if (parent_) return parent_->enable_item(id, enable) || find(id); + if (enable) { + disabled_.erase(id); + } else { + disabled_.insert(id); + } + return find(id); + } + + virtual void add_action(std::string const& id, QAction* action) { + parent_->add_action(id, action); + } + + std::string const& label() const { + return label_; + } + +protected: + bool find(const std::string& id) const { + for (auto const& pair : item_) { + if (pair.first == id) return true; + } + return false; + } + + QtCommonMenu* const parent_; + std::string const label_; + std::vector> item_; + std::unordered_set disabled_; + std::vector> children_; +}; + +class QtGuiMenu : public QtCommonMenu { +public: + QtGuiMenu(); + + void add_item(std::string const& id, std::string const& label) override; + void add_separator() override; + GuiMenu* add_menu(std::string const& label) override; + bool enable_item(const std::string& id, bool enable) override; + + void add_listener(Listener* listener) override; + void remove_listener(Listener* listener) override; + + void add_action(std::string const& id, QAction* action) override; + + void setup(QMenuBar* menubar); + +private: + void notify_item_activated(std::string const& id); + + Observers observers_; + QMenuBar* menubar_; + std::unordered_map action_; +}; + +class QGuiListModel : public QAbstractItemModel, GuiListModel::Listener { +public: + QGuiListModel(GuiListModel* model) + : model_(model) { + model_->add_listener(this); + } + + ~QGuiListModel() override { + model_->remove_listener(this); + } + + void rows_added(GuiListModel*, size_t first, size_t last) override { + beginInsertRows(QModelIndex(), first, last); + endInsertRows(); + } + + void rows_changed(GuiListModel*, size_t first, size_t last) override { + emit dataChanged(createIndex(first, 0), createIndex(last, model_->columns())); + } + + void rows_removed(GuiListModel*, size_t first, size_t last) override { + beginRemoveRows(QModelIndex(), first, last); + endRemoveRows(); + } + + GuiListModel* model() const { + return model_; + } + + bool hasChildren(const QModelIndex& parent) const override { + return !parent.isValid(); + } + + Qt::ItemFlags flags(const QModelIndex& index) const override { + auto flags = QAbstractItemModel::flags(index); + if (index.isValid()) flags |= Qt::ItemNeverHasChildren; + return flags; + } + + QModelIndex index(int row, int column, const QModelIndex& parent) const override { + if (!parent.isValid() + && row >= 0 && static_cast(row) < model_->rows() + && column >= 0 && static_cast(column) < model_->columns()) { + return createIndex(row, column); + } + return QModelIndex(); + } + + QModelIndex parent(const QModelIndex& UNUSED(index)) const override { + return QModelIndex(); + } + + int rowCount(const QModelIndex& parent) const override { + if (parent.isValid()) return 0; + return model_->rows(); + } + + int columnCount(const QModelIndex& parent = QModelIndex()) const override { + if (parent.isValid()) return 0; + return model_->columns(); + } + + QVariant data(const QModelIndex& index, int role) const override { + if (index.isValid() && role == Qt::DisplayRole + && index.row() >= 0 && static_cast(index.row()) < model_->rows() + && index.column() >= 0 && static_cast(index.column()) < model_->columns()) { + return QVariant(QString::fromStdString(model_->data(index.row(), index.column()))); + } + return QVariant(); + } + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override { + if (orientation == Qt::Horizontal && role == Qt::DisplayRole + && section >= 0 && static_cast(section) < model_->columns()) { + return QVariant(QString::fromStdString(model_->header(section))); + } + return QVariant(); + } + +private: + GuiListModel* const model_; +}; + +class QtGuiMain : public GuiMain, public QtGuiWindow { +public: + QtGuiMain(std::string const& title, uint32_t width, uint32_t height) + : title_(title), width_(width), height_(height), split_(0.7), + menu_(nullptr), statusbar_(nullptr), splitter_(nullptr), + top_(nullptr), bottom_(nullptr) { + } + + void set_menu(GuiMenu* menu) override { + menu_ = static_cast(menu); + } + + GuiMenu* menu() const override { + return menu_; + } + + void set_statusbar(GuiStatusBar* statusbar) override { + statusbar_ = static_cast(statusbar); + } + + GuiStatusBar* statusbar() const override { + return statusbar_; + } + + void set_split(double split) override { + split_ = std::max(0.0, std::min(split, 1.0)); + if (splitter_) { + QList sizes; + auto total = splitter_->size().height(); + sizes << round(total * split_); + sizes << total - sizes.front(); + splitter_->setSizes(sizes); + } + } + + double split() const override { + if (splitter_) { + auto sizes = splitter_->sizes(); + if (!sizes.empty()) { + split_ = static_cast(splitter_->size().height()) / sizes.front(); + } + } + return split_; + } + + void set_listmodel(GuiListModel* listmodel) override { + if (listmodel) { + listmodel_.reset(new QGuiListModel(listmodel)); + } else { + listmodel_.reset(); + } + } + + GuiListModel* listmodel() const override { + return listmodel_ ? listmodel_->model() : nullptr; + } + + void set_package(std::unique_ptr&& text) override { + if (text) { + package_ = "" + + static_cast(text.get())->html() + ""; + } else { + package_.clear(); + } + if (bottom_) { + bottom_->setHtml(QString::fromStdString(package_)); + } + } + + void add_listener(GuiMain::Listener* listener) override { + observers_.insert(listener); + } + + void remove_listener(GuiMain::Listener* listener) override { + observers_.erase(listener); + } + + void set_title(std::string const& title) override { + title_ = title; + QtGuiWindow::set_title(title); + } + + void* impl() const override { + return QtGuiWindow::impl(); + } + + QWidget* widget() const override { + return main_.get(); + } + + bool run(int argc, char** argv) override; + + void show(GuiWindow* window) override { + auto wnd = static_cast(window->impl()); + auto widget = wnd->widget(); + if (!widget) { + if (!wnd->showWidget()) { + assert(false); + } + return; + } + widget->show(); + } + + bool exit() override { + if (!notify_about_to_exit()) return false; + QApplication::exit(); + return true; + } + + void add_to_clipboard(std::string const& data, + std::string const& mimetype) override { + auto clipboard = QApplication::clipboard(); + if (mimetype.empty() || valid_utf8(data)) { + clipboard->setText(QString::fromStdString(data)); + return; + } + auto mimedata = new QMimeData(); + mimedata->setData(QString::fromStdString(mimetype), + QByteArray(data.data(), data.size())); + clipboard->setMimeData(mimedata); + } + +private: + static bool valid_utf8(std::string const& data) { + QTextCodec::ConverterState state; + QTextCodec* codec = QTextCodec::codecForName("UTF-8"); + const QString text = codec->toUnicode(data.data(), data.size(), &state); + return state.invalidChars == 0; + } + + bool notify_about_to_exit() { + auto it = observers_.notify(); + while (it.has_next()) { + if (!it.next()->about_to_exit(this)) return false; + } + return true; + } + + 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); + } + } + + std::string title_; + uint32_t width_; + uint32_t height_; + mutable double split_; + QtGuiMenu* menu_; + QtGuiStatusBar* statusbar_; + std::unique_ptr listmodel_; + std::string package_; + Observers observers_; + std::unique_ptr main_; + std::unique_ptr layout_; + std::unique_ptr center_; + QSplitter* splitter_; + QWidget* top_; + QTextEdit* bottom_; +}; + +class QtChildGuiMenu : public QtCommonMenu { +public: + QtChildGuiMenu(QtCommonMenu* parent, std::string const& label) + : QtCommonMenu(parent, label), menu_(nullptr) { + } + + void add_item(std::string const& id, std::string const& label) override { + if (!menu_) { + QtCommonMenu::add_item(id, label); + return; + } + auto action = menu_->addAction(QString::fromStdString(label)); + parent_->add_action(id, action); + } + + void add_separator() override { + if (!menu_) { + QtCommonMenu::add_separator(); + return; + } + menu_->addSeparator(); + } + + GuiMenu* add_menu(std::string const& label) override { + auto menu = static_cast(QtCommonMenu::add_menu(label)); + if (menu_) { + menu->setup(menu_->addMenu(QString::fromStdString(menu->label()))); + } + return menu; + } + + void add_listener(Listener* listener) override { + parent_->add_listener(listener); + } + + void remove_listener(Listener* listener) override { + parent_->remove_listener(listener); + } + + void setup(QMenu* menu) { + assert(!menu_); + assert(menu); + menu_ = menu; + + for (auto const& pair : item_) { + if (pair.first.empty() && pair.second.empty()) { + menu_->addSeparator(); + continue; + } + auto action = menu_->addAction(QString::fromStdString(pair.second)); + parent_->add_action(pair.first, action); + } + item_.clear(); + + for (auto& child : children_) { + child->setup(menu_->addMenu(QString::fromStdString(child->label()))); + } + } + +private: + QMenu* menu_; +}; + +GuiMenu* QtCommonMenu::add_menu(std::string const& label) { + auto child = new QtChildGuiMenu(this, label); + children_.emplace_back(child); + return child; +} + +QtGuiMenu::QtGuiMenu() + : QtCommonMenu(), menubar_(nullptr) { +} + +void QtGuiMenu::add_item(std::string const& id, std::string const& label) { + if (!menubar_) { + QtCommonMenu::add_item(id, label); + return; + } + add_action(id, menubar_->addAction(QString::fromStdString(label))); +} + +void QtGuiMenu::add_separator() { + if (!menubar_) { + QtCommonMenu::add_separator(); + return; + } + menubar_->addSeparator(); +} + +GuiMenu* QtGuiMenu::add_menu(std::string const& label) { + auto menu = static_cast(QtCommonMenu::add_menu(label)); + if (menubar_) { + menu->setup(menubar_->addMenu(QString::fromStdString(label))); + } + return menu; +} + +bool QtGuiMenu::enable_item(const std::string& id, bool enable) { + if (!menubar_) { + return QtCommonMenu::enable_item(id, enable); + } + auto pair = action_.find(id); + if (pair == action_.end()) return false; + pair->second->setEnabled(enable); + return true; +} + +void QtGuiMenu::add_listener(Listener* listener) { + observers_.insert(listener); +} + +void QtGuiMenu::remove_listener(Listener* listener) { + observers_.erase(listener); +} + +void QtGuiMenu::notify_item_activated(std::string const& id) { + auto it = observers_.notify(); + while (it.has_next()) { + it.next()->item_activated(id); + } +} + +void QtGuiMenu::add_action(std::string const& id, QAction* action) { + action_.emplace(id, action); + QObject::connect(action, &QAction::triggered, + [=](bool) { notify_item_activated(id); }); +} + +void QtGuiMenu::setup(QMenuBar* menubar) { + assert(menubar); + assert(!menubar_); + menubar_ = menubar; + for (auto const& pair : item_) { + auto action = menubar_->addAction(QString::fromStdString(pair.second)); + add_action(pair.first, action); + } + item_.clear(); + + for (auto& child : children_) { + child->setup(menubar_->addMenu(QString::fromStdString(child->label()))); + } + + for (auto const& id : disabled_) { + auto it = action_.find(id); + if (it == action_.end()) { + assert(false); + continue; + } + it->second->setEnabled(false); + } + disabled_.clear(); +} + +QtGuiStatusBar::QtGuiStatusBar() + : statusbar_(nullptr), label_(nullptr) { +} + +void QtGuiStatusBar::setup(QStatusBar* statusbar) { + if (!statusbar) { + statusbar_ = nullptr; + label_ = nullptr; + return; + } + label_ = new QLabel(); + label_->setText(QString::fromStdString(status_)); + statusbar_ = statusbar; + statusbar_->addWidget(label_, 1); + if (override_.empty()) { + statusbar_->clearMessage(); + } else { + statusbar_->showMessage(QString::fromStdString(override_)); + } +} + +void QtGuiStatusBar::set_status(std::string const& str) { + status_ = str; + if (label_) label_->setText(QString::fromStdString(str)); +} + +void QtGuiStatusBar::set_override(std::string const& str) { + override_ = str; + if (statusbar_) { + if (override_.empty()) { + statusbar_->clearMessage(); + } else { + statusbar_->showMessage(QString::fromStdString(override_)); + } + } +} + +class QtGuiAbout : public virtual GuiAbout, public QtGuiWindow { +public: + QtGuiAbout(std::string const& title, std::string const& version, + std::vector const& authors) { + title_ = title + " " + version; + auto it = authors.begin(); + while (it != authors.end()) { + text_ += *it++; + if (!it->empty()) { + text_.push_back(' '); + text_ += *it; + } + ++it; + text_.push_back('\n'); + } + } + + void add_listener(GuiAbout::Listener* listener) override { + observers_.insert(listener); + } + + void remove_listener(GuiAbout::Listener* listener) override { + observers_.erase(listener); + } + + void set_title(std::string const& title) override { + title_ = title; + } + + void* impl() const override { + return QtGuiWindow::impl(); + } + + bool showWidget() override { + QMessageBox::about(QApplication::activeWindow(), + QString::fromStdString(title_), + QString::fromStdString(text_)); + return true; + } + +private: + std::string title_; + std::string text_; + Observers observers_; +}; + +class OptionalCloseDialog : public QDialog { +public: + class Delegate { + public: + virtual ~Delegate() {} + + virtual bool about_to_close() = 0; + + protected: + Delegate() {} + }; + + OptionalCloseDialog(QWidget* parent, Delegate* delegate) + : QDialog(parent), delegate_(delegate) { + } + + void done(int result) override { + if (result == QDialog::Accepted) { + if (!delegate_->about_to_close()) return; + } + QDialog::done(result); + } + +private: + Delegate* const delegate_; +}; + +class QtGuiForm : public virtual GuiForm, public QtGuiWindow, + public virtual OptionalCloseDialog::Delegate { +public: + QtGuiForm(std::string const& title, std::string const& text) + : title_(title), text_(text), dialog_(nullptr), error_(nullptr), + buttons_(nullptr) { + } + + ~QtGuiForm() { + assert(!dialog_); + } + + void add_listener(GuiForm::Listener* listener) override { + observers_.insert(listener); + } + + void remove_listener(GuiForm::Listener* listener) override { + observers_.erase(listener); + } + + void set_title(std::string const& title) override { + title_ = title; + QtGuiWindow::set_title(title); + } + + QWidget* widget() const override { + return dialog_; + } + + void* impl() const override { + return QtGuiWindow::impl(); + } + + bool showWidget() override { + show(QApplication::activeWindow()); + return true; + } + + bool show(GuiWindow* parent) override { + auto wnd = static_cast(parent->impl()); + return show(wnd->widget()); + } + + 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->edit_) { + return value->edit_->text().toStdString(); + } + 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->edit_) { + uint64_t tmp; + if (get_number(value->edit_, &tmp)) { + return tmp; + } + } + 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()) { + error_->hide(); + } else { + error_->setText(QString::fromStdString(error)); + error_->show(); + } + } + + bool about_to_close() override { + return notify_about_to_close(); + } + +protected: + enum Type { + STRING, + NUMBER, + }; + + struct Value { + Type const type_; + std::string const id_; + std::string const label_; + QLineEdit* edit_; + + Value(Type type, std::string const& id, std::string const& label) + : type_(type), id_(id), label_(label), edit_(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) { + } + }; + + static bool get_number(QLineEdit* edit, uint64_t* number) { + assert(edit && number); + auto text = edit->text().toStdString(); + errno = 0; + char* end = nullptr; + auto tmp = strtoull(text.c_str(), &end, 10); + if (errno && end && !*end) { + if (number) *number = tmp; + return true; + } + return false; + } + + bool show(QWidget* parent) { + dialog_ = new OptionalCloseDialog(parent, this); + dialog_->setWindowTitle(QString::fromStdString(title_)); + auto layout = new QGridLayout(); + int row = 0; + if (!text_.empty()) { + auto text = new QLabel(QString::fromStdString(text_)); + layout->addWidget(text, row++, 0, 1, 2); + } + for (auto& value : values_) { + auto label = new QLabel(QString::fromStdString(value->label_)); + auto edit = new QLineEdit(); + value->edit_ = edit; + switch (value->type_) { + case STRING: + edit->setText(QString::fromStdString( + static_cast(value.get())->value_)); + break; + case NUMBER: { + char tmp[20]; + snprintf(tmp, sizeof(tmp), "%llu", static_cast( + static_cast(value.get())->value_)); + edit->setText(tmp); + edit->setValidator(new QIntValidator( + 0, std::numeric_limits::max(), edit)); + } + } + layout->addWidget(label, row, 0); + layout->addWidget(edit, row, 1); + ++row; + } + error_ = new QLabel(); + layout->addWidget(error_, row++, 0, 1, 2); + error_->hide(); + buttons_ = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel); + dialog_->connect(buttons_, &QDialogButtonBox::accepted, + dialog_, &QDialog::accept); + dialog_->connect(buttons_, &QDialogButtonBox::rejected, + dialog_, &QDialog::reject); + layout->addWidget(buttons_, row++, 0, 1, 2); + add_extra_widgets(layout, row); + dialog_->setLayout(layout); + auto ret = dialog_->exec(); + for (auto& value : values_) { + switch (value->type_) { + case STRING: + static_cast(value.get())->value_ = + value->edit_->text().toStdString(); + break; + case NUMBER: + get_number(value->edit_, + &static_cast(value.get())->value_); + } + value->edit_ = nullptr; + } + delete dialog_; + reset_extra_widgets(); + dialog_ = nullptr; + error_ = nullptr; + buttons_ = nullptr; + return ret == QDialog::Accepted; + } + + 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; + } + + virtual int add_extra_widgets(QGridLayout*, int row) { + return row; + } + + virtual void reset_extra_widgets() { + } + + std::string title_; + std::string text_; + Observers observers_; + std::vector> values_; + QDialog* dialog_; + QLabel* error_; + QDialogButtonBox* buttons_; +}; + +class QtGuiFormApply : public QtGuiForm, public virtual GuiFormApply { +public: + QtGuiFormApply(std::string const& title, std::string const& text, + std::string const& apply_button, + GuiFormApply::Delegate* delegate) + : QtGuiForm(title, text), apply_button_(apply_button), + delegate_(delegate), progress_(nullptr), applied_(false) { + } + + void applied(bool success) override { + assert(progress_); + if (progress_) { + progress_->hide(); + } + assert(buttons_); + if (buttons_) { + auto ok = buttons_->button(QDialogButtonBox::Ok); + ok->setEnabled(true); + } + if (success) { + applied_ = true; + dialog_->done(QDialog::Accepted); + } + } + +private: + int add_extra_widgets(QGridLayout* layout, int row) override { + assert(buttons_); + if (buttons_) { + auto ok = buttons_->button(QDialogButtonBox::Ok); + ok->setText(QString::fromStdString(apply_button_)); + } + assert(!progress_); + progress_ = new QProgressBar(); + progress_->setMinimum(0); + progress_->setMaximum(0); + layout->addWidget(progress_, row++, 0, 1, 2); + progress_->hide(); + return row; + } + + void reset_extra_widgets() override { + progress_ = nullptr; + } + + bool notify_about_to_close() override { + if (applied_) { + applied_ = false; + return true; + } + if (!QtGuiForm::notify_about_to_close()) return false; + assert(progress_); + if (progress_) { + progress_->show(); + } + assert(buttons_); + if (buttons_) { + auto ok = buttons_->button(QDialogButtonBox::Ok); + ok->setEnabled(false); + } + delegate_->apply(this); + return false; + } + + std::string apply_button_; + GuiFormApply::Delegate* const delegate_; + QProgressBar* progress_; + bool applied_; +}; + +bool QtGuiMain::run(int argc, char** argv) { + QApplication app(argc, argv); + app.setStyleSheet("QStatusBar::item { border: 0px }"); + main_.reset(new QMainWindow()); + center_.reset(new QWidget()); + layout_.reset(new SizeHintLayout(center_.get(), QSize(width_, height_))); + splitter_ = new QSplitter(Qt::Vertical, main_.get()); + center_->layout()->addWidget(splitter_); + if (listmodel_) { + auto tree = new QTreeView(); + tree->setModel(listmodel_.get()); + tree->setUniformRowHeights(true); + tree->setSelectionMode(QAbstractItemView::SingleSelection); + QObject::connect(tree->selectionModel(), &QItemSelectionModel::selectionChanged, + [=](QItemSelection const& selected, QItemSelection const& UNUSED(previous)) { + if (selected.size() == 1) { + notify_selected_row(selected.indexes()[0].row()); + } else { + notify_lost_selection(); + } + }); + tree->header()->setSectionsMovable(false); + auto columns = listmodel_->columnCount(); + if (columns > 0) { + tree->header()->setSectionResizeMode(columns - 1, QHeaderView::Stretch); + } + top_ = tree; + } else { + top_ = new QWidget(); + } + splitter_->addWidget(top_); + bottom_ = new QTextEdit(); + bottom_->setReadOnly(true); + bottom_->setHtml(QString::fromStdString(package_)); + splitter_->addWidget(bottom_); + main_->setCentralWidget(center_.get()); + if (menu_) { + menu_->setup(main_->menuBar()); + } + if (statusbar_) { + statusbar_->setup(main_->statusBar()); + } + main_->show(); + set_split(split_); + auto ret = app.exec() == 0; + layout_.reset(); + top_ = nullptr; + bottom_ = nullptr; + splitter_ = nullptr; + center_.reset(); + main_.reset(); // Make sure QMainWindow destructor is run before + // QApplication's destructor + return ret; +} + +class QtLooper : public Looper { +public: + QtLooper() { + } + + void add(int fd, uint8_t events, FdCallback const& callback) override { + auto pair = fds_.emplace(fd, new Fd(fd, callback)); + auto& handle = pair.first->second; + handle->read_->setEnabled(events & EVENT_READ); + handle->write_->setEnabled(events & EVENT_WRITE); + } + + void modify(int fd, uint8_t events) override { + auto it = fds_.find(fd); + if (it == fds_.end()) { + assert(false); + return; + } + auto& handle = it->second; + handle->read_->setEnabled(events & EVENT_READ); + handle->write_->setEnabled(events & EVENT_WRITE); + } + + 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.release()->delayed_delete_ = true; + } + fds_.erase(fd); + } + + void* schedule(float delay_s, ScheduleCallback const& callback) override { + auto timer = new QTimer(); + timer->setSingleShot(true); + timers_.insert(timer); + QObject::connect(timer, &QTimer::timeout, + [=]() { + timers_.erase(timer); + callback(timer); + timer->deleteLater(); + }); + timer->start(delay_s * 1000); + return timer; + } + + void cancel(void* handle) override { + auto timer = reinterpret_cast(handle); + if (timers_.erase(timer)) { + delete timer; + } + } + + bool run() override { + assert(false); + return false; + } + void quit() override { + assert(false); + } + + clock::time_point now() const override { + return clock::now(); + } + +private: + struct Fd { + FdCallback callback_; + std::unique_ptr read_; + std::unique_ptr write_; + bool in_callback_; + bool delayed_delete_; + + Fd(int fd, FdCallback const& fd_callback) + : callback_(fd_callback), + read_(new QSocketNotifier(fd, QSocketNotifier::Read)), + write_(new QSocketNotifier(fd, QSocketNotifier::Write)), + in_callback_(false), + delayed_delete_(false) { + QObject::connect(read_.get(), &QSocketNotifier::activated, + [=](int fd) { callback(fd, EVENT_READ); }); + QObject::connect(write_.get(), &QSocketNotifier::activated, + [=](int fd) { callback(fd, EVENT_WRITE); }); + } + + void callback(int fd, uint8_t events) { + assert(!in_callback_); + in_callback_ = true; + callback_(fd, events); + in_callback_ = false; + if (delayed_delete_) { + read_.release()->deleteLater(); + write_.release()->deleteLater(); + delete this; + } + } + }; + + std::unordered_map> fds_; + std::unordered_set timers_; +}; + +} // namespace + +// static +GuiMain* GuiMain::create(std::string const& title, uint32_t width, + uint32_t height) { + return new QtGuiMain(title, width, height); +} + +// static +GuiMenu* GuiMenu::create() { + return new QtGuiMenu(); +} + +// static +GuiStatusBar* GuiStatusBar::create() { + return new QtGuiStatusBar(); +} + +// static +GuiAbout* GuiAbout::create(std::string const& title, std::string const& version, + const char* author_name, const char* 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 QtGuiAbout(title, version, authors); +} + +// static +GuiForm* GuiForm::create(std::string const& title, + std::string const& text) { + return new QtGuiForm(title, text); +} + +// static +GuiFormApply* GuiFormApply::create(std::string const& title, + std::string const& text, + std::string const& apply_button, + Delegate* delegate) { + return new QtGuiFormApply(title, text, apply_button, delegate); +} + +// static +Looper* GuiMain::createLooper() { + return new QtLooper(); +} + +// static +AttributedText* AttributedText::create() { + return HtmlAttributedText::create(); +} diff --git a/src/gui_statusbar.hh b/src/gui_statusbar.hh new file mode 100644 index 0000000..d96a40e --- /dev/null +++ b/src/gui_statusbar.hh @@ -0,0 +1,25 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_STATUSBAR_HH +#define GUI_STATUSBAR_HH + +#include + +class GuiStatusBar { +public: + virtual ~GuiStatusBar() {} + + virtual void set_status(std::string const& str) = 0; + virtual void set_override(std::string const& str) = 0; + void clear_override() { + set_override(""); + } + + static GuiStatusBar* create(); + +protected: + GuiStatusBar() {} + GuiStatusBar(GuiStatusBar&) = delete; +}; + +#endif // GUI_STATUSBAR_HH diff --git a/src/gui_window.hh b/src/gui_window.hh new file mode 100644 index 0000000..1573cb8 --- /dev/null +++ b/src/gui_window.hh @@ -0,0 +1,32 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#ifndef GUI_WINDOW_HH +#define GUI_WINDOW_HH + +#include + +class GuiWindow { +public: + class Listener { + public: + virtual ~Listener() {} + + protected: + Listener() {} + }; + + virtual ~GuiWindow() {} + + virtual void set_title(std::string const& title) = 0; + + // Can be used by implementation if needed + virtual void* impl() const { + return nullptr; + } + +protected: + GuiWindow() {} + GuiWindow(GuiWindow const&) = delete; +}; + +#endif // GUI_WINDOW_HH diff --git a/src/monitor-gui.cc b/src/monitor-gui.cc new file mode 100644 index 0000000..682b441 --- /dev/null +++ b/src/monitor-gui.cc @@ -0,0 +1,487 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include +#include +#include +#include + +#include "gui_about.hh" +#include "gui_formapply.hh" +#include "gui_hexdump.hh" +#include "gui_listmodel.hh" +#include "gui_menu.hh" +#include "gui_main.hh" +#include "gui_statusbar.hh" +#include "looper.hh" +#include "monitor.hh" +#include "observers.hh" +#include "resolver.hh" + +namespace { + +std::string const ACTION_CONNECT = "connect"; +std::string const ACTION_DISCONNECT = "disconnect"; +std::string const ACTION_EXIT = "exit"; +std::string const ACTION_ABOUT = "about"; +std::string const ACTION_COPY_RAW = "copy_raw"; +std::string const ACTION_COPY_TEXT = "copy_text"; +std::string const ACTION_CLEAR = "clear"; + +bool valid_hostname(std::string const& host) { + return !host.empty(); +} + +bool parse_address(std::string const& addr, std::string* host, uint16_t* port) { + auto i = addr.find(':'); + if (i == std::string::npos) { + if (!valid_hostname(addr)) return false; + if (host) *host = addr; + if (port) *port = 9000; + return true; + } + auto h = addr.substr(0, i); + if (!valid_hostname(h)) return false; + char* end = nullptr; + auto p = strtoul(addr.c_str() + i + 1, &end, 10); + if (p == 0 || p > 65535 || !end || *end) return false; + if (host) *host = h; + if (port) *port = p; + return true; +} + +class PackageList : public GuiListModel { +public: + struct Package { + std::string timestamp; + std::string from; + std::string to; + std::string size; + + std::string data; + }; + + size_t rows() const override { + return packages_.size(); + } + + size_t columns() const override { + return 4; + } + + std::string header(size_t column) const override { + switch (column) { + case 0: + return "Time"; + case 1: + return "From"; + case 2: + return "To"; + case 3: + return "Data"; + } + assert(false); + return ""; + } + + std::string data(size_t row, size_t column) const override { + if (row < packages_.size()) { + auto const& pkg = packages_[row]; + switch (column) { + case 0: return pkg.timestamp; + case 1: return pkg.from; + case 2: return pkg.to; + case 3: return pkg.size; + } + } + assert(false); + return ""; + } + + void add_listener(Listener* listener) override { + listeners_.insert(listener); + } + + void remove_listener(Listener* listener) override { + listeners_.erase(listener); + } + + void package(Monitor::Package const& package) { + auto const index = packages_.size(); + open_.emplace(package.id, index); + packages_.emplace_back(); + auto& pkg = packages_.back(); + format_timestamp(&pkg.timestamp, package.timestamp); + format_host_port(&pkg.from, package.source_host, package.source_port); + format_host_port(&pkg.to, package.target_host, package.target_port); + format_size(&pkg.size, 0, false); + + notify_added(index, index); + } + + size_t package_data(uint32_t id, char const* data, size_t size, bool last) { + auto it = open_.find(id); + if (it == open_.end()) { + assert(false); + return std::string::npos; + } + auto const index = it->second; + auto& pkg = packages_[index]; + pkg.data.append(data, size); + format_size(&pkg.size, pkg.data.size(), last); + if (last) open_.erase(it); + + notify_changed(index, index); + return index; + } + + Package const& package(size_t index) const { + if (index < packages_.size()) { + return packages_[index]; + } + assert(false); + return EMPTY; + } + + void clear() { + auto last = packages_.size(); + epoch_.tv_sec = 0; + epoch_.tv_nsec = 0; + if (last == 0) return; + packages_.clear(); + open_.clear(); + notify_removed(0, last - 1); + } + +private: + static const Package EMPTY; + + static void format_size(std::string* out, size_t size, bool done) { + char tmp[50]; + auto len = snprintf(tmp, sizeof(tmp), "%llu bytes", static_cast(size)); + out->assign(tmp, len); + if (!done) out->append(" ...", 4); + } + + static void format_host_port(std::string* out, std::string const& host, uint16_t port) { + out->assign(host); + out->push_back(':'); + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", static_cast(port)); + out->append(tmp, len); + } + + void format_timestamp(std::string* out, struct timespec const& time) { + if (epoch_.tv_sec == 0 && epoch_.tv_nsec == 0) { + epoch_ = time; + } + auto s = time.tv_sec - epoch_.tv_sec; + auto n = time.tv_nsec - epoch_.tv_nsec; + if (n < 0) { + --s; + n += 1000000000ull; + } + char tmp[50]; + auto len = snprintf(tmp, sizeof(tmp), "%ld.%09lu", static_cast(s), static_cast(n)); + out->assign(tmp, len); + } + + void notify_added(size_t first, size_t last) { + auto it = listeners_.notify(); + while (it.has_next()) { + it.next()->rows_added(this, first, last); + } + } + + void notify_changed(size_t first, size_t last) { + auto it = listeners_.notify(); + while (it.has_next()) { + it.next()->rows_changed(this, first, last); + } + } + + void notify_removed(size_t first, size_t last) { + auto it = listeners_.notify(); + while (it.has_next()) { + it.next()->rows_removed(this, first, last); + } + } + + std::vector packages_; + std::unordered_map open_; + Observers listeners_; + struct timespec epoch_; + + static const std::string empty_; +}; + +// static +const std::string PackageList::empty_; + +class MonitorGui : GuiMenu::Listener, GuiMain::Listener, Monitor::Delegate { +private: + class ConnectFormListener : public GuiFormApply::Listener { + public: + bool about_to_close(GuiForm* form) override { + auto address = form->get_string("address"); + if (address.empty()) { + form->set_error("Empty address"); + return false; + } + if (!parse_address(address, nullptr, nullptr)) { + form->set_error("Invalid address, expects HOST[:PORT]"); + return false; + } + return true; + } + }; + + class ConnectFormDelegate : public GuiFormApply::Delegate { + public: + ConnectFormDelegate(Monitor* monitor) + : monitor_(monitor) { + } + + void apply(GuiFormApply* form) override { + std::string host; + uint16_t port; + if (!parse_address(form->get_string("address"), &host, &port)) { + form->set_error("Invalid address, expects HOST[:PORT]"); + form->applied(false); + return; + } + monitor_->connect(host, port); + } + + private: + Monitor* monitor_; + }; + +public: + MonitorGui() + : packages_(new PackageList()), + main_(GuiMain::create("TransparentProxy Monitor", 800, 400)), + menu_(GuiMenu::create()), + statusbar_(GuiStatusBar::create()), + looper_(main_->createLooper()), + has_selection_(false), + selection_(0) { + auto file = menu_->add_menu("File"); + file->add_item(ACTION_CONNECT, "Connect..."); + file->add_item(ACTION_DISCONNECT, "Disconnect"); + file->add_separator(); + file->add_item(ACTION_EXIT, "Exit"); + auto edit = menu_->add_menu("Edit"); + edit->add_item(ACTION_COPY_TEXT, "Copy"); + edit->add_item(ACTION_COPY_RAW, "Copy binary"); + edit->add_separator(); + edit->add_item(ACTION_CLEAR, "Clear"); + auto help = menu_->add_menu("Help"); + help->add_item(ACTION_ABOUT, "About..."); + main_->set_menu(menu_.get()); + main_->set_statusbar(statusbar_.get()); + main_->set_split(0.7); + + main_->set_listmodel(packages_.get()); + + statusbar_->set_status("Not connected"); + menu_->enable_item(ACTION_DISCONNECT, false); + + menu_->enable_item(ACTION_COPY_RAW, false); + menu_->enable_item(ACTION_COPY_TEXT, false); + + menu_->add_listener(this); + main_->add_listener(this); + } + + bool run(int argc, char** argv) { + return main_->run(argc, argv); + } + + // GuiMenu::Listener + void item_activated(const std::string& id) override { + if (id == ACTION_EXIT) { + main_->exit(); + } else if (id == ACTION_ABOUT) { + if (!about_) { + about_.reset(GuiAbout::create("TransparentProxy Monitor", + VERSION, + "Joel Klinghed", "the_jk@yahoo.com", + nullptr)); + } + main_->show(about_.get()); + } else if (id == ACTION_CONNECT) { + setup_monitor(); + auto dlg = std::unique_ptr( + new ConnectFormDelegate(monitor_.get())); + auto lst = std::unique_ptr( + new ConnectFormListener()); + connect_.reset( + GuiFormApply::create("Connect...", + "Enter address for monitor to connect to", + "Connect", + dlg.get())); + connect_->add_string("address", "Address", ""); + connect_->add_listener(lst.get()); + if (connect_->show(main_.get())) { + monitor_->attach(); + } else { + monitor_->disconnect(); + } + connect_.reset(); + } else if (id == ACTION_DISCONNECT) { + assert(monitor_); + if (monitor_) { + monitor_->disconnect(); + } + } else if (id == ACTION_COPY_TEXT) { + if (!has_selection_) { + assert(false); + return; + } + auto& pkg = packages_->package(selection_); + std::unique_ptr text(AttributedText::create()); + HexDump::write(text.get(), HexDump::ADDRESS | HexDump::CHARS, pkg.data); + std::string str; + str.append("From: ").append(pkg.from).push_back('\n'); + str.append("To : ").append(pkg.to).push_back('\n'); + str.append("At : ").append(pkg.timestamp).push_back('\n'); + str.append(text->text()); + main_->add_to_clipboard(str); + } else if (id == ACTION_COPY_RAW) { + if (!has_selection_) { + assert(false); + return; + } + auto& pkg = packages_->package(selection_); + main_->add_to_clipboard(pkg.data, "application/octet-stream"); + } else if (id == ACTION_CLEAR) { + packages_->clear(); + } else { + assert(false); + } + } + + // GuiMain::Listener + bool about_to_exit(GuiMain* main) override { + assert(main_.get() == main); + return true; + } + + void selected_row(GuiMain* main, size_t index) override { + assert(main_.get() == main); + if (has_selection_ && selection_ == index) return; + has_selection_ = true; + selection_ = index; + + auto& pkg = packages_->package(index); + std::unique_ptr text(AttributedText::create()); + HexDump::write(text.get(), HexDump::ADDRESS | HexDump::CHARS, pkg.data); + main_->set_package(std::move(text)); + + menu_->enable_item(ACTION_COPY_RAW, true); + menu_->enable_item(ACTION_COPY_TEXT, true); + } + + void lost_selection(GuiMain* main) override { + assert(main_.get() == main); + has_selection_ = false; + main_->set_package(nullptr); + + menu_->enable_item(ACTION_COPY_RAW, false); + menu_->enable_item(ACTION_COPY_TEXT, false); + } + + // Monitor::Delegate + void state(Monitor* monitor, Monitor::State state) override { + assert(monitor == monitor_.get()); + if (connect_) { + switch (state) { + case Monitor::DISCONNECTED: + connect_->set_error("Unable to connect, is the proxy running" + " and listening for monitors?"); + connect_->applied(false); + break; + case Monitor::CONNECTED: + connect_->applied(true); + break; + case Monitor::CONNECTING: + case Monitor::ATTACHED: + break; + } + } + bool allow_connect, allow_disconnect; + switch (state) { + case Monitor::DISCONNECTED: + statusbar_->set_status("Not connected"); + allow_connect = true; + allow_disconnect = false; + break; + case Monitor::CONNECTED: + statusbar_->set_status("Connected"); + allow_connect = false; + allow_disconnect = true; + break; + case Monitor::CONNECTING: + statusbar_->set_status("Connecting..."); + allow_connect = false; + allow_disconnect = false; + break; + case Monitor::ATTACHED: + statusbar_->set_status("Connected and attached"); + allow_connect = false; + allow_disconnect = true; + break; + } + menu_->enable_item(ACTION_CONNECT, allow_connect); + menu_->enable_item(ACTION_DISCONNECT, allow_disconnect); + } + + void error(Monitor* monitor, std::string const& UNUSED(error)) override { + assert(monitor == monitor_.get()); + } + + void package(Monitor* monitor, Monitor::Package const& package) override { + assert(monitor == monitor_.get()); + packages_->package(package); + } + + void package_data(Monitor* monitor, uint32_t id, + char const* data, size_t size, bool last) override { + assert(monitor == monitor_.get()); + auto index = packages_->package_data(id, data, size, last); + if (has_selection_ && index == selection_) { + selected_row(nullptr, index); + } + } + +private: + void setup_monitor() { + if (!resolver_) { + resolver_.reset(Resolver::create(looper_.get())); + } + if (!monitor_) { + monitor_.reset(Monitor::create(looper_.get(), resolver_.get(), this)); + } + } + + std::unique_ptr packages_; + std::unique_ptr main_; + std::unique_ptr menu_; + std::unique_ptr statusbar_; + std::unique_ptr about_; + std::unique_ptr connect_; + std::unique_ptr looper_; + std::unique_ptr resolver_; + std::unique_ptr monitor_; + bool has_selection_; + size_t selection_; +}; + +} // namespace + +int main(int argc, char** argv) { + auto gui = std::unique_ptr(new MonitorGui()); + return gui->run(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/test/.gitignore b/test/.gitignore index b102ce1..aa33417 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -8,3 +8,4 @@ /test-xdg /test-xdg /test-observers +/test-htmlattrtext diff --git a/test/Makefile.am b/test/Makefile.am index 4daf511..b5d2cee 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -3,7 +3,7 @@ MAINTAINERCLEANFILES = Makefile.in AM_CXXFLAGS = @DEFINES@ TESTS = test-url test-http test-args test-xdg test-paths test-strings \ - test-observers + test-observers test-htmlattrtext check_PROGRAMS = $(TESTS) @@ -26,3 +26,6 @@ test_paths_SOURCES = test-paths.cc test_paths_LDADD = $(top_builddir)/src/libtp.a test_observers_SOURCES = test-observers.cc + +test_htmlattrtext_SOURCES = test-htmlattrtext.cc +test_htmlattrtext_LDADD = $(top_builddir)/src/libattrstr.a diff --git a/test/test-htmlattrtext.cc b/test/test-htmlattrtext.cc new file mode 100644 index 0000000..a0d18e6 --- /dev/null +++ b/test/test-htmlattrtext.cc @@ -0,0 +1,38 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" +#include "test.hh" + +#include + +#include "gui_htmlattrtext.hh" + +namespace { + +bool test_sanity() { + std::unique_ptr attr(HtmlAttributedText::create()); + HtmlAttributedText::Attribute red(0xff, 0, 0); + HtmlAttributedText::Attribute bold; + HtmlAttributedText::Attribute green(0, 0xff, 0); + bold.set_bold(true); + ASSERT_EQ("", attr->html()); + attr->append("Hello World"); + ASSERT_EQ("Hello World", attr->html()); + attr->append(" ", red); + ASSERT_EQ("Hello World <!>", attr->html()); + attr->add(bold, 0, 5); + ASSERT_EQ("Hello World <!>", attr->html()); + attr->add(red, 1, 2); + ASSERT_EQ("Hello World <!>", attr->html()); + attr->set(green, 1, 1); + ASSERT_EQ("Hello World <!>", attr->html()); + return true; +} + +} // namespace + +int main(void) { + BEFORE; + RUN(test_sanity()); + AFTER; +} -- cgit v1.2.3-70-g09d2