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