diff options
Diffstat (limited to 'src/interface-gtk/interface.c')
-rw-r--r-- | src/interface-gtk/interface.c | 329 |
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); } |