diff options
Diffstat (limited to 'src/interface-gtk')
-rw-r--r-- | src/interface-gtk/Makefile.am | 2 | ||||
-rw-r--r-- | src/interface-gtk/gtk-info-popup.c | 57 | ||||
-rw-r--r-- | src/interface-gtk/gtk-info-popup.h | 2 | ||||
-rw-r--r-- | src/interface-gtk/gtk-label.c | 8 | ||||
-rw-r--r-- | src/interface-gtk/gtk-label.h | 3 | ||||
-rw-r--r-- | src/interface-gtk/interface.c | 329 | ||||
-rw-r--r-- | src/interface-gtk/view.c | 126 |
7 files changed, 398 insertions, 129 deletions
diff --git a/src/interface-gtk/Makefile.am b/src/interface-gtk/Makefile.am index 50e5311..e731a21 100644 --- a/src/interface-gtk/Makefile.am +++ b/src/interface-gtk/Makefile.am @@ -4,7 +4,7 @@ AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \ AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value noinst_LTLIBRARIES = libsciteco-interface.la -libsciteco_interface_la_SOURCES = interface.c \ +libsciteco_interface_la_SOURCES = view.c interface.c \ gtk-info-popup.c gtk-info-popup.h \ gtk-label.c gtk-label.h diff --git a/src/interface-gtk/gtk-info-popup.c b/src/interface-gtk/gtk-info-popup.c index 4e25224..aaa0a65 100644 --- a/src/interface-gtk/gtk-info-popup.c +++ b/src/interface-gtk/gtk-info-popup.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 @@ -47,12 +47,15 @@ struct _TecoGtkInfoPopup { GtkAdjustment *hadjustment, *vadjustment; GtkWidget *flow_box; + GdkCursor *cursor; /*< pointer/hand cursor */ GStringChunk *chunk; teco_stailq_head_t list; guint idle_id; gboolean frozen; }; +static guint teco_gtk_info_popup_clicked_signal; + static gboolean teco_gtk_info_popup_scroll_event(GtkWidget *widget, GdkEventScroll *event); static void teco_gtk_info_popup_show(GtkWidget *widget); static void teco_gtk_info_popup_vadjustment_changed(GtkAdjustment *vadjustment, GtkWidget *scrollbar); @@ -72,6 +75,9 @@ teco_gtk_info_popup_finalize(GObject *obj_self) while ((entry = teco_stailq_remove_head(&self->list))) g_free(entry); + if (self->cursor) + g_object_unref(self->cursor); + /* chain up to parent class */ G_OBJECT_CLASS(teco_gtk_info_popup_parent_class)->finalize(obj_self); } @@ -82,6 +88,31 @@ teco_gtk_info_popup_class_init(TecoGtkInfoPopupClass *klass) GTK_WIDGET_CLASS(klass)->scroll_event = teco_gtk_info_popup_scroll_event; GTK_WIDGET_CLASS(klass)->show = teco_gtk_info_popup_show; G_OBJECT_CLASS(klass)->finalize = teco_gtk_info_popup_finalize; + + teco_gtk_info_popup_clicked_signal = + g_signal_new("clicked", G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_ULONG); +} + +static void +teco_gtk_info_popup_activated_cb(GtkFlowBox *box, GtkFlowBoxChild *child, gpointer user_data) +{ + TecoGtkInfoPopup *popup = TECO_GTK_INFO_POPUP(user_data); + + /* + * Find the TecoGtkLabel in the flow box child. + */ + GtkWidget *hbox = gtk_bin_get_child(GTK_BIN(child)); + g_autoptr(GList) child_list = gtk_container_get_children(GTK_CONTAINER(hbox)); + GList *entry; + for (entry = child_list; entry != NULL && !TECO_IS_GTK_LABEL(entry->data); entry = g_list_next(entry)); + g_assert(entry != NULL); + const teco_string_t *str = teco_gtk_label_get_text(TECO_GTK_LABEL(entry->data)); + + g_signal_emit(popup, teco_gtk_info_popup_clicked_signal, 0, + str->data, (gulong)str->len); } static void @@ -106,6 +137,8 @@ teco_gtk_info_popup_init(TecoGtkInfoPopup *self) G_CALLBACK(teco_gtk_info_popup_vadjustment_changed), scrollbar); self->flow_box = gtk_flow_box_new(); + g_signal_connect(self->flow_box, "child-activated", + G_CALLBACK(teco_gtk_info_popup_activated_cb), self); /* take as little height as necessary */ gtk_orientable_set_orientation(GTK_ORIENTABLE(self->flow_box), GTK_ORIENTATION_HORIZONTAL); @@ -311,12 +344,6 @@ teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t typ gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_widget_set_valign(label, GTK_ALIGN_CENTER); - /* - * FIXME: This makes little sense once we've got mouse support. - * But for the time being, it's a useful setting. - */ - gtk_label_set_selectable(GTK_LABEL(label), TRUE); - switch (type) { case TECO_POPUP_PLAIN: gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_START); @@ -331,6 +358,16 @@ teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t typ gtk_widget_show_all(hbox); gtk_container_add(GTK_CONTAINER(self->flow_box), hbox); + + GtkWidget *flow_box_child = gtk_widget_get_parent(hbox); + g_assert(GTK_IS_FLOW_BOX_CHILD(flow_box_child)); + GdkWindow *window = gtk_widget_get_window(flow_box_child); + g_assert(window != NULL); + + if (G_UNLIKELY(!self->cursor)) + /* we only initialize it now after guaranteed widget realization */ + self->cursor = gdk_cursor_new_from_name(gdk_window_get_display(window), "pointer"); + gdk_window_set_cursor(window, self->cursor); } static gboolean @@ -417,12 +454,10 @@ teco_gtk_info_popup_scroll_page(TecoGtkInfoPopup *self) * Adjust this so only complete entries are shown. * Effectively, this rounds down to the line height. */ - GList *child_list = gtk_container_get_children(GTK_CONTAINER(self->flow_box)); - if (child_list) { + g_autoptr(GList) child_list = gtk_container_get_children(GTK_CONTAINER(self->flow_box)); + if (child_list) new_value -= (gint)new_value % gtk_widget_get_allocated_height(GTK_WIDGET(child_list->data)); - g_list_free(child_list); - } /* clip to the maximum possible value */ new_value = MIN(new_value, gtk_adjustment_get_upper(adj)); diff --git a/src/interface-gtk/gtk-info-popup.h b/src/interface-gtk/gtk-info-popup.h index c3a62ec..ad79b84 100644 --- a/src/interface-gtk/gtk-info-popup.h +++ b/src/interface-gtk/gtk-info-popup.h @@ -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 diff --git a/src/interface-gtk/gtk-label.c b/src/interface-gtk/gtk-label.c index 50cd345..ef370a2 100644 --- a/src/interface-gtk/gtk-label.c +++ b/src/interface-gtk/gtk-label.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 @@ -269,3 +269,9 @@ teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len) gtk_label_set_text(GTK_LABEL(self), plaintext); } + +const teco_string_t * +teco_gtk_label_get_text(TecoGtkLabel *self) +{ + return &self->string; +} diff --git a/src/interface-gtk/gtk-label.h b/src/interface-gtk/gtk-label.h index bed6642..c52d073 100644 --- a/src/interface-gtk/gtk-label.h +++ b/src/interface-gtk/gtk-label.h @@ -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 @@ -27,6 +27,7 @@ G_DECLARE_FINAL_TYPE(TecoGtkLabel, teco_gtk_label, TECO, GTK_LABEL, GtkLabel) GtkWidget *teco_gtk_label_new(const gchar *str, gssize len); void teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len); +const teco_string_t *teco_gtk_label_get_text(TecoGtkLabel *self); void teco_gtk_label_parse_string(const gchar *str, gssize len, PangoColor *fg, guint16 fg_alpha, 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); } diff --git a/src/interface-gtk/view.c b/src/interface-gtk/view.c new file mode 100644 index 0000000..ef839d6 --- /dev/null +++ b/src/interface-gtk/view.c @@ -0,0 +1,126 @@ +/* + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <string.h> + +#include <glib.h> + +#include <gtk/gtk.h> + +#include <Scintilla.h> +#include <ScintillaWidget.h> + +#include "view.h" + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(ScintillaObject, g_object_unref) + +#define TECO_TYPE_VIEW teco_view_get_type() +G_DECLARE_FINAL_TYPE(TecoView, teco_view, TECO, VIEW, ScintillaObject) + +struct _TecoView { + ScintillaObject parent_instance; + /** current size allocation */ + GdkRectangle allocation; +}; + +G_DEFINE_TYPE(TecoView, teco_view, SCINTILLA_TYPE_OBJECT) + +static void +teco_view_scintilla_notify_cb(ScintillaObject *sci, gint iMessage, SCNotification *notify) +{ + teco_view_process_notify((teco_view_t *)TECO_VIEW(sci), notify); +} + +/** + * Called when the view is size allocated. + * + * This especially ensures that the caret is visible after startup and when + * opening files on specific lines. + * It's important to scroll the caret only when the size actually changes, + * so we do not interfere with mouse scrolling. + * That callback is invoked even if the size does not change, so that's why + * we have to store the current allocation in teco_view_t. + * Calling it once is unfortunately not sufficient since the window size + * can change during startup. + */ +static void +teco_view_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation) +{ + /* chain to parent class */ + GTK_WIDGET_CLASS(teco_view_parent_class)->size_allocate(widget, allocation); + + TecoView *view = TECO_VIEW(widget); + + if (allocation->width == view->allocation.width && allocation->height == view->allocation.height) + return; + teco_view_ssm((teco_view_t *)view, SCI_SCROLLCARET, 0, 0); + memcpy(&view->allocation, allocation, sizeof(view->allocation)); +} + +teco_view_t * +teco_view_new(void) +{ + TecoView *ctx = TECO_VIEW(g_object_new(TECO_TYPE_VIEW, NULL)); + /* + * We don't want the object to be destroyed + * when it is removed from the vbox. + */ + g_object_ref_sink(ctx); + + scintilla_set_id(SCINTILLA(ctx), 0); + + gtk_widget_set_size_request(GTK_WIDGET(ctx), 500, 300); + + /* + * This disables mouse and key events on this view. + * For some strange reason, masking events on + * the event box does NOT work. + */ + gtk_widget_set_can_focus(GTK_WIDGET(ctx), FALSE); + gint events = gtk_widget_get_events(GTK_WIDGET(ctx)); + events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_SCROLL_MASK | + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); + gtk_widget_set_events(GTK_WIDGET(ctx), events); + + return (teco_view_t *)ctx; +} + +static void +teco_view_class_init(TecoViewClass *klass) +{ + SCINTILLA_CLASS(klass)->notify = teco_view_scintilla_notify_cb; + GTK_WIDGET_CLASS(klass)->size_allocate = teco_view_size_allocate_cb; +} + +static void teco_view_init(TecoView *self) {} + +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(ctx); +} |