aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/interface-gtk/interface.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/interface-gtk/interface.c')
-rw-r--r--src/interface-gtk/interface.c329
1 files changed, 215 insertions, 114 deletions
diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c
index 0dbd2ba..7f58c45 100644
--- a/src/interface-gtk/interface.c
+++ b/src/interface-gtk/interface.c
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012-2024 Robin Haberkorn
+ * Copyright (C) 2012-2025 Robin Haberkorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -60,16 +60,17 @@
//#define DEBUG
+static gboolean teco_interface_busy_timeout_cb(gpointer user_data);
+static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data);
static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
GdkRectangle *allocation,
gpointer user_data);
static void teco_interface_cmdline_commit_cb(GtkIMContext *context, gchar *str,
gpointer user_data);
-static void teco_interface_size_allocate_cb(GtkWidget *widget,
- GdkRectangle *allocation,
+static gboolean teco_interface_input_cb(GtkWidget *widget, GdkEvent *event,
+ gpointer user_data);
+static void teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len,
gpointer user_data);
-static gboolean teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
- gpointer user_data);
static gboolean teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event,
gpointer user_data);
static gboolean teco_interface_sigterm_handler(gpointer user_data) G_GNUC_UNUSED;
@@ -102,73 +103,6 @@ teco_bgr2rgb(guint32 bgr)
return GUINT32_SWAP_LE_BE(bgr) >> 8;
}
-/*
- * NOTE: The teco_view_t pointer is reused to directly
- * point to the ScintillaObject.
- * This saves one heap object per view.
- */
-
-static void
-teco_view_scintilla_notify(ScintillaObject *sci, gint iMessage,
- SCNotification *notify, gpointer user_data)
-{
- teco_view_process_notify((teco_view_t *)sci, notify);
-}
-
-teco_view_t *
-teco_view_new(void)
-{
- ScintillaObject *sci = SCINTILLA(scintilla_new());
- /*
- * We don't want the object to be destroyed
- * when it is removed from the vbox.
- */
- g_object_ref_sink(sci);
-
- scintilla_set_id(sci, 0);
-
- gtk_widget_set_size_request(GTK_WIDGET(sci), 500, 300);
-
- /*
- * This disables mouse and key events on this view.
- * For some strange reason, masking events on
- * the event box does NOT work.
- *
- * NOTE: Scroll events are still allowed - scrolling
- * is currently not under direct control of SciTECO
- * (i.e. it is OK the side effects of scrolling are not
- * tracked).
- */
- gtk_widget_set_can_focus(GTK_WIDGET(sci), FALSE);
- gint events = gtk_widget_get_events(GTK_WIDGET(sci));
- events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
- events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
- gtk_widget_set_events(GTK_WIDGET(sci), events);
-
- g_signal_connect(sci, SCINTILLA_NOTIFY,
- G_CALLBACK(teco_view_scintilla_notify), NULL);
-
- return (teco_view_t *)sci;
-}
-
-static inline GtkWidget *
-teco_view_get_widget(teco_view_t *ctx)
-{
- return GTK_WIDGET(ctx);
-}
-
-sptr_t
-teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam)
-{
- return scintilla_send_message(SCINTILLA(ctx), iMessage, wParam, lParam);
-}
-
-void
-teco_view_free(teco_view_t *ctx)
-{
- g_object_unref(teco_view_get_widget(ctx));
-}
-
static struct {
GtkCssProvider *css_var_provider;
@@ -198,6 +132,7 @@ static struct {
GtkIMContext *input_method;
GtkWidget *popup_widget;
+ gsize popup_prefix_len;
GtkWidget *current_view_widget;
@@ -240,7 +175,7 @@ teco_interface_init(void)
G_CALLBACK(teco_interface_window_delete_cb), NULL);
g_signal_connect(teco_interface.window, "key-press-event",
- G_CALLBACK(teco_interface_key_pressed_cb), NULL);
+ G_CALLBACK(teco_interface_input_cb), NULL);
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
@@ -313,8 +248,20 @@ teco_interface_init(void)
gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.event_box_widget,
TRUE, TRUE, 0);
- g_signal_connect(teco_interface.event_box_widget, "size-allocate",
- G_CALLBACK(teco_interface_size_allocate_cb), NULL);
+ g_signal_connect(teco_interface.event_box_widget, "realize",
+ G_CALLBACK(teco_interface_event_box_realized_cb), NULL);
+
+ gint events = gtk_widget_get_events(teco_interface.event_box_widget);
+ events |= GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
+ GDK_SCROLL_MASK;
+ gtk_widget_set_events(teco_interface.event_box_widget, events);
+
+ g_signal_connect(teco_interface.event_box_widget, "button-press-event",
+ G_CALLBACK(teco_interface_input_cb), NULL);
+ g_signal_connect(teco_interface.event_box_widget, "button-release-event",
+ G_CALLBACK(teco_interface_input_cb), NULL);
+ g_signal_connect(teco_interface.event_box_widget, "scroll-event",
+ G_CALLBACK(teco_interface_input_cb), NULL);
teco_interface.message_bar_widget = gtk_info_bar_new();
gtk_widget_set_name(teco_interface.message_bar_widget, "sciteco-message-bar");
@@ -350,7 +297,7 @@ teco_interface_init(void)
/* we will forward key events, so the view should only react to text insertion */
teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALLCMDKEYS, 0, 0);
- GtkWidget *cmdline_widget = teco_view_get_widget(teco_interface.cmdline_view);
+ GtkWidget *cmdline_widget = GTK_WIDGET(teco_interface.cmdline_view);
gtk_widget_set_name(cmdline_widget, "sciteco-cmdline");
g_signal_connect(cmdline_widget, "size-allocate",
G_CALLBACK(teco_interface_cmdline_size_allocate_cb), NULL);
@@ -373,6 +320,8 @@ teco_interface_init(void)
*/
teco_interface.popup_widget = teco_gtk_info_popup_new();
gtk_widget_set_name(teco_interface.popup_widget, "sciteco-info-popup");
+ g_signal_connect(teco_interface.popup_widget, "clicked",
+ G_CALLBACK(teco_interface_popup_clicked_cb), NULL);
gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), teco_interface.popup_widget);
g_signal_connect(overlay_widget, "get-child-position",
G_CALLBACK(teco_gtk_info_popup_get_position_in_overlay), NULL);
@@ -389,6 +338,17 @@ teco_interface_init(void)
teco_interface_cmdline_update(&empty_cmdline);
}
+static void
+teco_interface_set_cursor(GtkWidget *widget, const gchar *name)
+{
+ GdkWindow *window = gtk_widget_get_window(widget);
+ g_assert(window != NULL);
+ GdkDisplay *display = gdk_window_get_display(window);
+
+ g_autoptr(GdkCursor) cursor = name ? gdk_cursor_new_from_name(display, name) : NULL;
+ gdk_window_set_cursor(window, cursor);
+}
+
GOptionGroup *
teco_interface_get_options(void)
{
@@ -746,12 +706,16 @@ teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize
}
void
-teco_interface_popup_show(void)
+teco_interface_popup_show(gsize prefix_len)
{
- if (gtk_widget_get_visible(teco_interface.popup_widget))
- teco_gtk_info_popup_scroll_page(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
- else
- gtk_widget_show(teco_interface.popup_widget);
+ teco_interface.popup_prefix_len = prefix_len;
+ gtk_widget_show(teco_interface.popup_widget);
+}
+
+void
+teco_interface_popup_scroll(void)
+{
+ teco_gtk_info_popup_scroll_page(TECO_GTK_INFO_POPUP(teco_interface.popup_widget));
}
gboolean
@@ -876,7 +840,7 @@ teco_interface_set_css_variables(teco_view_t *view)
* This cannot be done via CSS or Scintilla messages.
* Currently, it is always exactly one line high in order to mimic the Curses UI.
*/
- gtk_widget_set_size_request(teco_view_get_widget(teco_interface.cmdline_view), -1, text_height);
+ gtk_widget_set_size_request(GTK_WIDGET(teco_interface.cmdline_view), -1, text_height);
}
static void
@@ -910,19 +874,12 @@ teco_interface_refresh(gboolean current_view_changed)
gtk_container_remove(GTK_CONTAINER(teco_interface.event_box_widget),
teco_interface.current_view_widget);
- teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view);
+ teco_interface.current_view_widget = GTK_WIDGET(teco_interface_current_view);
gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget),
teco_interface.current_view_widget);
gtk_widget_show(teco_interface.current_view_widget);
}
-
- /*
- * Scintilla has been patched to avoid any automatic scrolling since that
- * has been benchmarked to be a very costly operation.
- * Instead we do it only once after every keypress.
- */
- teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
}
static void
@@ -977,7 +934,7 @@ teco_interface_get_ansi_key(GdkEventKey *event)
static gboolean
teco_interface_handle_key_press(GdkEventKey *event, GError **error)
{
- const teco_view_t *last_view = teco_interface_current_view;
+ g_assert(event->type == GDK_KEY_PRESS);
switch (event->keyval) {
case GDK_KEY_Escape:
@@ -1105,10 +1062,81 @@ teco_interface_handle_key_press(GdkEventKey *event, GError **error)
gtk_im_context_filter_keypress(teco_interface.input_method, event);
}
- teco_interface_refresh(teco_interface_current_view != last_view);
return TRUE;
}
+static gboolean
+teco_interface_handle_mouse_button(GdkEventButton *event, GError **error)
+{
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ teco_mouse.type = TECO_MOUSE_PRESSED;
+ break;
+ case GDK_BUTTON_RELEASE:
+ teco_mouse.type = TECO_MOUSE_RELEASED;
+ break;
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ default:
+ /* delivered in addition to GDK_BUTTON_PRESS */
+ return TRUE;
+ }
+
+ teco_mouse.x = event->x;
+ teco_mouse.y = event->y;
+ teco_mouse.button = event->button;
+
+ teco_mouse.mods = 0;
+ if (event->state & GDK_SHIFT_MASK)
+ teco_mouse.mods |= TECO_MOUSE_SHIFT;
+ if (event->state & GDK_CONTROL_MASK)
+ teco_mouse.mods |= TECO_MOUSE_CTRL;
+ /*
+ * NOTE: GTK returns MOD1 *without* SHIFT for ALT.
+ */
+ if ((event->state & (GDK_MOD1_MASK | GDK_SHIFT_MASK)) == GDK_MOD1_MASK)
+ teco_mouse.mods |= TECO_MOUSE_ALT;
+
+ return teco_cmdline_keymacro("MOUSE", -1, error);
+}
+
+static gboolean
+teco_interface_handle_scroll(GdkEventScroll *event, GError **error)
+{
+ g_assert(event->type == GDK_SCROLL);
+
+ /*
+ * FIXME: Do we have to support GDK_SCROLL_SMOOTH?
+ */
+ switch (event->direction) {
+ case GDK_SCROLL_UP:
+ teco_mouse.type = TECO_MOUSE_SCROLLUP;
+ break;
+ case GDK_SCROLL_DOWN:
+ teco_mouse.type = TECO_MOUSE_SCROLLDOWN;
+ break;
+ default:
+ return TRUE;
+ }
+
+ teco_mouse.x = event->x;
+ teco_mouse.y = event->y;
+ teco_mouse.button = -1;
+
+ teco_mouse.mods = 0;
+ if (event->state & GDK_SHIFT_MASK)
+ teco_mouse.mods |= TECO_MOUSE_SHIFT;
+ if (event->state & GDK_CONTROL_MASK)
+ teco_mouse.mods |= TECO_MOUSE_CTRL;
+ /*
+ * NOTE: GTK returns MOD1 *without* SHIFT for ALT.
+ */
+ if ((event->state & (GDK_MOD1_MASK | GDK_SHIFT_MASK)) == GDK_MOD1_MASK)
+ teco_mouse.mods |= TECO_MOUSE_ALT;
+
+ return teco_cmdline_keymacro("MOUSE", -1, error);
+}
+
gboolean
teco_interface_event_loop(GError **error)
{
@@ -1251,6 +1279,29 @@ teco_interface_cleanup(void)
*/
/**
+ * Called some time after processing an input event in order to show
+ * business.
+ *
+ * The delay avoids cursor flickering during normal typing.
+ *
+ * @fixme It would be nicer to set the cursor for the entire window,
+ * but that would apparently require another GtkEventBox, spanning everything.
+ */
+static gboolean
+teco_interface_busy_timeout_cb(gpointer user_data)
+{
+ teco_interface_set_cursor(teco_interface.event_box_widget, "wait");
+ return G_SOURCE_REMOVE;
+}
+
+static void
+teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data)
+{
+ /* It's only now safe to get the GdkWindow. */
+ teco_interface_set_cursor(widget, "text");
+}
+
+/**
* Called when the commandline widget is resized.
* This should ensure that the caret jumps to the middle of the command line,
* imitating the behaviour of the current Curses command line.
@@ -1263,26 +1314,16 @@ teco_interface_cmdline_size_allocate_cb(GtkWidget *widget,
CARET_SLOP | CARET_EVEN, allocation->width/2);
}
-static void
-teco_interface_size_allocate_cb(GtkWidget *widget,
- GdkRectangle *allocation, gpointer user_data)
-{
- /*
- * This especially ensures that the caret is visible after startup.
- */
- teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
-}
-
static gboolean
-teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
+teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data)
{
static gboolean recursed = FALSE;
- g_autoptr(GError) error = NULL;
#ifdef DEBUG
- g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n",
- event->string, *event->string,
- event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK);
+ if (event->type == GDK_KEY_PRESS)
+ g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n",
+ event->key.string, *event->key.string,
+ event->key.state & GDK_SHIFT_MASK, event->key.state & GDK_CONTROL_MASK);
#endif
if (recursed) {
@@ -1295,8 +1336,9 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us
* during execution, but the current implementation is
* probably easier.
*/
- if (event->state & GDK_CONTROL_MASK &&
- gdk_keyval_to_upper(event->keyval) == GDK_KEY_C)
+ if (event->type == GDK_KEY_PRESS &&
+ event->key.state & GDK_CONTROL_MASK &&
+ gdk_keyval_to_upper(event->key.keyval) == GDK_KEY_C)
/*
* Handle asynchronous interruptions if CTRL+C is pressed.
* If the execution thread is currently blocking,
@@ -1305,20 +1347,26 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us
teco_interrupted = TRUE;
else
g_queue_push_tail(teco_interface.event_queue,
- gdk_event_copy((GdkEvent *)event));
+ gdk_event_copy(event));
return TRUE;
}
recursed = TRUE;
+ GSource *busy_timeout = g_timeout_source_new(500); /* ms */
+ g_source_set_callback(busy_timeout, teco_interface_busy_timeout_cb, NULL, NULL);
+ g_source_attach(busy_timeout, NULL);
+
teco_memory_start_limiting();
- g_queue_push_tail(teco_interface.event_queue, gdk_event_copy((GdkEvent *)event));
+ g_queue_push_tail(teco_interface.event_queue, gdk_event_copy(event));
GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window));
do {
+ g_autoptr(GError) error = NULL;
+
/*
* The event queue might be filled when pressing keys when SciTECO
* is busy executing code.
@@ -1334,10 +1382,37 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us
*/
gdk_window_freeze_updates(top_window);
+ const teco_view_t *last_view = teco_interface_current_view;
+ sptr_t last_pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0);
+
teco_interrupted = FALSE;
- teco_interface_handle_key_press(&event->key, &error);
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ teco_interface_handle_key_press(&event->key, &error);
+ break;
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ teco_interface_handle_mouse_button(&event->button, &error);
+ break;
+ case GDK_SCROLL:
+ teco_interface_handle_scroll(&event->scroll, &error);
+ break;
+ default:
+ g_assert_not_reached();
+ }
teco_interrupted = FALSE;
+ teco_interface_refresh(teco_interface_current_view != last_view);
+ /*
+ * Scintilla has been patched to avoid any automatic scrolling since that
+ * has been benchmarked to be a very costly operation.
+ * Instead we do it only once after every keypress.
+ */
+ if (last_pos != teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0))
+ teco_interface_ssm(SCI_SCROLLCARET, 0, 0);
+
gdk_window_thaw_updates(top_window);
if (g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT)) {
@@ -1355,10 +1430,36 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us
teco_memory_stop_limiting();
+ g_source_destroy(busy_timeout);
+ g_source_unref(busy_timeout);
+ teco_interface_set_cursor(teco_interface.event_box_widget, "text");
+
recursed = FALSE;
return TRUE;
}
+static void
+teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpointer user_data)
+{
+ g_assert(len >= teco_interface.popup_prefix_len);
+ const teco_string_t insert = {str+teco_interface.popup_prefix_len, len-teco_interface.popup_prefix_len};
+ teco_machine_t *machine = &teco_cmdline.machine.parent;
+
+ const teco_view_t *last_view = teco_interface_current_view;
+
+ /*
+ * NOTE: It shouldn't really be necessary to catch TECO_ERROR_QUIT here.
+ * A auto completion should never result in program termination.
+ */
+ if (machine->current->insert_completion_cb &&
+ !machine->current->insert_completion_cb(machine, &insert, NULL))
+ return;
+ teco_interface_popup_clear();
+ teco_interface_cmdline_update(&teco_cmdline);
+
+ teco_interface_refresh(teco_interface_current_view != last_view);
+}
+
static gboolean
teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer user_data)
{
@@ -1374,7 +1475,7 @@ teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer
close_event->key.window = gtk_widget_get_parent_window(widget);
close_event->key.keyval = GDK_KEY_Close;
- return teco_interface_key_pressed_cb(widget, &close_event->key, NULL);
+ return teco_interface_input_cb(widget, close_event, NULL);
}
static gboolean
@@ -1386,5 +1487,5 @@ teco_interface_sigterm_handler(gpointer user_data)
g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS);
close_event->key.keyval = GDK_KEY_Close;
- return teco_interface_key_pressed_cb(teco_interface.window, &close_event->key, NULL);
+ return teco_interface_input_cb(teco_interface.window, close_event, NULL);
}