/* * Copyright (C) 2012-2022 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #ifdef G_OS_UNIX #include #endif #include #include #include #ifdef GDK_WINDOWING_X11 #include #endif #include #include #include #include "gtk-info-popup.h" #include "gtk-label.h" #include "sciteco.h" #include "error.h" #include "string-utils.h" #include "cmdline.h" #include "qreg.h" #include "ring.h" #include "memory.h" #include "interface.h" //#define DEBUG static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, 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; #define 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) { 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_interface_process_notify(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) { /* * FIXME: It's not entirely clear why g_object_unref() won't do here. * This results in crashes later on because something is still referencing * the widget/GObject. * However, currently displayed views (ctx == teco_interface.current_view_widget) * should have a reference count of 2 and unreffing them should not actually * touch the object until is is removed from the view. */ gtk_widget_destroy(teco_view_get_widget(ctx)); } static struct { GtkCssProvider *css_var_provider; GtkWidget *window; enum { TECO_INFO_TYPE_BUFFER = 0, TECO_INFO_TYPE_BUFFER_DIRTY, TECO_INFO_TYPE_QREG } info_type; teco_string_t info_current; gboolean no_csd; gint xembed_id; GtkWidget *info_bar_widget; GtkWidget *info_image; GtkWidget *info_type_widget; GtkWidget *info_name_widget; GtkWidget *event_box_widget; GtkWidget *message_bar_widget; GtkWidget *message_widget; teco_view_t *cmdline_view; GtkWidget *popup_widget; GtkWidget *current_view_widget; GQueue *event_queue; } teco_interface; void teco_interface_init(void) { /* * gtk_init() is not necessary when using gtk_get_option_group(), * but this will open the default display. * * FIXME: Perhaps it is possible to defer this until we initialize * interactive mode!? */ gtk_init(NULL, NULL); /* * Register clipboard registers. * Unfortunately, we cannot find out which * 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_interface.event_queue = g_queue_new(); #ifdef GDK_WINDOWING_X11 teco_interface.window = teco_interface.xembed_id ? gtk_plug_new(teco_interface.xembed_id) : gtk_window_new(GTK_WINDOW_TOPLEVEL); #else teco_interface.window = gtk_window_new(GTK_WINDOW_TOPLEVEL); #endif g_signal_connect(teco_interface.window, "delete-event", 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); GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); /* * The info bar is tried to be made the title bar of the * window which also disables the default window decorations * (client-side decorations) unless --no-csd was specified. * * NOTE: Client-side decoations could fail, leaving us with a * standard title bar and the info bar with close buttons. * Other window managers have undesirable side-effects. */ 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); 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), "label"); gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget), "name-label"); gtk_label_set_selectable(GTK_LABEL(teco_interface.info_name_widget), TRUE); /* NOTE: Header bar does not resize for multi-line labels */ //gtk_label_set_line_wrap(GTK_LABEL(teco_interface.info_name_widget), TRUE); //gtk_label_set_lines(GTK_LABEL(teco_interface.info_name_widget), 2); gtk_header_bar_set_custom_title(GTK_HEADER_BAR(teco_interface.info_bar_widget), teco_interface.info_name_widget); teco_interface.info_image = gtk_image_new(); gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget), teco_interface.info_image); teco_interface.info_type_widget = gtk_label_new(NULL); gtk_widget_set_valign(teco_interface.info_type_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_type_widget), "label"); gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_type_widget), "type-label"); gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget), teco_interface.info_type_widget); if (teco_interface.xembed_id || teco_interface.no_csd) { /* fall back to adding the info bar as an ordinary widget */ gtk_box_pack_start(GTK_BOX(vbox), teco_interface.info_bar_widget, FALSE, FALSE, 0); } else { /* use client-side decorations */ gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(teco_interface.info_bar_widget), TRUE); gtk_window_set_titlebar(GTK_WINDOW(teco_interface.window), teco_interface.info_bar_widget); } /* * Overlay widget will allow overlaying the Scintilla view * and message widgets with the info popup. * Therefore overlay_vbox (containing the view and popup) * will be the main child of the overlay. */ GtkWidget *overlay_widget = gtk_overlay_new(); GtkWidget *overlay_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); /* * The event box is the parent of all Scintilla views * that should be displayed. * This is handy when adding or removing current views, * enabling and disabling GDK updates and in order to filter * mouse and keyboard events going to Scintilla. */ teco_interface.event_box_widget = gtk_event_box_new(); gtk_event_box_set_above_child(GTK_EVENT_BOX(teco_interface.event_box_widget), TRUE); gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.event_box_widget, TRUE, TRUE, 0); 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); /* 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"); gtk_label_set_selectable(GTK_LABEL(teco_interface.message_widget), TRUE); gtk_label_set_line_wrap(GTK_LABEL(teco_interface.message_widget), TRUE); gtk_container_add(GTK_CONTAINER(message_bar_content), teco_interface.message_widget); gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.message_bar_widget, FALSE, FALSE, 0); 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)"*"); 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); GtkWidget *cmdline_widget = teco_view_get_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); gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(teco_interface.window), vbox); /* * Popup widget will be shown in the bottom * of the overlay widget (i.e. the Scintilla views), * filling the entire width. */ teco_interface.popup_widget = teco_gtk_info_popup_new(); gtk_widget_set_name(teco_interface.popup_widget, "sciteco-info-popup"); 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); /* * FIXME: Nothing can really take the focus, so it will end up in the * selectable labels unless we explicitly prevent it. */ 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); } GOptionGroup * teco_interface_get_options(void) { /* * FIXME: On platforms where you want to disable CSD, you usually * want to disable it always, so it should be configurable in the SciTECO * profile. * On the other hand, you could just install gtk3-nocsd. */ static const GOptionEntry entries[] = { {"no-csd", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, &teco_interface.no_csd, "Disable client-side decorations.", NULL}, #ifdef GDK_WINDOWING_X11 {"xembed", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_INT, &teco_interface.xembed_id, "Embed into an existing X11 Window.", "ID"}, #endif {NULL} }; /* * Parsing the option context with the Gtk option group * will automatically initialize Gtk, but we do not yet * open the default display. */ GOptionGroup *group = gtk_get_option_group(FALSE); g_option_group_add_entries(group, entries); return group; } void teco_interface_init_color(guint color, guint32 rgb) {} void teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) { /* * The message types are chosen such that there is a CSS class * for every one of them. GTK_MESSAGE_OTHER does not have * a CSS class. */ static const GtkMessageType type2gtk[] = { [TECO_MSG_USER] = GTK_MESSAGE_QUESTION, [TECO_MSG_INFO] = GTK_MESSAGE_INFO, [TECO_MSG_WARNING] = GTK_MESSAGE_WARNING, [TECO_MSG_ERROR] = GTK_MESSAGE_ERROR }; 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); 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); if (type == TECO_MSG_ERROR) gtk_widget_error_bell(teco_interface.window); } void 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), ""); } void teco_interface_show_view(teco_view_t *view) { teco_interface_current_view = view; } static void teco_interface_refresh_info(void) { GtkStyleContext *style = gtk_widget_get_style_context(teco_interface.info_bar_widget); gtk_style_context_remove_class(style, "info-qregister"); gtk_style_context_remove_class(style, "info-buffer"); 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_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), info_current_temp.data, info_current_temp.len); g_autofree gchar *info_current_canon = teco_string_echo(info_current_temp.data, info_current_temp.len); const gchar *info_type_str = PACKAGE; g_autoptr(GIcon) icon = NULL; switch (teco_interface.info_type) { case TECO_INFO_TYPE_QREG: gtk_style_context_add_class(style, "info-qregister"); info_type_str = PACKAGE_NAME " - "; gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "QRegister"); gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget), PANGO_ELLIPSIZE_START); /* * FIXME: Perhaps we should use the SciTECO icon for Q-Registers. */ icon = g_icon_new_for_string("emblem-generic", NULL); break; case TECO_INFO_TYPE_BUFFER_DIRTY: gtk_style_context_add_class(style, "dirty"); /* fall through */ case TECO_INFO_TYPE_BUFFER: gtk_style_context_add_class(style, "info-buffer"); info_type_str = PACKAGE_NAME " - "; gtk_label_set_text(GTK_LABEL(teco_interface.info_type_widget), "Buffer"); gtk_label_set_ellipsize(GTK_LABEL(teco_interface.info_name_widget), PANGO_ELLIPSIZE_MIDDLE); icon = teco_gtk_info_popup_get_icon_for_path(teco_interface.info_current.data, "text-x-generic"); break; } g_autofree gchar *title = g_strconcat(info_type_str, info_current_canon, NULL); gtk_window_set_title(GTK_WINDOW(teco_interface.window), title); if (icon) { gint width, height; gtk_icon_size_lookup(GTK_ICON_SIZE_LARGE_TOOLBAR, &width, &height); gtk_image_set_from_gicon(GTK_IMAGE(teco_interface.info_image), icon, GTK_ICON_SIZE_LARGE_TOOLBAR); /* This is necessary so that oversized icons get scaled down. */ gtk_image_set_pixel_size(GTK_IMAGE(teco_interface.info_image), height); } } void teco_interface_info_update_qreg(const teco_qreg_t *reg) { teco_string_clear(&teco_interface.info_current); teco_string_init(&teco_interface.info_current, reg->head.name.data, reg->head.name.len); teco_interface.info_type = TECO_INFO_TYPE_QREG; } 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); teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 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); } static GdkAtom teco_interface_get_selection_by_name(const gchar *name) { /* * We can use gdk_atom_intern() to support arbitrary X11 selection * names. However, since we cannot find out which selections are * registered, we are only providing QRegisters for the three default * selections. * 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); } gboolean teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error) { GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name)); /* * NOTE: function has compatible semantics for str_len < 0. */ gtk_clipboard_set_text(clipboard, str, str_len); return TRUE; } gboolean teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error) { GtkClipboard *clipboard = gtk_clipboard_get(teco_interface_get_selection_by_name(name)); /* * Could return NULL for an empty clipboard. * * FIXME: This converts to UTF8 and we loose the ability * to get clipboard with embedded nulls. */ g_autofree gchar *contents = gtk_clipboard_wait_for_text(clipboard); *len = contents ? strlen(contents) : 0; if (str) *str = g_steal_pointer(&contents); return TRUE; } void teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len, gboolean highlight) { teco_gtk_info_popup_add(TECO_GTK_INFO_POPUP(teco_interface.popup_widget), type, name, name_len, highlight); } void teco_interface_popup_show(void) { 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); } gboolean teco_interface_popup_is_shown(void) { return gtk_widget_get_visible(teco_interface.popup_widget); } void teco_interface_popup_clear(void) { if (gtk_widget_get_visible(teco_interface.popup_widget)) { gtk_widget_hide(teco_interface.popup_widget); teco_gtk_info_popup_clear(TECO_GTK_INFO_POPUP(teco_interface.popup_widget)); } } /** * Whether the execution has been interrupted (CTRL+C). * * This is called regularily, so it is used to drive the * main loop so that we can still process key presses. * * This approach is significantly slower in interactive mode * than executing in a separate thread probably due to the * system call overhead. * But the GDK lock that would be necessary for synchronization * has been deprecated. * * @todo It would be great to have platform-specific optimizations, * so we can detect interruptions without having to drive the Glib * event loop (e.g. using libX11 or Win32 APIs). * There already is a keyboard hook foHTTP/1.1 200 OK Connection: keep-alive Connection: keep-alive Content-Disposition: inline; filename="interface.c" Content-Disposition: inline; filename="interface.c" Content-Length: 42225 Content-Length: 42225 Content-Security-Policy: default-src 'none' Content-Security-Policy: default-src 'none' Content-Type: text/plain; charset=UTF-8 Content-Type: text/plain; charset=UTF-8 Date: Sat, 18 Oct 2025 18:56:47 UTC ETag: "dc7367a074c033373f2bc05a0267057ee8ea8cd7" ETag: "dc7367a074c033373f2bc05a0267057ee8ea8cd7" Expires: Tue, 16 Oct 2035 18:56:47 GMT Expires: Tue, 16 Oct 2035 18:56:47 GMT Last-Modified: Sat, 18 Oct 2025 18:56:47 GMT Last-Modified: Sat, 18 Oct 2025 18:56:47 GMT Server: OpenBSD httpd Server: OpenBSD httpd X-Content-Type-Options: nosniff X-Content-Type-Options: nosniff /* * Copyright (C) 2012-2022 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #ifdef G_OS_UNIX #include #endif #include #include #include #ifdef GDK_WINDOWING_X11 #include #endif #include #include #include #include "gtk-info-popup.h" #include "gtk-label.h" #include "sciteco.h" #include "error.h" #include "string-utils.h" #include "cmdline.h" #include "qreg.h" #include "ring.h" #include "memory.h" #include "interface.h" //#define DEBUG static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, 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; #define 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) { 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_interface_process_notify(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) { /* * FIXME: It's not entirely clear why g_object_unref() won't do here. * This results in crashes later on because something is still referencing * the widget/GObject. * However, currently displayed views (ctx == teco_interface.current_view_widget) * should have a reference count of 2 and unreffing them should not actually * touch the object until is is removed from the view. */ gtk_widget_destroy(teco_view_get_widget(ctx)); } static struct { GtkCssProvider *css_var_provider; GtkWidget *window; enum { TECO_INFO_TYPE_BUFFER = 0, TECO_INFO_TYPE_BUFFER_DIRTY, TECO_INFO_TYPE_QREG } info_type; teco_string_t info_current; gboolean no_csd; gint xembed_id; GtkWidget *info_bar_widget; GtkWidget *info_image; GtkWidget *info_type_widget; GtkWidget *info_name_widget; GtkWidget *event_box_widget; GtkWidget *message_bar_widget; GtkWidget *message_widget; teco_view_t *cmdline_view; GtkWidget *popup_widget; GtkWidget *current_view_widget; GQueue *event_queue; } teco_interface; void teco_interface_init(void) { /* * gtk_init() is not necessary when using gtk_get_option_group(), * but this will open the default display. * * FIXME: Perhaps it is possible to defer this until we initialize * interactive mode!? */ gtk_init(NULL, NULL); /* * Register clipboard registers. * Unfortunately, we cannot find out which * 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_interface.event_queue = g_queue_new(); #ifdef GDK_WINDOWING_X11 teco_interface.window = teco_interface.xembed_id ? gtk_plug_new(teco_interface.xembed_id) : gtk_window_new(GTK_WINDOW_TOPLEVEL); #else teco_interface.window = gtk_window_new(GTK_WINDOW_TOPLEVEL); #endif g_signal_connect(teco_interface.window, "delete-event", 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); GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); /* * The info bar is tried to be made the title bar of the * window which also disables the default window decorations * (client-side decorations) unless --no-csd was specified. * * NOTE: Client-side decoations could fail, leaving us with a * standard title bar and the info bar with close buttons. * Other window managers have undesirable side-effects. */ 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); 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), "label"); gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget), "name-label"); gtk_label_set_selectable(GTK_LABEL(teco_interface.info_name_widget), TRUE); /* NOTE: Header bar does not resize for multi-line labels */ //gtk_label_set_line_wrap(GTK_LABEL(teco_interface.info_name_widget), TRUE); //gtk_label_set_lines(GTK_LABEL(teco_interface.info_name_widget), 2); gtk_header_bar_set_custom_title(GTK_HEADER_BAR(teco_interface.info_bar_widget), teco_interface.info_name_widget); teco_interface.info_image = gtk_image_new(); gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget), teco_interface.info_image); teco_interface.info_type_widget = gtk_label_new(NULL); gtk_widget_set_valign(teco_interface.info_type_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_type_widget), "label"); gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_type_widget), "type-label"); gtk_header_bar_pack_start(GTK_HEADER_BAR(teco_interface.info_bar_widget), teco_interface.info_type_widget); if (teco_interface.xembed_id || teco_interface.no_csd) { /* fall back to adding the info bar as an ordinary widget */ gtk_box_pack_start(GTK_BOX(vbox), teco_interface.info_bar_widget, FALSE, FALSE, 0); } else { /* use client-side decorations */ gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(teco_interface.info_bar_widget), TRUE); gtk_window_set_titlebar(GTK_WINDOW(teco_interface.window), teco_interface.info_bar_widget); } /* * Overlay widget will allow overlaying the Scintilla view * and message widgets with the info popup. * Therefore overlay_vbox (containing the view and popup) * will be the main child of the overlay. */ GtkWidget *overlay_widget = gtk_overlay_new(); GtkWidget *overlay_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); /* * The event box is the parent of all Scintilla views * that should be displayed. * This is handy when adding or removing current views, * enabling and disabling GDK updates and in order to filter * mouse and keyboard events going to Scintilla. */ teco_interface.event_box_widget = gtk_event_box_new(); gtk_event_box_set_above_child(GTK_EVENT_BOX(teco_interface.event_box_widget), TRUE); gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.event_box_widget, TRUE, TRUE, 0); 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); /* 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"); gtk_label_set_selectable(GTK_LABEL(teco_interface.message_widget), TRUE); gtk_label_set_line_wrap(GTK_LABEL(teco_interface.message_widget), TRUE); gtk_container_add(GTK_CONTAINER(message_bar_content), teco_interface.message_widget); gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.message_bar_widget, FALSE, FALSE, 0); 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)"*"); 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); GtkWidget *cmdline_widget = teco_view_get_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); gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(teco_interface.window), vbox); /* * Popup widget will be shown in the bottom * of the overlay widget (i.e. the Scintilla views), * filling the entire width. */ teco_interface.popup_widget = teco_gtk_info_popup_new(); gtk_widget_set_name(teco_interface.popup_widget, "sciteco-info-popup"); 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); /* * FIXME: Nothing can really take the focus, so it will end up in the * selectable labels unless we explicitly prevent it. */ 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); } GOptionGroup * teco_interface_get_options(void) { /* * FIXME: On platforms where you want to disable CSD, you usually * want to disable it always, so it should be configurable in the SciTECO * profile. * On the other hand, you could just install gtk3-nocsd. */ static const GOptionEntry entries[] = { {"no-csd", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, &teco_interface.no_csd, "Disable client-side decorations.", NULL}, #ifdef GDK_WINDOWING_X11 {"xembed", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_INT, &teco_interface.xembed_id, "Embed into an existing X11 Window.", "ID"}, #endif {NULL} }; /* * Parsing the option context with the Gtk option group * will automatically initialize Gtk, but we do not yet * open the default display. */ GOptionGroup *group = gtk_get_option_group(FALSE); g_option_group_add_entries(group, entries); return group; } void teco_interface_init_color(guint color, guint32 rgb) {} void teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) { /* * The message types are chosen such that there is a CSS class * for every one of them. GTK_MESSAGE_OTHER does not have * a CSS class. */ static const GtkMessageType type2gtk[] = { [TECO_MSG_USER] = GTK_MESSAGE_QUESTION, [TECO_MSG_INFO] = GTK_MESSAGE_INFO, [TECO_MSG_WARNING] = GTK_MESSAGE_WARNING, [TECO_MSG_ERROR] = GTK_MESSAGE_ERROR }; 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); 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); if (type == TECO_MSG_ERROR) gtk_widget_error_bell(teco_interface.window); } void 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), ""); } void teco_interface_show_view(teco_view_t *view) { teco_interface_current_view = view; } static void teco_interface_refresh_info(void) { GtkStyleContext *style = gtk_widget_get_style_context(teco_interface.info_bar_widget); gtk_style_context_remove_class(style, "info-qregister"); gtk_style_context_remove_class(style, "info-buffer"); 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_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), info_current_temp.data, info_current_temp.len); g_autofree gchar *info_current_canon = teco_string_echo(info_current_temp.data, info_current_temp.len); const gchar *info_type_str = PACKAGE; g_autoptr(GIcon) icon = NULL; switch (teco_interface.info_type) { case TECO_INFO_TYPE_QREG: gtk_style_context_add_class(styl