diff options
Diffstat (limited to 'src/interface-gtk')
| -rw-r--r-- | src/interface-gtk/gtk-info-popup.c | 21 | ||||
| -rw-r--r-- | src/interface-gtk/gtk-info-popup.h | 2 | ||||
| -rw-r--r-- | src/interface-gtk/gtk-label.c | 60 | ||||
| -rw-r--r-- | src/interface-gtk/gtk-label.h | 6 | ||||
| -rw-r--r-- | src/interface-gtk/interface.c | 461 | ||||
| -rw-r--r-- | src/interface-gtk/view.c | 6 |
6 files changed, 315 insertions, 241 deletions
diff --git a/src/interface-gtk/gtk-info-popup.c b/src/interface-gtk/gtk-info-popup.c index aaa0a65..769f772 100644 --- a/src/interface-gtk/gtk-info-popup.c +++ b/src/interface-gtk/gtk-info-popup.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 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 @@ -37,6 +37,7 @@ typedef struct { teco_stailq_entry_t entry; teco_popup_entry_type_t type; + /** entry name or empty string for the "(Unnamed)" buffer */ teco_string_t name; gboolean highlight; } teco_popup_entry_t; @@ -109,10 +110,10 @@ teco_gtk_info_popup_activated_cb(GtkFlowBox *box, GtkFlowBoxChild *child, gpoint 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)); + 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); + str.data, (gulong)str.len); } static void @@ -249,6 +250,10 @@ teco_gtk_info_popup_new(void) GIcon * teco_gtk_info_popup_get_icon_for_path(const gchar *path, const gchar *fallback_name) { + if (!path || !*path) + /* "(Unnamed)" file */ + return g_icon_new_for_string(fallback_name, NULL); + GIcon *icon = NULL; g_autoptr(GFile) file = g_file_new_for_path(path); @@ -299,7 +304,7 @@ teco_gtk_info_popup_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type, static void teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type, - const gchar *name, gssize len, gboolean highlight) + const gchar *name, gsize len, gboolean highlight) { g_return_if_fail(self != NULL); g_return_if_fail(TECO_IS_GTK_INFO_POPUP(self)); @@ -318,12 +323,8 @@ teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t typ const gchar *fallback = type == TECO_POPUP_FILE ? "text-x-generic" : "folder"; - /* - * `name` is not guaranteed to be null-terminated. - */ - g_autofree gchar *path = len < 0 ? g_strdup(name) : g_strndup(name, len); - - g_autoptr(GIcon) icon = teco_gtk_info_popup_get_icon_for_path(path, fallback); + /* name comes from a teco_string_t and is guaranteed to be null-terminated */ + g_autoptr(GIcon) icon = teco_gtk_info_popup_get_icon_for_path(name, fallback); if (icon) { gint width, height; gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height); diff --git a/src/interface-gtk/gtk-info-popup.h b/src/interface-gtk/gtk-info-popup.h index ad79b84..3ce8e1f 100644 --- a/src/interface-gtk/gtk-info-popup.h +++ b/src/interface-gtk/gtk-info-popup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 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 ef370a2..5052cdc 100644 --- a/src/interface-gtk/gtk-label.c +++ b/src/interface-gtk/gtk-label.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 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 @@ -32,7 +32,9 @@ #include "gtk-label.h" -#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16)) +#define TECO_UNNAMED_FILE "(Unnamed)" + +#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16)) struct _TecoGtkLabel { GtkLabel parent_instance; @@ -40,6 +42,7 @@ struct _TecoGtkLabel { PangoColor fg, bg; guint16 fg_alpha, bg_alpha; + /** text backing the label or empty string for "(Unnamed)" buffer */ teco_string_t string; }; @@ -143,7 +146,6 @@ teco_gtk_label_add_highlight_attribs(PangoAttrList *attribs, PangoColor *fg, gui * even in Pango v1.38. * Perhaps, this has been fixed in later versions. */ -#if PANGO_VERSION_CHECK(1,38,0) attr = pango_attr_foreground_alpha_new(fg_alpha); attr->start_index = index; attr->end_index = index + len; @@ -153,7 +155,6 @@ teco_gtk_label_add_highlight_attribs(PangoAttrList *attribs, PangoColor *fg, gui attr->start_index = index; attr->end_index = index + len; pango_attr_list_insert(attribs, attr); -#endif attr = pango_attr_foreground_new(fg->red, fg->green, fg->blue); attr->start_index = index; @@ -253,25 +254,52 @@ teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len) teco_string_clear(&self->string); teco_string_init(&self->string, str, len < 0 ? strlen(str) : len); - g_autofree gchar *plaintext = NULL; + teco_string_t string = self->string; + if (!string.len) { + string.data = TECO_UNNAMED_FILE; + string.len = strlen(string.data); + } - if (self->string.len > 0) { - PangoAttrList *attribs = NULL; + g_autofree gchar *plaintext = NULL; + PangoAttrList *attribs = NULL; - teco_gtk_label_parse_string(self->string.data, self->string.len, - &self->fg, self->fg_alpha, - &self->bg, self->bg_alpha, - &attribs, &plaintext); + teco_gtk_label_parse_string(string.data, string.len, + &self->fg, self->fg_alpha, + &self->bg, self->bg_alpha, + &attribs, &plaintext); - gtk_label_set_attributes(GTK_LABEL(self), attribs); - pango_attr_list_unref(attribs); - } + gtk_label_set_attributes(GTK_LABEL(self), attribs); + pango_attr_list_unref(attribs); gtk_label_set_text(GTK_LABEL(self), plaintext); } -const teco_string_t * +teco_string_t teco_gtk_label_get_text(TecoGtkLabel *self) { - return &self->string; + return self->string; +} + +/** + * Signal that a keypress is expected (after executing ^T) + * by printing the first character in reverse. + * + * @fixme This mimics the current Curses implementation. + * Perhaps better show an icon? + */ +void +teco_gtk_label_highlight_getch(TecoGtkLabel *self) +{ + const gchar *plaintext = gtk_label_get_text(GTK_LABEL(self)); + g_assert(plaintext != NULL); + if (!*plaintext || !strcmp(plaintext, "\u258C")) { + gtk_label_set_text(GTK_LABEL(self), "\u258C"); + } else { + PangoAttrList *attribs = gtk_label_get_attributes(GTK_LABEL(self)); + teco_gtk_label_add_highlight_attribs(attribs, + &self->fg, self->fg_alpha, + &self->bg, self->bg_alpha, + 0, 1); + gtk_label_set_attributes(GTK_LABEL(self), attribs); + } } diff --git a/src/interface-gtk/gtk-label.h b/src/interface-gtk/gtk-label.h index c52d073..ad39c6e 100644 --- a/src/interface-gtk/gtk-label.h +++ b/src/interface-gtk/gtk-label.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 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,9 +27,11 @@ 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); +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, PangoColor *bg, guint16 bg_alpha, PangoAttrList **attribs, gchar **text); + +void teco_gtk_label_highlight_getch(TecoGtkLabel *self); diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c index 045f9d7..0e1507f 100644 --- a/src/interface-gtk/interface.c +++ b/src/interface-gtk/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 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 @@ -19,7 +19,7 @@ #include "config.h" #endif -#include <stdarg.h> +#include <stdlib.h> #include <string.h> #include <signal.h> @@ -28,6 +28,7 @@ #include <glib/gstdio.h> #ifdef G_OS_UNIX +#include <unistd.h> #include <glib-unix.h> #endif @@ -61,6 +62,7 @@ //#define DEBUG static gboolean teco_interface_busy_timeout_cb(gpointer user_data); +static gboolean teco_interface_dump_recovery_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, @@ -69,33 +71,22 @@ static void teco_interface_cmdline_commit_cb(GtkIMContext *context, gchar *str, gpointer user_data); static gboolean teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data); +static void teco_interface_scroll_cb(GtkEventControllerScroll *controller, + double dx, double dy, gpointer data); static void teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, 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; +static gchar teco_interface_get_ansi_key(GdkEventKey *event); -/** - * Interval between polling for keypresses. - * In other words, this is the maximum latency to detect CTRL+C interruptions. - */ -#define TECO_POLL_INTERVAL 100000 /* microseconds */ - -#define UNNAMED_FILE "(Unnamed)" +#define TECO_UNNAMED_FILE "(Unnamed)" #define USER_CSS_FILE ".teco_css" /** printf() format for CSS RGB colors given as guint32 */ #define CSS_COLOR_FORMAT "#%06" G_GINT32_MODIFIER "X" -/** Style used for the asterisk at the beginning of the command line */ -#define STYLE_ASTERISK 16 - -/** Indicator number used for control characters in the command line */ -#define INDIC_CONTROLCHAR (INDIC_CONTAINER+0) -/** Indicator number used for the rubbed out part of the command line */ -#define INDIC_RUBBEDOUT (INDIC_CONTAINER+1) - /** Convert Scintilla-style BGR color triple to RGB. */ static inline guint32 teco_bgr2rgb(guint32 bgr) @@ -113,9 +104,10 @@ static struct { TECO_INFO_TYPE_BUFFER_DIRTY, TECO_INFO_TYPE_QREG } info_type; + /* current document's name or empty string for "(Unnamed)" buffer */ teco_string_t info_current; - gboolean no_csd; + gboolean no_csd, detach; gint xembed_id; GtkWidget *info_bar_widget; @@ -124,11 +116,11 @@ static struct { GtkWidget *info_name_widget; GtkWidget *event_box_widget; + GtkEventController *scroll_controller; GtkWidget *message_bar_widget; GtkWidget *message_widget; - teco_view_t *cmdline_view; GtkIMContext *input_method; GtkWidget *popup_widget; @@ -142,6 +134,26 @@ static struct { void teco_interface_init(void) { +#ifdef G_OS_UNIX + if (teco_interface.detach) { + /* + * NOTE: There is also daemon() on BSD/Linux, + * but the following should be more portable. + */ + pid_t pid = fork(); + g_assert(pid >= 0); + if (pid != 0) + /* parent process */ + exit(EXIT_SUCCESS); + + setsid(); + + g_freopen("/dev/null", "r", stdin); + g_freopen("/dev/null", "a+", stdout); + g_freopen("/dev/null", "a+", stderr); + } +#endif + /* * gtk_init() is not necessary when using gtk_get_option_group(), * but this will open the default display. @@ -157,10 +169,10 @@ teco_interface_init(void) * clipboards/selections are supported on this system, * so we register only some default ones. */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); teco_interface.event_queue = g_queue_new(); @@ -190,7 +202,7 @@ teco_interface_init(void) */ teco_interface.info_bar_widget = gtk_header_bar_new(); gtk_widget_set_name(teco_interface.info_bar_widget, "sciteco-info-bar"); - teco_interface.info_name_widget = teco_gtk_label_new(NULL, 0); + teco_interface.info_name_widget = teco_gtk_label_new("", 0); gtk_widget_set_valign(teco_interface.info_name_widget, GTK_ALIGN_CENTER); /* eases writing portable fallback.css that avoids CSS element names */ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget), @@ -230,7 +242,7 @@ teco_interface_init(void) /* * Overlay widget will allow overlaying the Scintilla view * and message widgets with the info popup. - * Therefore overlay_vbox (containing the view and popup) + * Therefore overlay_vbox (containing the view and message line) * will be the main child of the overlay. */ GtkWidget *overlay_widget = gtk_overlay_new(); @@ -253,22 +265,30 @@ teco_interface_init(void) gint events = gtk_widget_get_events(teco_interface.event_box_widget); events |= GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | - GDK_SCROLL_MASK; + GDK_SCROLL_MASK | GDK_SMOOTH_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); + + /* + * On some platforms only GDK_SCROLL_SMOOTH events are reported, which are hard + * to translate to discrete scroll events, as required by the `4EJ` API. + * This work is therefore delegated to a scroll controller. + */ + teco_interface.scroll_controller = gtk_event_controller_scroll_new(teco_interface.event_box_widget, + GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | + GTK_EVENT_CONTROLLER_SCROLL_DISCRETE); + g_signal_connect(teco_interface.scroll_controller, "scroll", + G_CALLBACK(teco_interface_scroll_cb), NULL); teco_interface.message_bar_widget = gtk_info_bar_new(); gtk_widget_set_name(teco_interface.message_bar_widget, "sciteco-message-bar"); GtkWidget *message_bar_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(teco_interface.message_bar_widget)); - /* NOTE: Messages are always pre-canonicalized */ - teco_interface.message_widget = gtk_label_new(NULL); + teco_interface.message_widget = teco_gtk_label_new(NULL, 0); /* eases writing portable fallback.css that avoids CSS element names */ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.message_widget), "label"); @@ -281,23 +301,9 @@ teco_interface_init(void) gtk_container_add(GTK_CONTAINER(overlay_widget), overlay_vbox); gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0); - teco_interface.cmdline_view = teco_view_new(); - teco_view_setup(teco_interface.cmdline_view); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETUNDOCOLLECTION, FALSE, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETVSCROLLBAR, FALSE, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINTYPEN, 1, SC_MARGIN_TEXT); - teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETSTYLE, 0, STYLE_ASTERISK); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINWIDTHN, 1, - teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTWIDTH, STYLE_ASTERISK, (sptr_t)"*")); - teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*"); - /* only required as long as we avoid ordinary character representations */ - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_CONTROLCHAR, INDIC_ROUNDBOX); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETALPHA, INDIC_CONTROLCHAR, 128); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_RUBBEDOUT, INDIC_STRIKE); - /* 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 = GTK_WIDGET(teco_interface.cmdline_view); + teco_cmdline_init(); + + GtkWidget *cmdline_widget = GTK_WIDGET(teco_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); @@ -332,10 +338,6 @@ teco_interface_init(void) */ gtk_widget_set_can_focus(teco_interface.message_widget, FALSE); gtk_widget_set_can_focus(teco_interface.info_name_widget, FALSE); - - teco_cmdline_t empty_cmdline; - memset(&empty_cmdline, 0, sizeof(empty_cmdline)); - teco_interface_cmdline_update(&empty_cmdline); } static void @@ -361,6 +363,11 @@ teco_interface_get_options(void) G_OPTION_ARG_INT, &teco_interface.xembed_id, "Embed into an existing X11 Window.", "ID"}, #endif +#ifdef G_OS_UNIX + {"detach", 'd', G_OPTION_FLAG_IN_MAIN, + G_OPTION_ARG_NONE, &teco_interface.detach, + "Detach from controlling terminal (daemonize).", NULL}, +#endif {NULL} }; @@ -379,7 +386,7 @@ teco_interface_get_options(void) void teco_interface_init_color(guint color, guint32 rgb) {} void -teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) +teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len) { /* * The message types are chosen such that there is a CSS class @@ -395,21 +402,11 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) g_assert(type < G_N_ELEMENTS(type2gtk)); - gchar buf[256]; - - /* - * stdio_vmsg() leaves `ap` undefined and we are expected - * to do the same and behave like vprintf(). - */ - va_list aq; - va_copy(aq, ap); - teco_interface_stdio_vmsg(type, fmt, ap); - g_vsnprintf(buf, sizeof(buf), fmt, aq); - va_end(aq); + teco_interface_stdio_msg(type, str, len); gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget), type2gtk[type]); - gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), buf); + teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.message_widget), str, len); if (type == TECO_MSG_ERROR) gtk_widget_error_bell(teco_interface.window); @@ -420,7 +417,100 @@ teco_interface_msg_clear(void) { gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget), GTK_MESSAGE_QUESTION); - gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), ""); + teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.message_widget), "", 0); +} + +static void +teco_interface_getch_commit_cb(GtkIMContext *context, gchar *str, gpointer user_data) +{ + teco_int_t *cp = user_data; + + /* + * FIXME: What if str contains several characters? + */ + *cp = g_utf8_get_char_validated(str, -1); + g_assert(*cp >= 0); + gtk_main_quit(); +} + +static gboolean +teco_interface_getch_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) +{ + teco_int_t *cp = user_data; + + g_assert(event->type == GDK_KEY_PRESS); + + switch (event->key.keyval) { + case GDK_KEY_Escape: *cp = '\e'; break; + case GDK_KEY_BackSpace: *cp = TECO_CTL_KEY('H'); break; + case GDK_KEY_Tab: *cp = '\t'; break; + case GDK_KEY_Return: *cp = '\n'; break; + default: + /* + * NOTE: Alt-Gr key-combinations are sometimes reported as + * Ctrl+Alt, so we filter those out. + */ + if ((event->key.state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) == GDK_CONTROL_MASK && + (*cp = teco_interface_get_ansi_key(&event->key))) { + *cp = TECO_CTL_KEY(g_ascii_toupper(*cp)); + switch (*cp) { + case TECO_CTL_KEY('C'): + teco_interrupted = TRUE; + /* fall through */ + case TECO_CTL_KEY('D'): + *cp = -1; + } + break; + } + + gtk_im_context_filter_keypress(teco_interface.input_method, &event->key); + return TRUE; + } + + gtk_main_quit(); + return TRUE; +} + +teco_int_t +teco_interface_getch(gboolean widechar) +{ + if (!gtk_main_level()) + /* batch mode */ + return teco_interface_stdio_getch(widechar); + + teco_int_t cp = -1; + gulong key_handler, commit_handler; + + /* temporarily replace the "key-press-event" and "commit" handlers */ + g_signal_handlers_block_by_func(teco_interface.window, + teco_interface_input_cb, NULL); + key_handler = g_signal_connect(teco_interface.window, "key-press-event", + G_CALLBACK(teco_interface_getch_input_cb), &cp); + g_signal_handlers_block_by_func(teco_interface.input_method, + teco_interface_cmdline_commit_cb, NULL); + commit_handler = g_signal_connect(teco_interface.input_method, "commit", + G_CALLBACK(teco_interface_getch_commit_cb), &cp); + + /* + * Highlights the first character in the label. + * This mimics what the Curses UI does. + * Is there a better way to signal that we expect input? + */ + teco_gtk_label_highlight_getch(TECO_GTK_LABEL(teco_interface.message_widget)); + + GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window)); + gdk_window_thaw_updates(top_window); + + gtk_main(); + + gdk_window_freeze_updates(top_window); + + g_signal_handler_disconnect(teco_interface.input_method, commit_handler); + g_signal_handlers_unblock_by_func(teco_interface.input_method, teco_interface_cmdline_commit_cb, NULL); + g_signal_handler_disconnect(teco_interface.window, key_handler); + g_signal_handlers_unblock_by_func(teco_interface.window, teco_interface_input_cb, NULL); + + return cp; } void @@ -439,8 +529,13 @@ teco_interface_refresh_info(void) gtk_style_context_remove_class(style, "dirty"); g_auto(teco_string_t) info_current_temp; - teco_string_init(&info_current_temp, - teco_interface.info_current.data, teco_interface.info_current.len); + + if (!teco_interface.info_current.len) + teco_string_init(&info_current_temp, TECO_UNNAMED_FILE, strlen(TECO_UNNAMED_FILE)); + else + teco_string_init(&info_current_temp, + teco_interface.info_current.data, teco_interface.info_current.len); + if (teco_interface.info_type == TECO_INFO_TYPE_BUFFER_DIRTY) teco_string_append_c(&info_current_temp, '*'); teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.info_name_widget), @@ -508,92 +603,18 @@ teco_interface_info_update_qreg(const teco_qreg_t *reg) void teco_interface_info_update_buffer(const teco_buffer_t *buffer) { - const gchar *filename = buffer->filename ? : UNNAMED_FILE; - teco_string_clear(&teco_interface.info_current); - teco_string_init(&teco_interface.info_current, filename, strlen(filename)); - teco_interface.info_type = buffer->dirty ? TECO_INFO_TYPE_BUFFER_DIRTY - : TECO_INFO_TYPE_BUFFER; -} - -/** - * Insert a single character into the command line. - * - * @fixme - * Control characters should be inserted verbatim since the Scintilla - * representations of them should be preferred. - * However, Scintilla would break the line on every CR/LF and there is - * currently no way to prevent this. - * Scintilla needs to be patched. - * - * @see teco_view_set_representations() - * @see teco_curses_format_str() - */ -static void -teco_interface_cmdline_insert_c(gchar chr) -{ - gchar buffer[3+1] = ""; - - /* - * NOTE: This mapping is similar to teco_view_set_representations() - */ - switch (chr) { - case '\e': strcpy(buffer, "$"); break; - case '\r': strcpy(buffer, "CR"); break; - case '\n': strcpy(buffer, "LF"); break; - case '\t': strcpy(buffer, "TAB"); break; - default: - if (TECO_IS_CTL(chr)) { - buffer[0] = '^'; - buffer[1] = TECO_CTL_ECHO(chr); - buffer[2] = '\0'; - } - } - - if (*buffer) { - gsize len = strlen(buffer); - teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, len, (sptr_t)buffer); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_CONTROLCHAR, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE, - teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - len, len); - } else { - teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, 1, (sptr_t)&chr); - } -} - -void -teco_interface_cmdline_update(const teco_cmdline_t *cmdline) -{ - /* - * We don't know if the new command line is similar to - * the old one, so we can just as well rebuild it. - * - * NOTE: teco_view_ssm() already locks the GDK lock. - */ - teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALL, 0, 0); - - /* format effective command line */ - for (guint i = 0; i < cmdline->effective_len; i++) - teco_interface_cmdline_insert_c(cmdline->str.data[i]); - - /* cursor should be after effective command line */ - guint pos = teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_GOTOPOS, pos, 0); - - /* format rubbed out command line */ - for (guint i = cmdline->effective_len; i < cmdline->str.len; i++) - teco_interface_cmdline_insert_c(cmdline->str.data[i]); - - teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_RUBBEDOUT, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE, pos, - teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - pos); - - teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 0, 0); + teco_string_init(&teco_interface.info_current, buffer->filename, + buffer->filename ? strlen(buffer->filename) : 0); + teco_interface.info_type = buffer->state > TECO_BUFFER_CLEAN + ? TECO_INFO_TYPE_BUFFER_DIRTY : TECO_INFO_TYPE_BUFFER; } static GdkAtom teco_interface_get_selection_by_name(const gchar *name) { + g_assert(*name != '\0'); + /* * We can use gdk_atom_intern() to support arbitrary X11 selection * names. However, since we cannot find out which selections are @@ -602,11 +623,9 @@ teco_interface_get_selection_by_name(const gchar *name) * Checking them here avoids expensive X server roundtrips. */ switch (*name) { - case '\0': return GDK_NONE; case 'P': return GDK_SELECTION_PRIMARY; case 'S': return GDK_SELECTION_SECONDARY; case 'C': return GDK_SELECTION_CLIPBOARD; - default: break; } return gdk_atom_intern(name, FALSE); @@ -767,6 +786,27 @@ teco_interface_is_interrupted(void) return teco_interrupted != FALSE; } +void +teco_interface_refresh(gboolean force) +{ + if (!gtk_main_level()) /* batch mode */ + return; + + if (G_UNLIKELY(force)) + gtk_widget_queue_draw(teco_interface.window); + + GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window)); + gdk_window_thaw_updates(top_window); + + /* + * FIXME: Why do we need two iterations to see any updates? + */ + for (gint i = 0; i < 2; i++) + gtk_main_iteration_do(FALSE); + + gdk_window_freeze_updates(top_window); +} + static void teco_interface_set_css_variables(teco_view_t *view) { @@ -776,40 +816,6 @@ teco_interface_set_css_variables(teco_view_t *view) guint32 calltip_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_CALLTIP, 0); /* - * FIXME: Font and colors of Scintilla views cannot be set via CSS. - * But some day, there will be a way to send messages to the commandline view - * from SciTECO code via ES. - * Configuration will then be in the hands of color schemes. - * - * NOTE: We don't actually know apriori how large the font_size buffer should be, - * but luckily SCI_STYLEGETFONT with a sptr==0 will return only the size. - * This is undocumented in the Scintilla docs. - */ - g_autofree gchar *font_name = g_malloc(teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, 0) + 1); - teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, (sptr_t)font_name); - - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_DEFAULT, default_fg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_DEFAULT, default_bg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)font_name); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETSIZE, STYLE_DEFAULT, - teco_view_ssm(view, SCI_STYLEGETSIZE, STYLE_DEFAULT, 0)); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLECLEARALL, 0, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_CALLTIP, calltip_fg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_CALLTIP, calltip_bg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETCARETFORE, - teco_view_ssm(view, SCI_GETCARETFORE, 0, 0), 0); - /* used for the asterisk at the beginning of the command line */ - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBOLD, STYLE_ASTERISK, TRUE); - /* used for character representations */ - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_CONTROLCHAR, default_fg_color); - /* used for the rubbed out command line */ - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_RUBBEDOUT, default_fg_color); - /* this somehow gets reset */ - teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*"); - - guint text_height = teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTHEIGHT, 0, 0); - - /* * Generates a CSS that sets some predefined color variables. * This effectively "exports" Scintilla styles into the CSS * world. @@ -835,16 +841,17 @@ teco_interface_set_css_variables(teco_view_t *view) gtk_css_provider_load_from_data(teco_interface.css_var_provider, css, -1, NULL); /* - * The font and size of the commandline view might have changed, + * The font and size and height of the command-line view might have changed, * so we resize it. * 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(GTK_WIDGET(teco_interface.cmdline_view), -1, text_height); + g_assert(teco_cmdline.height > 0); + gtk_widget_set_size_request(GTK_WIDGET(teco_cmdline.view), -1, + teco_cmdline.height*teco_cmdline_ssm(SCI_TEXTHEIGHT, 0, 0)); } static void -teco_interface_refresh(gboolean current_view_changed) +teco_interface_update(gboolean current_view_changed) { /* * The styles configured via Scintilla might change @@ -1116,9 +1123,6 @@ 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; @@ -1157,7 +1161,7 @@ teco_interface_event_loop(GError **error) if (!scitecoconfig_reg->vtable->get_string(scitecoconfig_reg, &scitecoconfig.data, &scitecoconfig.len, NULL, error)) return FALSE; - if (teco_string_contains(&scitecoconfig, '\0')) { + if (teco_string_contains(scitecoconfig, '\0')) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, "Null-character not allowed in filenames"); return FALSE; @@ -1195,7 +1199,7 @@ teco_interface_event_loop(GError **error) GTK_STYLE_PROVIDER(user_css_provider), GTK_STYLE_PROVIDER_PRIORITY_USER); - teco_interface_refresh(TRUE); + teco_interface_update(TRUE); gtk_widget_show_all(teco_interface.window); /* don't show popup by default */ @@ -1215,7 +1219,7 @@ teco_interface_event_loop(GError **error) * This is not necessary on Windows since the icon included * as a resource will be used by default. */ - static const gchar *icon_files[] = { + static const gchar *const icon_files[] = { "sciteco-48.png", "sciteco-32.png", "sciteco-16.png" }; GList *icon_list = NULL; @@ -1249,6 +1253,10 @@ teco_interface_event_loop(GError **error) g_unix_signal_add(SIGTERM, teco_interface_sigterm_handler, NULL); #endif + /* the interval might have been changed in the profile */ + g_timeout_add_seconds(teco_ring_recovery_interval, + teco_interface_dump_recovery_cb, NULL); + /* don't limit while waiting for input as this might be a busy operation */ teco_memory_stop_limiting(); @@ -1274,6 +1282,8 @@ teco_interface_cleanup(void) if (teco_interface.window) gtk_widget_destroy(teco_interface.window); + if (teco_interface.scroll_controller) + g_object_unref(teco_interface.scroll_controller); scintilla_release_resources(); @@ -1305,6 +1315,20 @@ teco_interface_busy_timeout_cb(gpointer user_data) return G_SOURCE_REMOVE; } +static gboolean +teco_interface_dump_recovery_cb(gpointer user_data) +{ + teco_ring_dump_recovery(); + + /* + * The backup interval could have changed (6EJ). + * New intervals will not be effective immediately, though. + */ + g_timeout_add_seconds(teco_ring_recovery_interval, + teco_interface_dump_recovery_cb, NULL); + return G_SOURCE_REMOVE; +} + static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data) { @@ -1312,17 +1336,12 @@ teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data) 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. - */ +/** Called when the commandline widget is resized */ static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, gpointer user_data) { - teco_view_ssm(teco_interface.cmdline_view, SCI_SETXCARETPOLICY, - CARET_SLOP | CARET_EVEN, allocation->width/2); + teco_cmdline_resized(allocation->width); } static gboolean @@ -1415,7 +1434,9 @@ teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) } teco_interrupted = FALSE; - teco_interface_refresh(teco_interface_current_view != last_view); + teco_interface_update(teco_interface_current_view != last_view); + /* always expand folds, even after mouse clicks */ + teco_interface_unfold(); /* * Scintilla has been patched to avoid any automatic scrolling since that * has been benchmarked to be a very costly operation. @@ -1458,10 +1479,34 @@ teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) } static void +teco_interface_scroll_cb(GtkEventControllerScroll *controller, double dx, double dy, gpointer data) +{ + /* + * FIXME: Using teco_interface.event_box_widget will cause crashes in + * teco_interface_input_cb() + */ + GtkWidget *widget = teco_interface.window; + + /* + * Emulate a GDK_SCROLL event to make use of the existing + * event queuing in teco_interface_input_cb(). + */ + g_autoptr(GdkEvent) scroll_event = gdk_event_new(GDK_SCROLL); + scroll_event->scroll.window = gtk_widget_get_parent_window(widget); + scroll_event->scroll.direction = dy > 0 ? GDK_SCROLL_DOWN : GDK_SCROLL_UP; + scroll_event->scroll.delta_x = dx; + scroll_event->scroll.delta_y = dy; + + teco_interface_input_cb(widget, scroll_event, NULL); +} + +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}; + /* str is an empty string for the "(Unnamed)" buffer */ + 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; @@ -1471,12 +1516,12 @@ teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpoint * A auto completion should never result in program termination. */ if (machine->current->insert_completion_cb && - !machine->current->insert_completion_cb(machine, &insert, NULL)) + !machine->current->insert_completion_cb(machine, insert, NULL)) return; teco_interface_popup_clear(); - teco_interface_cmdline_update(&teco_cmdline); + teco_cmdline_update(); - teco_interface_refresh(teco_interface_current_view != last_view); + teco_interface_update(teco_interface_current_view != last_view); } static gboolean @@ -1484,7 +1529,6 @@ teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer { /* * Emulate that the "close" key was pressed - * which may then be handled by the execution thread * which invokes the appropriate "function key macro" * if it exists. Its default action will ensure that * the execution thread shuts down and the main loop @@ -1503,8 +1547,11 @@ teco_interface_sigterm_handler(gpointer user_data) /* * Similar to window deletion - emulate "close" key press. */ + GtkWidget *widget = teco_interface.window; + g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS); + close_event->key.window = gtk_widget_get_parent_window(widget); close_event->key.keyval = GDK_KEY_Close; - return teco_interface_input_cb(teco_interface.window, close_event, NULL); + return teco_interface_input_cb(widget, close_event, NULL); } diff --git a/src/interface-gtk/view.c b/src/interface-gtk/view.c index 81db3d7..3a18f33 100644 --- a/src/interface-gtk/view.c +++ b/src/interface-gtk/view.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 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 @@ -102,12 +102,8 @@ teco_view_new(void) gint events = gtk_widget_get_events(GTK_WIDGET(ctx)); events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK | GDK_TOUCH_MASK | -#ifdef GDK_VERSION_3_18 GDK_TOUCHPAD_GESTURE_MASK | -#endif -#ifdef GDK_VERSION_3_22 GDK_TABLET_PAD_MASK | -#endif GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); gtk_widget_set_events(GTK_WIDGET(ctx), events); |
