diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2015-06-22 22:41:50 +0200 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2015-06-22 22:41:50 +0200 |
commit | 730f801f1d75bc247c86f76345e71f9ea128a07d (patch) | |
tree | fdf51e89c6e6b023a8e0e0ab75d2cef0b1dd8f9f | |
parent | 6f48745171ab3ef898b24d8da9732c4f309b08c7 (diff) | |
download | sciteco-730f801f1d75bc247c86f76345e71f9ea128a07d.tar.gz |
major GTK UI rewrite: use a separate execution thread
* the execution thread does not block the main thread (with
the main loop).
* therefore if SciTECO executes a long-running macro,
the UI stays "responsive".
* also this allows us to handle ^C interruptions.
This is part of the solution to #4 for GTK+ UIs.
* to speed up execution and avoid frequent UI redraws
(now that we run concurrently, there are even more redraws),
the view change is applied only after key presses.
* also we freeze all UI updates on the view during SciTECO's
key processing.
* Closing the window, requests a graceful execution thread
shut down. This may later be extended to allow programmable
window close-behaviour using a special function key macro
(e.g. mapped to the "Break" key).
-rw-r--r-- | src/interface-gtk.cpp | 294 | ||||
-rw-r--r-- | src/interface-gtk.h | 35 |
2 files changed, 284 insertions, 45 deletions
diff --git a/src/interface-gtk.cpp b/src/interface-gtk.cpp index 3253139..83c20cb 100644 --- a/src/interface-gtk.cpp +++ b/src/interface-gtk.cpp @@ -47,9 +47,10 @@ namespace SciTECO { extern "C" { static void scintilla_notify(ScintillaObject *sci, uptr_t idFrom, SCNotification *notify, gpointer user_data); +static gpointer exec_thread_cb(gpointer data); static gboolean cmdline_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data); -static gboolean exit_app(GtkWidget *w, GdkEventAny *e, gpointer p); +static gboolean exit_app(GtkWidget *w, GdkEventAny *e, gpointer user_data); } #define UNNAMED_FILE "(Unnamed)" @@ -57,6 +58,8 @@ static gboolean exit_app(GtkWidget *w, GdkEventAny *e, gpointer p); void ViewGtk::initialize_impl(void) { + gdk_threads_enter(); + sci = SCINTILLA(scintilla_new()); /* * We don't want the object to be destroyed @@ -72,6 +75,12 @@ ViewGtk::initialize_impl(void) g_signal_connect(G_OBJECT(sci), SCINTILLA_NOTIFY, G_CALLBACK(scintilla_notify), NULL); + /* + * setup() calls Scintilla messages, so we must unlock + * here already to avoid deadlocks. + */ + gdk_threads_leave(); + setup(); } @@ -81,31 +90,57 @@ InterfaceGtk::main_impl(int &argc, char **&argv) static const Cmdline empty_cmdline; GtkWidget *info_content; + /* + * g_thread_init() is required prior to v2.32 + * (we still support v2.28) but generates a warning + * on newer versions. + */ +#if !GLIB_CHECK_VERSION(2,32,0) + g_thread_init(NULL); +#endif + gdk_threads_init(); gtk_init(&argc, &argv); + /* + * The event queue is initialized now, so we can + * pass it as user data to C-linkage callbacks. + */ + event_queue = g_async_queue_new(); + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), PACKAGE_NAME); g_signal_connect(G_OBJECT(window), "delete-event", - G_CALLBACK(exit_app), NULL); + G_CALLBACK(exit_app), event_queue); vbox = gtk_vbox_new(FALSE, 0); info_current = g_strdup(PACKAGE_NAME); - cmdline_widget = gtk_entry_new(); - gtk_entry_set_has_frame(GTK_ENTRY(cmdline_widget), FALSE); - gtk_editable_set_editable(GTK_EDITABLE(cmdline_widget), FALSE); - widget_set_font(cmdline_widget, "Courier"); - g_signal_connect(G_OBJECT(cmdline_widget), "key-press-event", - G_CALLBACK(cmdline_key_pressed), NULL); - gtk_box_pack_end(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0); + /* + * The event box is the parent of all Scintilla views + * that should be displayed. + * This is handy when adding or removing current views, + * enabling and disabling GDK updates and in order to filter + * mouse and keyboard events going to Scintilla. + */ + event_box_widget = gtk_event_box_new(); + gtk_event_box_set_above_child(GTK_EVENT_BOX(event_box_widget), TRUE); + gtk_box_pack_start(GTK_BOX(vbox), event_box_widget, TRUE, TRUE, 0); info_widget = gtk_info_bar_new(); info_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_widget)); message_widget = gtk_label_new(""); gtk_misc_set_alignment(GTK_MISC(message_widget), 0., 0.); gtk_container_add(GTK_CONTAINER(info_content), message_widget); - gtk_box_pack_end(GTK_BOX(vbox), info_widget, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), info_widget, FALSE, FALSE, 0); + + cmdline_widget = gtk_entry_new(); + gtk_entry_set_has_frame(GTK_ENTRY(cmdline_widget), FALSE); + gtk_editable_set_editable(GTK_EDITABLE(cmdline_widget), FALSE); + widget_set_font(cmdline_widget, "Courier"); + g_signal_connect(G_OBJECT(cmdline_widget), "key-press-event", + G_CALLBACK(cmdline_key_pressed), event_queue); + gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(window), vbox); @@ -134,37 +169,31 @@ InterfaceGtk::vmsg_impl(MessageType type, const gchar *fmt, va_list ap) g_vsnprintf(buf, sizeof(buf), fmt, aq); va_end(aq); + gdk_threads_enter(); + gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget), type2gtk[type]); gtk_label_set_text(GTK_LABEL(message_widget), buf); + + gdk_threads_leave(); } void InterfaceGtk::msg_clear(void) { + gdk_threads_enter(); + gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget), GTK_MESSAGE_OTHER); gtk_label_set_text(GTK_LABEL(message_widget), ""); + + gdk_threads_leave(); } void InterfaceGtk::show_view_impl(ViewGtk *view) { - /* - * The last view's object is not guaranteed to - * still exist. - * However its widget is, due to reference counting. - */ - if (current_view_widget) - gtk_container_remove(GTK_CONTAINER(vbox), - current_view_widget); - current_view = view; - current_view_widget = view->get_widget(); - - gtk_box_pack_start(GTK_BOX(vbox), current_view_widget, - TRUE, TRUE, 0); - gtk_widget_show(current_view_widget); } void @@ -230,6 +259,8 @@ InterfaceGtk::cmdline_update_impl(const Cmdline *cmdline) gint pos = 1; gint cmdline_len; + gdk_threads_enter(); + /* * We don't know if the new command line is similar to * the old one, so we can just as well rebuild it. @@ -248,6 +279,8 @@ InterfaceGtk::cmdline_update_impl(const Cmdline *cmdline) /* set cursor after effective command line */ gtk_editable_set_position(GTK_EDITABLE(cmdline_widget), cmdline_len); + + gdk_threads_leave(); } void @@ -260,17 +293,25 @@ InterfaceGtk::popup_add_impl(PopupEntryType type, /* [POPUP_DIRECTORY] = */ GTK_INFO_POPUP_DIRECTORY }; + gdk_threads_enter(); + gtk_info_popup_add(GTK_INFO_POPUP(popup_widget), type2gtk[type], name, highlight); + + gdk_threads_leave(); } void InterfaceGtk::popup_clear_impl(void) { + gdk_threads_enter(); + if (gtk_widget_get_visible(popup_widget)) { gtk_widget_hide(popup_widget); gtk_info_popup_clear(GTK_INFO_POPUP(popup_widget)); } + + gdk_threads_leave(); } void @@ -284,9 +325,129 @@ InterfaceGtk::widget_set_font(GtkWidget *widget, const gchar *font_name) } void +InterfaceGtk::event_loop_impl(void) +{ + GThread *thread; + + /* + * When changing views, the new widget is not + * added immediately to avoid flickering in the GUI. + * It is only updated once per key press and only + * if it really changed. + * Therefore we must add the current view to the + * window initially. + * For the same reason, window title updates are + * deferred to once after every key press, so we must + * set the window title initially. + */ + if (current_view) { + current_view_widget = current_view->get_widget(); + gtk_container_add(GTK_CONTAINER(event_box_widget), + current_view_widget); + } + gtk_window_set_title(GTK_WINDOW(window), info_current); + + gtk_widget_show_all(window); + + /* + * Start up SciTECO execution thread. + * Whenever it needs to send a Scintilla message + * it locks the GDK mutex. + */ + thread = g_thread_new("sciteco-exec", + exec_thread_cb, event_queue); + + /* + * NOTE: The watchers do not modify any GTK objects + * using one of the methods that lock the GDK mutex. + * This is from now on reserved to the execution + * thread. Therefore there can be no dead-locks. + */ + gdk_threads_enter(); + gtk_main(); + gdk_threads_leave(); + + /* + * This usually means that the user requested program + * termination and the execution thread called + * gtk_main_quit(). + * We still wait for the execution thread to shut down + * properly. This also frees `thread`. + */ + g_thread_join(thread); + + /* + * Make sure the window is hidden + * now already, as there may be code that has to be + * executed in batch mode. + */ + gtk_widget_hide(window); +} + +static gpointer +exec_thread_cb(gpointer data) +{ + GAsyncQueue *event_queue = (GAsyncQueue *)data; + + for (;;) { + GdkEventKey *event = (GdkEventKey *)g_async_queue_pop(event_queue); + + bool is_shift = event->state & GDK_SHIFT_MASK; + bool is_ctl = event->state & GDK_CONTROL_MASK; + + try { + sigint_occurred = FALSE; + interface.handle_key_press(is_shift, is_ctl, event->keyval); + sigint_occurred = FALSE; + } catch (Quit) { + /* + * SciTECO should terminate, so we exit + * this thread. + * The main loop will terminate and + * event_loop() will return. + */ + gdk_event_free((GdkEvent *)event); + + gdk_threads_enter(); + gtk_main_quit(); + gdk_threads_leave(); + break; + } + + gdk_event_free((GdkEvent *)event); + } + + return NULL; +} + +void InterfaceGtk::handle_key_press(bool is_shift, bool is_ctl, guint keyval) { + GdkWindow *view_window; + ViewGtk *last_view = current_view; + + /* + * Avoid redraws of the current view freezing updates + * on the view's GDK window. + * Since we're running in parallel to the main loop + * this would in frequent redraws. + * By freezing updates, the behaviour is similar to + * the Curses UI. + */ + gdk_threads_enter(); + view_window = gtk_widget_get_parent_window(event_box_widget); + gdk_window_freeze_updates(view_window); + gdk_threads_leave(); + switch (keyval) { + case GDK_Break: + /* + * FIXME: This usually means that the window's close + * button was pressed. + * It should be a function key macro, with quitting + * as the default action. + */ + throw Quit(); case GDK_Escape: cmdline.keypress(CTL_KEY_ESC); break; @@ -356,8 +517,34 @@ InterfaceGtk::handle_key_press(bool is_shift, bool is_ctl, guint keyval) * window title each time it is updated is VERY costly. * So we set it here once after every keypress even if the * info line did not change. + * View changes are also only applied here to the GTK + * window even though GDK updates have been frozen since + * the size reallocations are very costly. */ + gdk_threads_enter(); + + if (current_view != last_view) { + /* + * The last view's object is not guaranteed to + * still exist. + * However its widget is, due to reference counting. + */ + if (current_view_widget) + gtk_container_remove(GTK_CONTAINER(event_box_widget), + current_view_widget); + + current_view_widget = current_view->get_widget(); + + gtk_container_add(GTK_CONTAINER(event_box_widget), + current_view_widget); + gtk_widget_show(current_view_widget); + } + gtk_window_set_title(GTK_WINDOW(window), info_current); + + gdk_window_thaw_updates(view_window); + + gdk_threads_leave(); } InterfaceGtk::~InterfaceGtk() @@ -369,6 +556,15 @@ InterfaceGtk::~InterfaceGtk() gtk_widget_destroy(window); scintilla_release_resources(); + + if (event_queue) { + GdkEvent *e; + + while ((e = (GdkEvent *)g_async_queue_try_pop(event_queue))) + gdk_event_free(e); + + g_async_queue_unref(event_queue); + } } /* @@ -386,8 +582,9 @@ static gboolean cmdline_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { - bool is_shift = event->state & GDK_SHIFT_MASK; - bool is_ctl = event->state & GDK_CONTROL_MASK; + GAsyncQueue *event_queue = (GAsyncQueue *)user_data; + + bool is_ctl = event->state & GDK_CONTROL_MASK; #ifdef DEBUG g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n", @@ -395,27 +592,52 @@ cmdline_key_pressed(GtkWidget *widget, GdkEventKey *event, event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK); #endif - try { - interface.handle_key_press(is_shift, is_ctl, event->keyval); - } catch (Quit) { + g_async_queue_lock(event_queue); + + if (g_async_queue_length_unlocked(event_queue) >= 0 && + is_ctl && gdk_keyval_to_upper(event->keyval) == GDK_C) { /* - * SciTECO should terminate, so we exit - * the main loop. event_loop() will return. + * Handle asynchronous interruptions if CTRL+C is pressed. + * If the execution thread is currently blocking, + * the key is delivered like an ordinary key press. */ - gtk_main_quit(); + sigint_occurred = TRUE; + } else { + /* + * Copies the key-press event, since it must be evaluated + * by the exec_thread_cb. This is costly, but since we're + * using the event queue as a kind of keyboard buffer, + * who cares? + */ + g_async_queue_push_unlocked(event_queue, + gdk_event_copy((GdkEvent *)event)); } + g_async_queue_unlock(event_queue); + return TRUE; } static gboolean -exit_app(GtkWidget *w, GdkEventAny *e, gpointer p) +exit_app(GtkWidget *w, GdkEventAny *e, gpointer user_data) { + GAsyncQueue *event_queue = (GAsyncQueue *)user_data; + GdkEventKey *break_event; + /* - * FIXME: should instead insert "(EX)" or similar - * Perhaps something like a "QUIT" function key macro + * We cannot yet call gtk_main_quit() as the execution + * thread must shut down properly. + * Therefore we emulate that the "break" key was pressed + * which may then be handled by the execution thread. + * It may also be used to insert a function key macro. + * NOTE: We might also create a GDK_DELETE event. */ - gtk_main_quit(); + break_event = (GdkEventKey *)gdk_event_new(GDK_KEY_RELEASE); + break_event->window = gtk_widget_get_parent_window(w); + break_event->keyval = GDK_Break; + + g_async_queue_push(event_queue, break_event); + return TRUE; } diff --git a/src/interface-gtk.h b/src/interface-gtk.h index 31deaf6..8150c8c 100644 --- a/src/interface-gtk.h +++ b/src/interface-gtk.h @@ -21,6 +21,8 @@ #include <stdarg.h> #include <glib.h> + +#include <gdk/gdk.h> #include <gtk/gtk.h> #include <Scintilla.h> @@ -60,7 +62,13 @@ public: inline sptr_t ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0) { - return scintilla_send_message(sci, iMessage, wParam, lParam); + sptr_t ret; + + gdk_threads_enter(); + ret = scintilla_send_message(sci, iMessage, wParam, lParam); + gdk_threads_leave(); + + return ret; } } ViewCurrent; @@ -68,6 +76,8 @@ typedef class InterfaceGtk : public Interface<InterfaceGtk, ViewGtk> { GtkWidget *window; GtkWidget *vbox; + GtkWidget *event_box_widget; + gchar *info_current; GtkWidget *info_widget; @@ -78,15 +88,19 @@ typedef class InterfaceGtk : public Interface<InterfaceGtk, ViewGtk> { GtkWidget *current_view_widget; + GAsyncQueue *event_queue; + public: InterfaceGtk() : Interface(), window(NULL), vbox(NULL), + event_box_widget(NULL), info_current(NULL), info_widget(NULL), message_widget(NULL), cmdline_widget(NULL), popup_widget(NULL), - current_view_widget(NULL) {} + current_view_widget(NULL), + event_queue(NULL) {} ~InterfaceGtk(); /* overrides Interface::get_options() */ @@ -122,24 +136,27 @@ public: popup_show_impl(void) { /* FIXME: scroll through popup contents */ + gdk_threads_enter(); gtk_widget_show(popup_widget); + gdk_threads_leave(); } /* implementation of Interface::popup_is_shown() */ inline bool popup_is_shown_impl(void) { - return gtk_widget_get_visible(popup_widget); + bool ret; + + gdk_threads_enter(); + ret = gtk_widget_get_visible(popup_widget); + gdk_threads_leave(); + + return ret; } /* implementation of Interface::popup_clear() */ void popup_clear_impl(void); /* main entry point (implementation) */ - inline void - event_loop_impl(void) - { - gtk_widget_show_all(window); - gtk_main(); - } + void event_loop_impl(void); /* * FIXME: This is for internal use only and could be |