diff options
-rw-r--r-- | .github/workflows/ci.yml | 2 | ||||
-rw-r--r-- | .github/workflows/nightly.yml | 2 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | INSTALL | 3 | ||||
-rw-r--r-- | configure.ac | 2 | ||||
-rw-r--r-- | debian/control | 2 | ||||
-rw-r--r-- | m4/gob2.m4 | 58 | ||||
-rw-r--r-- | src/interface-curses/curses-info-popup.c | 2 | ||||
-rw-r--r-- | src/interface-gtk/Makefile.am | 17 | ||||
-rw-r--r-- | src/interface-gtk/gtk-info-popup.c | 457 | ||||
-rw-r--r-- | src/interface-gtk/gtk-info-popup.h | 45 | ||||
-rw-r--r-- | src/interface-gtk/gtk-label.c | 271 | ||||
-rw-r--r-- | src/interface-gtk/gtk-label.h | 34 | ||||
-rw-r--r-- | src/interface-gtk/interface.c | 4 | ||||
-rw-r--r-- | src/interface-gtk/teco-gtk-info-popup.gob | 446 | ||||
-rw-r--r-- | src/interface-gtk/teco-gtk-label.gob | 253 |
16 files changed, 817 insertions, 783 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09ff86..8e8c945 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: sudo apt-get install -y build-essential autoconf automake libtool - libglib2.0-dev libncurses-dev libgtk-3-dev gob2 xvfb + libglib2.0-dev libncurses-dev libgtk-3-dev xvfb groff doxygen - name: Configure Build diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f6e6ca0..1582119 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -28,7 +28,7 @@ jobs: sudo apt-get install -y devscripts build-essential lintian debhelper dh-exec autoconf automake libtool - libglib2.0-dev libncurses-dev libgtk-3-dev gob2 xvfb + libglib2.0-dev libncurses-dev libgtk-3-dev xvfb groff-base # NOTE: We need to configure the build directory only to generate distribute.mk. @@ -34,8 +34,6 @@ testsuite.dir /src/sciteco-minimal # Generated source files -/src/interface-gtk/teco-gtk-info-popup.[ch] -/src/interface-gtk/teco-gtk-label.[ch] /src/symbols-scintilla.c /src/symbols-scilexer.c @@ -34,7 +34,6 @@ SciTECO Build and Runtime Dependencies * other curses implementations might work as well but are untested * When choosing the GTK interface: * GTK+ v3.12 or later: http://www.gtk.org/ - * GObject Builder v2.0.20 or later: http://www.jirka.org/gob.html * GNU roff (groff): https://www.gnu.org/software/groff/ Required at build-time, but it is already shipped on most UNIX-like systems to format man pages. @@ -54,7 +53,7 @@ On Ubuntu, you can install all dependencies you could possibly need as follows: $ sudo apt-get install git build-essential autoconf automake libtool \ - libglib2.0-dev libncurses-dev libgtk-3-dev gob2 \ + libglib2.0-dev libncurses-dev libgtk-3-dev \ groff doxygen Building from Source Tar Ball or Repository diff --git a/configure.ac b/configure.ac index 2d272c0..c16acf1 100644 --- a/configure.ac +++ b/configure.ac @@ -297,8 +297,6 @@ gtk) LIBS="$LIBS $LIBGTK_LIBS" ]) - GOB2_CHECK(2.0.20) - AC_DEFINE(INTERFACE_GTK, 1, [Build with GTK+ 3.0 support]) # For Scintilla: diff --git a/debian/control b/debian/control index 5852e5a..7e8cc5c 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: Robin Haberkorn <robin.haberkorn@googlemail.com> Build-Depends: debhelper (>= 10), dh-exec, g++ (>= 4:4.4), libglib2.0-dev (>= 2.44), ncurses-base, ncurses-term, libncurses5-dev, - libgtk-3-dev (>= 3.12), gob2 (>= 2.0.20), xvfb, + libgtk-3-dev (>= 3.12), xvfb, groff-base Standards-Version: 3.9.2 Homepage: http://sciteco.sf.net/ diff --git a/m4/gob2.m4 b/m4/gob2.m4 deleted file mode 100644 index 196d6c2..0000000 --- a/m4/gob2.m4 +++ /dev/null @@ -1,58 +0,0 @@ -dnl -dnl GOB_HOOK(script if found, fail) -dnl if fail = "failure", abort if GOB not found -dnl - - -AC_DEFUN([GOB2_HOOK],[ - AC_PATH_PROG(GOB2,gob2) - if test ! x$GOB2 = x; then - if test ! x$1 = x; then - AC_MSG_CHECKING(for gob-2 >= $1) - g_r_ve=`echo $1|sed 's/\([[0-9]]*\).\([[0-9]]*\).\([[0-9]]*\)/\1/'` - g_r_ma=`echo $1|sed 's/\([[0-9]]*\).\([[0-9]]*\).\([[0-9]]*\)/\2/'` - g_r_mi=`echo $1|sed 's/\([[0-9]]*\).\([[0-9]]*\).\([[0-9]]*\)/\3/'` - g_ve=`$GOB2 --version 2>&1|sed 's/Gob version \([[0-9]]*\).\([[0-9]]*\).\([[0-9]]*\)/\1/'` - g_ma=`$GOB2 --version 2>&1|sed 's/Gob version \([[0-9]]*\).\([[0-9]]*\).\([[0-9]]*\)/\2/'` - g_mi=`$GOB2 --version 2>&1|sed 's/Gob version \([[0-9]]*\).\([[0-9]]*\).\([[0-9]]*\)/\3/'` - - if test $g_ve -eq $g_r_ve; then - if test $g_ma -ge $g_r_ma; then - if test $g_mi -ge $g_r_mi; then - AC_MSG_RESULT(ok) - else - if test $g_ma -gt $g_r_ma; then - AC_MSG_RESULT(ok) - else - AC_MSG_ERROR("found $g_ve.$g_ma.$g_mi requires $g_r_ve.$g_r_ma.$g_r_mi") - fi - fi - else - AC_MSG_ERROR("found $g_ve.$g_ma.$g_mi requires $g_r_ve.$g_r_ma.$g_r_mi") - fi - else - if test $g_ve -gt $g_r_ve; then - AC_MSG_RESULT(ok) - else - AC_MSG_ERROR(major version $g_ve found but $g_r_ve required) - fi - fi - - unset gob_version - unset g_ve - unset g_ma - unset g_mi - unset g_r_ve - unset g_r_ma - unset g_r_mi - fi - AC_SUBST(GOB2) - $2 - else - $3 - fi -]) - -AC_DEFUN([GOB2_CHECK],[ - GOB2_HOOK($1,[],[AC_MSG_ERROR([Cannot find GOB-2, check http://www.5z.com/jirka/gob.html])]) -]) diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c index 7d661a2..339acc5 100644 --- a/src/interface-curses/curses-info-popup.c +++ b/src/interface-curses/curses-info-popup.c @@ -30,7 +30,7 @@ #include "curses-info-popup.h" /* - * FIXME: This is redundant with teco-gtk-info-popup.gob. + * FIXME: This is redundant with gtk-info-popup.c. */ typedef struct { teco_stailq_entry_t entry; diff --git a/src/interface-gtk/Makefile.am b/src/interface-gtk/Makefile.am index af26519..d470f85 100644 --- a/src/interface-gtk/Makefile.am +++ b/src/interface-gtk/Makefile.am @@ -3,20 +3,9 @@ AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \ AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value -EXTRA_DIST = teco-gtk-info-popup.gob \ - teco-gtk-label.gob -BUILT_SOURCES = teco-gtk-info-popup.c teco-gtk-info-popup.h \ - teco-gtk-info-popup-private.h \ - teco-gtk-label.c teco-gtk-label.h - noinst_LTLIBRARIES = libsciteco-interface.la -libsciteco_interface_la_SOURCES = interface.c -nodist_libsciteco_interface_la_SOURCES = teco-gtk-info-popup.c \ - teco-gtk-label.c +libsciteco_interface_la_SOURCES = interface.c \ + gtk-info-popup.c gtk-info-popup.h \ + gtk-label.c gtk-label.h dist_pkgdata_DATA = fallback.css - -CLEANFILES = $(BUILT_SOURCES) - -%.c %.h %-private.h : %.gob - @GOB2@ --gtk3 $< diff --git a/src/interface-gtk/gtk-info-popup.c b/src/interface-gtk/gtk-info-popup.c new file mode 100644 index 0000000..43569ea --- /dev/null +++ b/src/interface-gtk/gtk-info-popup.c @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2012-2021 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 <math.h> + +#include <glib.h> +#include <glib/gprintf.h> + +#include "list.h" +#include "string-utils.h" +#include "gtk-label.h" +#include "gtk-info-popup.h" + +/* + * FIXME: This is redundant with curses-info-popup.c. + */ +typedef struct { + teco_stailq_entry_t entry; + + teco_popup_entry_type_t type; + teco_string_t name; + gboolean highlight; +} teco_popup_entry_t; + +struct _TecoGtkInfoPopup { + GtkEventBox parent_instance; + + GtkAdjustment *hadjustment, *vadjustment; + + GtkWidget *flow_box; + GStringChunk *chunk; + teco_stailq_head_t list; + guint idle_id; + gboolean frozen; +}; + +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); + +G_DEFINE_TYPE(TecoGtkInfoPopup, teco_gtk_info_popup, GTK_TYPE_EVENT_BOX) + +/** Overrides GObject::finalize() (object destructor) */ +static void +teco_gtk_info_popup_finalize(GObject *obj_self) +{ + TecoGtkInfoPopup *self = TECO_GTK_INFO_POPUP(obj_self); + + if (self->chunk) + g_string_chunk_free(self->chunk); + + teco_stailq_entry_t *entry; + while ((entry = teco_stailq_remove_head(&self->list))) + g_free(entry); + + /* chain up to parent class */ + G_OBJECT_CLASS(teco_gtk_info_popup_parent_class)->finalize(obj_self); +} + +static void +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; +} + +static void +teco_gtk_info_popup_init(TecoGtkInfoPopup *self) +{ + self->hadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0); + self->vadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0); + + /* + * A box containing a viewport and scrollbar will + * "emulate" a scrolled window. + * We cannot use a scrolled window since it ignores + * the preferred height of its viewport which breaks + * height-for-width management. + */ + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + + GtkWidget *scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, + self->vadjustment); + /* show/hide the scrollbar dynamically */ + g_signal_connect(self->vadjustment, "changed", + G_CALLBACK(teco_gtk_info_popup_vadjustment_changed), scrollbar); + + self->flow_box = gtk_flow_box_new(); + /* take as little height as necessary */ + gtk_orientable_set_orientation(GTK_ORIENTABLE(self->flow_box), + GTK_ORIENTATION_HORIZONTAL); + //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(self->flow_box), TRUE); + /* this for focus handling only, not for scrolling */ + gtk_flow_box_set_hadjustment(GTK_FLOW_BOX(self->flow_box), + self->hadjustment); + gtk_flow_box_set_vadjustment(GTK_FLOW_BOX(self->flow_box), + self->vadjustment); + + GtkWidget *viewport = gtk_viewport_new(self->hadjustment, self->vadjustment); + gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE); + gtk_container_add(GTK_CONTAINER(viewport), self->flow_box); + + gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(box), scrollbar, FALSE, FALSE, 0); + gtk_widget_show_all(box); + + /* + * NOTE: Everything shown except the top-level container. + * Therefore a gtk_widget_show() is enough to show our popup. + */ + gtk_container_add(GTK_CONTAINER(self), box); + + self->chunk = g_string_chunk_new(32); + self->list = TECO_STAILQ_HEAD_INITIALIZER(&self->list); + self->idle_id = 0; + self->frozen = FALSE; +} + +gboolean +teco_gtk_info_popup_get_position_in_overlay(GtkOverlay *overlay, GtkWidget *widget, + GdkRectangle *allocation, gpointer user_data) +{ + GtkWidget *main_child = gtk_bin_get_child(GTK_BIN(overlay)); + GtkAllocation main_child_alloc; + gint natural_height; + + gtk_widget_get_allocation(main_child, &main_child_alloc); + gtk_widget_get_preferred_height_for_width(widget, + main_child_alloc.width, + NULL, &natural_height); + + /* + * FIXME: Probably due to some bug in the height-for-width + * calculation of Gtk (at least in 3.10 or in the GtkFlowBox + * fallback included with SciTECO), the natural height + * is a bit too small to accommodate the entire GtkFlowBox, + * resulting in the GtkViewport always scrolling. + * This hack fixes it up in a NONPORTABLE manner. + */ + natural_height += 5; + + allocation->width = main_child_alloc.width; + allocation->height = MIN(natural_height, main_child_alloc.height); + allocation->x = 0; + allocation->y = main_child_alloc.height - allocation->height; + + return TRUE; +} + +/** Overrides GtkWidget::scroll_event() */ +static gboolean +teco_gtk_info_popup_scroll_event(GtkWidget *widget, GdkEventScroll *event) +{ + TecoGtkInfoPopup *self = TECO_GTK_INFO_POPUP(widget); + gdouble delta_x, delta_y; + + if (!gdk_event_get_scroll_deltas((GdkEvent *)event, + &delta_x, &delta_y)) + return FALSE; + + GtkAdjustment *adj = self->vadjustment; + gdouble page_size = gtk_adjustment_get_page_size(adj); + gdouble scroll_unit = pow(page_size, 2.0 / 3.0); + gdouble new_value; + + new_value = CLAMP(gtk_adjustment_get_value(adj) + delta_y * scroll_unit, + gtk_adjustment_get_lower(adj), + gtk_adjustment_get_upper(adj) - + gtk_adjustment_get_page_size(adj)); + + gtk_adjustment_set_value(adj, new_value); + + return TRUE; +} + +static void +teco_gtk_info_popup_vadjustment_changed(GtkAdjustment *vadjustment, GtkWidget *scrollbar) +{ + /* + * This shows/hides the widget using opacity instead of using + * gtk_widget_set_visibility() since the latter would influence + * size allocations. A widget with opacity 0 keeps its size. + */ + gtk_widget_set_opacity(scrollbar, + gtk_adjustment_get_upper(vadjustment) - + gtk_adjustment_get_lower(vadjustment) > + gtk_adjustment_get_page_size(vadjustment) ? 1 : 0); +} + +GtkWidget * +teco_gtk_info_popup_new(void) +{ + return GTK_WIDGET(g_object_new(TECO_TYPE_GTK_INFO_POPUP, NULL)); +} + +GIcon * +teco_gtk_info_popup_get_icon_for_path(const gchar *path, const gchar *fallback_name) +{ + GIcon *icon = NULL; + + g_autoptr(GFile) file = g_file_new_for_path(path); + g_autoptr(GFileInfo) info = g_file_query_info(file, "standard::icon", 0, NULL, NULL); + if (info) { + icon = g_file_info_get_icon(info); + g_object_ref(icon); + } else { + /* fall back to standard icon, but this can still return NULL! */ + icon = g_icon_new_for_string(fallback_name, NULL); + } + + return icon; +} + +void +teco_gtk_info_popup_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type, + const gchar *name, gssize len, gboolean highlight) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (TECO_IS_GTK_INFO_POPUP (self)); + + teco_popup_entry_t *entry = g_new(teco_popup_entry_t, 1); + entry->type = type; + /* + * Popup entries aren't removed individually, so we can + * more efficiently store them via GStringChunk. + */ + teco_string_init_chunk(&entry->name, name, len < 0 ? strlen(name) : len, + self->chunk); + entry->highlight = highlight; + + /* + * NOTE: We don't immediately create the Gtk+ widget and add it + * to the GtkFlowBox since it would be too slow for very large + * numbers of popup entries. + * Instead, we queue and process them in idle time only once the widget + * is shown. This ensures a good reactivity, even though the popup may + * not yet be complete when first shown. + * + * While it would be possible to show the widget before the first + * add() call to achieve the same effect, this would prevent keyboard + * interaction unless we add support for interruptions or drive + * the event loop manually. + */ + teco_stailq_insert_tail(&self->list, &entry->entry); +} + +static void +teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type, + const gchar *name, gssize len, gboolean highlight) +{ + g_return_if_fail(self != NULL); + g_return_if_fail(TECO_IS_GTK_INFO_POPUP(self)); + + GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + if (highlight) + gtk_style_context_add_class(gtk_widget_get_style_context(hbox), + "highlight"); + + /* + * FIXME: The icon fetching takes about 1/3 of the time required to + * add all widgets. + * Perhaps it's possible to optimize this. + */ + if (type == TECO_POPUP_FILE || type == TECO_POPUP_DIRECTORY) { + 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); + if (icon) { + gint width, height; + gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height); + + GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU); + /* This is necessary so that oversized icons get scaled down. */ + gtk_image_set_pixel_size(GTK_IMAGE(image), height); + gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0); + } + } + + GtkWidget *label = teco_gtk_label_new(name, len); + /* + * Gtk v3.20 changed the CSS element names. + * Adding a style class eases writing a portable fallback.css. + */ + gtk_style_context_add_class(gtk_widget_get_style_context(label), "label"); + 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); + break; + case TECO_POPUP_FILE: + case TECO_POPUP_DIRECTORY: + gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE); + break; + } + + gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0); + + gtk_widget_show_all(hbox); + gtk_container_add(GTK_CONTAINER(self->flow_box), hbox); +} + +static gboolean +teco_gtk_info_popup_idle_cb(TecoGtkInfoPopup *self) +{ + g_return_val_if_fail(self != NULL, FALSE); + g_return_val_if_fail(TECO_IS_GTK_INFO_POPUP(self), FALSE); + + /* + * The more often this is repeated, the faster we will add all popup entries, + * but at the same time, the UI will be less responsive. + */ + for (gint i = 0; i < 5; i++) { + teco_popup_entry_t *head = (teco_popup_entry_t *)teco_stailq_remove_head(&self->list); + if (G_UNLIKELY(!head)) { + if (self->frozen) + gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self))); + self->frozen = FALSE; + self->idle_id = 0; + return G_SOURCE_REMOVE; + } + + teco_gtk_info_popup_idle_add(self, head->type, head->name.data, head->name.len, head->highlight); + + /* All teco_popup_entry_t::names are freed via GStringChunk */ + g_free(head); + } + + if (self->frozen && + gtk_adjustment_get_upper(self->vadjustment) - + gtk_adjustment_get_lower(self->vadjustment) > gtk_adjustment_get_page_size(self->vadjustment)) { + /* the GtkFlowBox needs scrolling - time to thaw */ + gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self))); + self->frozen = FALSE; + } + + return G_SOURCE_CONTINUE; +} + +/** Overrides GtkWidget::show() */ +static void +teco_gtk_info_popup_show(GtkWidget *widget) +{ + TecoGtkInfoPopup *self = TECO_GTK_INFO_POPUP(widget); + + if (!self->idle_id) { + self->idle_id = gdk_threads_add_idle((GSourceFunc)teco_gtk_info_popup_idle_cb, self); + + /* + * To prevent a visible popup build-up for small popups, + * the display is frozen until the popup is large enough for + * scrolling or until all entries have been added. + */ + GdkWindow *window = gtk_widget_get_window(widget); + if (window) { + gdk_window_freeze_updates(window); + self->frozen = TRUE; + } + } + + /* chain to parent class */ + GTK_WIDGET_CLASS(teco_gtk_info_popup_parent_class)->show(widget); +} + +void +teco_gtk_info_popup_scroll_page(TecoGtkInfoPopup *self) +{ + g_return_if_fail(self != NULL); + g_return_if_fail(TECO_IS_GTK_INFO_POPUP(self)); + + GtkAdjustment *adj = self->vadjustment; + gdouble new_value; + + if (gtk_adjustment_get_value(adj) + gtk_adjustment_get_page_size(adj) == + gtk_adjustment_get_upper(adj)) { + /* wrap and scroll back to the top */ + new_value = gtk_adjustment_get_lower(adj); + } else { + /* scroll one page */ + new_value = gtk_adjustment_get_value(adj) + + gtk_adjustment_get_page_size(adj); + + /* + * 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) { + 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)); + } + + gtk_adjustment_set_value(adj, new_value); +} + +static void +teco_gtk_info_popup_destroy_cb(GtkWidget *widget, gpointer user_data) +{ + gtk_widget_destroy(widget); +} + +void +teco_gtk_info_popup_clear(TecoGtkInfoPopup *self) +{ + g_return_if_fail(self != NULL); + g_return_if_fail(TECO_IS_GTK_INFO_POPUP(self)); + + gtk_container_foreach(GTK_CONTAINER(self->flow_box), teco_gtk_info_popup_destroy_cb, NULL); + + /* + * If there are still queued popoup entries, the next teco_gtk_info_popup_idle_cb() + * invocation will also stop the GSource. + */ + teco_stailq_entry_t *entry; + while ((entry = teco_stailq_remove_head(&self->list))) + g_free(entry); + + g_string_chunk_clear(self->chunk); +} diff --git a/src/interface-gtk/gtk-info-popup.h b/src/interface-gtk/gtk-info-popup.h new file mode 100644 index 0000000..04a4d42 --- /dev/null +++ b/src/interface-gtk/gtk-info-popup.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2012-2021 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/>. + */ +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <gio/gio.h> + +#include <gdk/gdk.h> +#include <gtk/gtk.h> + +#include "interface.h" + +#define TECO_TYPE_GTK_INFO_POPUP teco_gtk_info_popup_get_type() +G_DECLARE_FINAL_TYPE(TecoGtkInfoPopup, teco_gtk_info_popup, TECO, GTK_INFO_POPUP, GtkEventBox) + +GtkWidget *teco_gtk_info_popup_new(void); + +void teco_gtk_info_popup_add(TecoGtkInfoPopup *self, + teco_popup_entry_type_t type, + const gchar *name, gssize len, + gboolean highlight); +void teco_gtk_info_popup_scroll_page(TecoGtkInfoPopup *self); +void teco_gtk_info_popup_clear(TecoGtkInfoPopup *self); + +gboolean teco_gtk_info_popup_get_position_in_overlay(GtkOverlay *overlay, + GtkWidget *widget, + GdkRectangle *allocation, + gpointer user_data); +GIcon *teco_gtk_info_popup_get_icon_for_path(const gchar *path, + const gchar *fallback_name); diff --git a/src/interface-gtk/gtk-label.c b/src/interface-gtk/gtk-label.c new file mode 100644 index 0000000..537269d --- /dev/null +++ b/src/interface-gtk/gtk-label.c @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2012-2021 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 <glib/gprintf.h> + +#include <gdk/gdk.h> +#include <gtk/gtk.h> + +#include "sciteco.h" +#include "string-utils.h" + +#include "gtk-label.h" + +#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16)) + +struct _TecoGtkLabel { + GtkLabel parent_instance; + + PangoColor fg, bg; + guint16 fg_alpha, bg_alpha; + + teco_string_t string; +}; + +G_DEFINE_TYPE(TecoGtkLabel, teco_gtk_label, GTK_TYPE_LABEL) + +/** Overrides GObject::finalize() (object destructor) */ +static void +teco_gtk_label_finalize(GObject *obj_self) +{ + TecoGtkLabel *self = TECO_GTK_LABEL(obj_self); + + teco_string_clear(&self->string); + + /* chain up to parent class */ + G_OBJECT_CLASS(teco_gtk_label_parent_class)->finalize(obj_self); +} + +/** Overrides GtkWidget::style_updated() */ +static void +teco_gtk_label_style_updated(GtkWidget *widget) +{ + TecoGtkLabel *self = TECO_GTK_LABEL(widget); + + /* chain to parent class */ + GTK_WIDGET_CLASS(teco_gtk_label_parent_class)->style_updated(widget); + + GtkStyleContext *style = gtk_widget_get_style_context(widget); + + GdkRGBA normal_color; + gtk_style_context_get_color(style, GTK_STATE_NORMAL, &normal_color); + self->bg.red = GDK_TO_PANGO_COLOR(normal_color.red); + self->bg.green = GDK_TO_PANGO_COLOR(normal_color.green); + self->bg.blue = GDK_TO_PANGO_COLOR(normal_color.blue); + self->bg_alpha = GDK_TO_PANGO_COLOR(normal_color.alpha); + + /* + * If Pango does not support transparent foregrounds, + * it will at least use a high-contrast foreground. + * + * NOTE: It would be very hard to get an appropriate background + * color even if Gtk supports it since the label itself may + * not have one but one of its parents. + * + * FIXME: We may want to honour the background color, + * so we can at least get decent reverse text when setting + * the background color in the CSS. + */ + self->fg.red = normal_color.red > 0.5 ? 0 : G_MAXUINT16; + self->fg.green = normal_color.green > 0.5 ? 0 : G_MAXUINT16; + self->fg.blue = normal_color.blue > 0.5 ? 0 : G_MAXUINT16; + /* try hard to get a transparent foreground anyway */ + self->fg_alpha = 0; + + /* + * The widget might be styled after the text has been set on it, + * we must recreate the Pango attributes. + */ + if (self->string.len > 0) { + PangoAttrList *attribs = NULL; + g_autofree gchar *plaintext = NULL; + + teco_gtk_label_parse_string(self->string.data, self->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); + + g_assert(!g_strcmp0(plaintext, gtk_label_get_text(GTK_LABEL(self)))); + } +} + +static void +teco_gtk_label_class_init(TecoGtkLabelClass *klass) +{ + GTK_WIDGET_CLASS(klass)->style_updated = teco_gtk_label_style_updated; + G_OBJECT_CLASS(klass)->finalize = teco_gtk_label_finalize; +} + +static void teco_gtk_label_init(TecoGtkLabel *self) {} + +GtkWidget * +teco_gtk_label_new(const gchar *str, gssize len) +{ + TecoGtkLabel *widget = TECO_GTK_LABEL(g_object_new(TECO_TYPE_GTK_LABEL, NULL)); + + teco_gtk_label_set_text(widget, str, len); + + return GTK_WIDGET(widget); +} + +static void +teco_gtk_label_add_highlight_attribs(PangoAttrList *attribs, PangoColor *fg, guint16 fg_alpha, + PangoColor *bg, guint16 bg_alpha, guint index, gsize len) +{ + PangoAttribute *attr; + + /* + * NOTE: Transparent foreground do not seem to work, + * 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; + pango_attr_list_insert(attribs, attr); + + attr = pango_attr_background_alpha_new(bg_alpha); + 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; + attr->end_index = index + len; + pango_attr_list_insert(attribs, attr); + + attr = pango_attr_background_new(bg->red, bg->green, bg->blue); + attr->start_index = index; + attr->end_index = index + len; + pango_attr_list_insert(attribs, attr); +} + +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) +{ + if (len < 0) + len = strlen(str); + + /* + * Approximate size of unformatted text. + */ + gsize text_len = 1; /* for trailing 0 */ + for (gint i = 0; i < len; i++) + text_len += TECO_IS_CTL(str[i]) ? 3 : 1; + + *attribs = pango_attr_list_new(); + *text = g_malloc(text_len); + + gint index = 0; + while (len > 0) { + /* + * NOTE: This mapping is similar to + * teco_view_set_presentations() + */ + switch (*str) { + case '\e': + teco_gtk_label_add_highlight_attribs(*attribs, + fg, fg_alpha, + bg, bg_alpha, + index, 1); + (*text)[index++] = '$'; + break; + case '\r': + teco_gtk_label_add_highlight_attribs(*attribs, + fg, fg_alpha, + bg, bg_alpha, + index, 2); + (*text)[index++] = 'C'; + (*text)[index++] = 'R'; + break; + case '\n': + teco_gtk_label_add_highlight_attribs(*attribs, + fg, fg_alpha, + bg, bg_alpha, + index, 2); + (*text)[index++] = 'L'; + (*text)[index++] = 'F'; + break; + case '\t': + teco_gtk_label_add_highlight_attribs(*attribs, + fg, fg_alpha, + bg, bg_alpha, + index, 3); + (*text)[index++] = 'T'; + (*text)[index++] = 'A'; + (*text)[index++] = 'B'; + break; + default: + if (TECO_IS_CTL(*str)) { + teco_gtk_label_add_highlight_attribs(*attribs, + fg, fg_alpha, + bg, bg_alpha, + index, 2); + (*text)[index++] = '^'; + (*text)[index++] = TECO_CTL_ECHO(*str); + } else { + (*text)[index++] = *str; + } + break; + } + + str++; + len--; + } + + /* null-terminate generated text */ + (*text)[index] = '\0'; +} + +void +teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len) +{ + g_return_if_fail(self != NULL); + g_return_if_fail(TECO_IS_GTK_LABEL(self)); + + teco_string_clear(&self->string); + teco_string_init(&self->string, str, len < 0 ? strlen(str) : len); + + g_autofree gchar *plaintext = NULL; + + if (self->string.len > 0) { + 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); + + gtk_label_set_attributes(GTK_LABEL(self), attribs); + pango_attr_list_unref(attribs); + } + + gtk_label_set_text(GTK_LABEL(self), plaintext); +} diff --git a/src/interface-gtk/gtk-label.h b/src/interface-gtk/gtk-label.h new file mode 100644 index 0000000..e20646d --- /dev/null +++ b/src/interface-gtk/gtk-label.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012-2021 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/>. + */ +#pragma once + +#include <glib.h> +#include <glib-object.h> + +#include <gtk/gtk.h> + +#define TECO_TYPE_GTK_LABEL teco_gtk_label_get_type() +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); + +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); diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c index afc8fe3..1e4e32b 100644 --- a/src/interface-gtk/interface.c +++ b/src/interface-gtk/interface.c @@ -41,8 +41,8 @@ #include <Scintilla.h> #include <ScintillaWidget.h> -#include "teco-gtk-info-popup.h" -#include "teco-gtk-label.h" +#include "gtk-info-popup.h" +#include "gtk-label.h" #include "sciteco.h" #include "error.h" diff --git a/src/interface-gtk/teco-gtk-info-popup.gob b/src/interface-gtk/teco-gtk-info-popup.gob deleted file mode 100644 index f08b0d7..0000000 --- a/src/interface-gtk/teco-gtk-info-popup.gob +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Copyright (C) 2012-2021 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/>. - */ - -requires 2.0.20 - -%ctop{ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include <string.h> -#include <math.h> - -#include <glib/gprintf.h> - -#include "list.h" -#include "string-utils.h" -#include "teco-gtk-label.h" -%} - -%h{ -#include <gtk/gtk.h> -#include <gdk/gdk.h> - -#include <gio/gio.h> - -#include "interface.h" -%} - -%{ -/* - * FIXME: This is redundant with curses-info-popup.c. - */ -typedef struct { - teco_stailq_entry_t entry; - - teco_popup_entry_type_t type; - teco_string_t name; - gboolean highlight; -} teco_popup_entry_t; -%} - -/* - * NOTE: Deriving from GtkEventBox ensures that we can - * set a background on the entire popup widget. - */ -class Teco:Gtk:Info:Popup from Gtk:Event:Box { - /* These are added to other widgets, so they don't have to be destroyed */ - public GtkAdjustment *hadjustment = {gtk_adjustment_new(0, 0, 0, 0, 0, 0)}; - public GtkAdjustment *vadjustment = {gtk_adjustment_new(0, 0, 0, 0, 0, 0)}; - - private GtkWidget *flow_box = {gtk_flow_box_new()}; - - private GStringChunk *chunk = {g_string_chunk_new(32)} - destroywith g_string_chunk_free; - private teco_stailq_head_t list - destroy { - teco_stailq_entry_t *entry; - while ((entry = teco_stailq_remove_head(&VAR))) - g_free(entry); - }; - private guint idle_id = 0; - private gboolean frozen = FALSE; - - init(self) - { - /* - * A box containing a viewport and scrollbar will - * "emulate" a scrolled window. - * We cannot use a scrolled window since it ignores - * the preferred height of its viewport which breaks - * height-for-width management. - */ - GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - - GtkWidget *scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, - self->vadjustment); - /* show/hide the scrollbar dynamically */ - g_signal_connect(self->vadjustment, "changed", - G_CALLBACK(self_vadjustment_changed), scrollbar); - - /* take as little height as necessary */ - gtk_orientable_set_orientation(GTK_ORIENTABLE(selfp->flow_box), - GTK_ORIENTATION_HORIZONTAL); - //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(selfp->flow_box), TRUE); - /* this for focus handling only, not for scrolling */ - gtk_flow_box_set_hadjustment(GTK_FLOW_BOX(selfp->flow_box), - self->hadjustment); - gtk_flow_box_set_vadjustment(GTK_FLOW_BOX(selfp->flow_box), - self->vadjustment); - - GtkWidget *viewport = gtk_viewport_new(self->hadjustment, self->vadjustment); - gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE); - gtk_container_add(GTK_CONTAINER(viewport), selfp->flow_box); - - gtk_box_pack_start(GTK_BOX(box), viewport, TRUE, TRUE, 0); - gtk_box_pack_start(GTK_BOX(box), scrollbar, FALSE, FALSE, 0); - gtk_widget_show_all(box); - - /* - * NOTE: Everything shown except the top-level container. - * Therefore a gtk_widget_show() is enough to show our popup. - */ - gtk_container_add(GTK_CONTAINER(self), box); - - selfp->list = TECO_STAILQ_HEAD_INITIALIZER(&selfp->list); - } - - /** - * Allocate position in an overlay. - * - * This function can be used as the "get-child-position" signal - * handler of a GtkOverlay in order to position the popup at the - * bottom of the overlay's main child, spanning the entire width. - * In contrast to the GtkOverlay's default allocation schemes, - * this makes sure that the widget will not be larger than the - * main child, so the popup properly scrolls when becoming too large - * in height. - * - * @param user_data unused by this callback - */ - public gboolean - get_position_in_overlay(Gtk:Overlay *overlay, Gtk:Widget *widget, - Gdk:Rectangle *allocation, gpointer user_data) - { - GtkWidget *main_child = gtk_bin_get_child(GTK_BIN(overlay)); - GtkAllocation main_child_alloc; - gint natural_height; - - gtk_widget_get_allocation(main_child, &main_child_alloc); - gtk_widget_get_preferred_height_for_width(widget, - main_child_alloc.width, - NULL, &natural_height); - - /* - * FIXME: Probably due to some bug in the height-for-width - * calculation of Gtk (at least in 3.10 or in the GtkFlowBox - * fallback included with SciTECO), the natural height - * is a bit too small to accommodate the entire GtkFlowBox, - * resulting in the GtkViewport always scrolling. - * This hack fixes it up in a NONPORTABLE manner. - */ - natural_height += 5; - - allocation->width = main_child_alloc.width; - allocation->height = MIN(natural_height, main_child_alloc.height); - allocation->x = 0; - allocation->y = main_child_alloc.height - allocation->height; - - return TRUE; - } - - /* - * Adapted from GtkScrolledWindow's gtk_scrolled_window_scroll_event() - * since the viewport does not react to scroll events. - * This is registered for our container widget instead of only for - * GtkViewport since this is what GtkScrolledWindow does. - * - * FIXME: May need to handle non-delta scrolling, i.e. GDK_SCROLL_UP - * and GDK_SCROLL_DOWN. - */ - override (Gtk:Widget) gboolean - scroll_event(Gtk:Widget *widget, Gdk:Event:Scroll *event) - { - Self *self = SELF(widget); - gdouble delta_x, delta_y; - - if (!gdk_event_get_scroll_deltas((GdkEvent *)event, - &delta_x, &delta_y)) - return FALSE; - - GtkAdjustment *adj = self->vadjustment; - gdouble page_size = gtk_adjustment_get_page_size(adj); - gdouble scroll_unit = pow(page_size, 2.0 / 3.0); - gdouble new_value; - - new_value = CLAMP(gtk_adjustment_get_value(adj) + delta_y * scroll_unit, - gtk_adjustment_get_lower(adj), - gtk_adjustment_get_upper(adj) - - gtk_adjustment_get_page_size(adj)); - - gtk_adjustment_set_value(adj, new_value); - - return TRUE; - } - - private void - vadjustment_changed(Gtk:Adjustment *vadjustment, Gtk:Widget *scrollbar) - { - /* - * This shows/hides the widget using opacity instead of using - * gtk_widget_set_visibility() since the latter would influence - * size allocations. A widget with opacity 0 keeps its size. - */ - gtk_widget_set_opacity(scrollbar, - gtk_adjustment_get_upper(vadjustment) - - gtk_adjustment_get_lower(vadjustment) > - gtk_adjustment_get_page_size(vadjustment) ? 1 : 0); - } - - public GtkWidget * - new(void) - { - return GTK_WIDGET(GET_NEW); - } - - public GIcon * - get_icon_for_path(const gchar *path, const gchar *fallback_name) - { - GIcon *icon = NULL; - - g_autoptr(GFile) file = g_file_new_for_path(path); - g_autoptr(GFileInfo) info = g_file_query_info(file, "standard::icon", 0, NULL, NULL); - if (info) { - icon = g_file_info_get_icon(info); - g_object_ref(icon); - } else { - /* fall back to standard icon, but this can still return NULL! */ - icon = g_icon_new_for_string(fallback_name, NULL); - } - - return icon; - } - - public void - add(self, teco_popup_entry_type_t type, - const gchar *name, gssize len, gboolean highlight) - { - teco_popup_entry_t *entry = g_new(teco_popup_entry_t, 1); - entry->type = type; - /* - * Popup entries aren't removed individually, so we can - * more efficiently store them via GStringChunk. - */ - teco_string_init_chunk(&entry->name, name, len < 0 ? strlen(name) : len, - selfp->chunk); - entry->highlight = highlight; - - /* - * NOTE: We don't immediately create the Gtk+ widget and add it - * to the GtkFlowBox since it would be too slow for very large - * numbers of popup entries. - * Instead, we queue and process them in idle time only once the widget - * is shown. This ensures a good reactivity, even though the popup may - * not yet be complete when first shown. - * - * While it would be possible to show the widget before the first - * add() call to achieve the same effect, this would prevent keyboard - * interaction unless we add support for interruptions or drive - * the event loop manually. - */ - teco_stailq_insert_tail(&selfp->list, &entry->entry); - } - - override (Gtk:Widget) void - show(Gtk:Widget *widget) - { - Self *self = SELF(widget); - - if (!selfp->idle_id) { - selfp->idle_id = gdk_threads_add_idle((GSourceFunc)self_idle_cb, self); - - /* - * To prevent a visible popup build-up for small popups, - * the display is frozen until the popup is large enough for - * scrolling or until all entries have been added. - */ - GdkWindow *window = gtk_widget_get_window(widget); - if (window) { - gdk_window_freeze_updates(window); - selfp->frozen = TRUE; - } - } - - PARENT_HANDLER(widget); - } - - private gboolean - idle_cb(self) - { - /* - * The more often this is repeated, the faster we will add all popup entries, - * but at the same time, the UI will be less responsive. - */ - for (gint i = 0; i < 5; i++) { - teco_popup_entry_t *head = (teco_popup_entry_t *)teco_stailq_remove_head(&selfp->list); - if (G_UNLIKELY(!head)) { - if (selfp->frozen) - gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self))); - selfp->frozen = FALSE; - selfp->idle_id = 0; - return G_SOURCE_REMOVE; - } - - self_idle_add(self, head->type, head->name.data, head->name.len, head->highlight); - - /* All teco_popup_entry_t::names are freed via GStringChunk */ - g_free(head); - } - - if (selfp->frozen && - gtk_adjustment_get_upper(self->vadjustment) - - gtk_adjustment_get_lower(self->vadjustment) > gtk_adjustment_get_page_size(self->vadjustment)) { - /* the GtkFlowBox needs scrolling - time to thaw */ - gdk_window_thaw_updates(gtk_widget_get_window(GTK_WIDGET(self))); - selfp->frozen = FALSE; - } - - return G_SOURCE_CONTINUE; - } - - private void - idle_add(self, teco_popup_entry_type_t type, - const gchar *name, gssize len, gboolean highlight) - { - GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); - if (highlight) - gtk_style_context_add_class(gtk_widget_get_style_context(hbox), - "highlight"); - - /* - * FIXME: The icon fetching takes about 1/3 of the time required to - * add all widgets. - * Perhaps it's possible to optimize this. - */ - if (type == TECO_POPUP_FILE || type == TECO_POPUP_DIRECTORY) { - 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 = self_get_icon_for_path(path, fallback); - if (icon) { - gint width, height; - gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height); - - GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU); - /* This is necessary so that oversized icons get scaled down. */ - gtk_image_set_pixel_size(GTK_IMAGE(image), height); - gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0); - } - } - - GtkWidget *label = teco_gtk_label_new(name, len); - /* - * Gtk v3.20 changed the CSS element names. - * Adding a style class eases writing a portable fallback.css. - */ - gtk_style_context_add_class(gtk_widget_get_style_context(label), "label"); - 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); - break; - case TECO_POPUP_FILE: - case TECO_POPUP_DIRECTORY: - gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE); - break; - } - - gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0); - - gtk_widget_show_all(hbox); - gtk_container_add(GTK_CONTAINER(selfp->flow_box), hbox); - } - - public void - scroll_page(self) - { - GtkAdjustment *adj = self->vadjustment; - gdouble new_value; - - if (gtk_adjustment_get_value(adj) + gtk_adjustment_get_page_size(adj) == - gtk_adjustment_get_upper(adj)) { - /* wrap and scroll back to the top */ - new_value = gtk_adjustment_get_lower(adj); - } else { - /* scroll one page */ - new_value = gtk_adjustment_get_value(adj) + - gtk_adjustment_get_page_size(adj); - - /* - * 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(selfp->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)); - } - - gtk_adjustment_set_value(adj, new_value); - } - - private void - destroy_cb(Gtk:Widget *widget, gpointer user_data) - { - gtk_widget_destroy(widget); - } - - public void - clear(self) - { - gtk_container_foreach(GTK_CONTAINER(selfp->flow_box), self_destroy_cb, NULL); - - /* - * If there are still queued popoup entries, the next self_idle_cb() - * invocation will also stop the GSource. - */ - teco_stailq_entry_t *entry; - while ((entry = teco_stailq_remove_head(&selfp->list))) - g_free(entry); - - g_string_chunk_clear(selfp->chunk); - } -} diff --git a/src/interface-gtk/teco-gtk-label.gob b/src/interface-gtk/teco-gtk-label.gob deleted file mode 100644 index 2167dbc..0000000 --- a/src/interface-gtk/teco-gtk-label.gob +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright (C) 2012-2021 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/>. - */ - -requires 2.0.20 - -%ctop{ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include <string.h> - -#include <glib/gprintf.h> - -#include <gdk/gdk.h> - -#include "sciteco.h" -#include "string-utils.h" - -#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16)) -%} - -%h{ -#include <gtk/gtk.h> -%} - -class Teco:Gtk:Label from Gtk:Label { - private PangoColor fg; - private guint16 fg_alpha; - private PangoColor bg; - private guint16 bg_alpha; - - private teco_string_t string - destroy { - teco_string_clear(&VAR); - }; - - override (Gtk:Widget) void - style_updated(Gtk:Widget *widget) - { - Self *self = SELF(widget); - - PARENT_HANDLER(widget); - GtkStyleContext *style = gtk_widget_get_style_context(widget); - - GdkRGBA normal_color; - gtk_style_context_get_color(style, GTK_STATE_NORMAL, &normal_color); - selfp->bg.red = GDK_TO_PANGO_COLOR(normal_color.red); - selfp->bg.green = GDK_TO_PANGO_COLOR(normal_color.green); - selfp->bg.blue = GDK_TO_PANGO_COLOR(normal_color.blue); - selfp->bg_alpha = GDK_TO_PANGO_COLOR(normal_color.alpha); - - /* - * If Pango does not support transparent foregrounds, - * it will at least use a high-contrast foreground. - * - * NOTE: It would be very hard to get an appropriate background - * color even if Gtk supports it since the label itself may - * not have one but one of its parents. - * - * FIXME: We may want to honour the background color, - * so we can at least get decent reverse text when setting - * the background color in the CSS. - */ - selfp->fg.red = normal_color.red > 0.5 ? 0 : G_MAXUINT16; - selfp->fg.green = normal_color.green > 0.5 ? 0 : G_MAXUINT16; - selfp->fg.blue = normal_color.blue > 0.5 ? 0 : G_MAXUINT16; - /* try hard to get a transparent foreground anyway */ - selfp->fg_alpha = 0; - - /* - * The widget might be styled after the text has been set on it, - * we must recreate the Pango attributes. - */ - if (selfp->string.len > 0) { - PangoAttrList *attribs = NULL; - g_autofree gchar *plaintext = NULL; - - self_parse_string(selfp->string.data, selfp->string.len, - &selfp->fg, selfp->fg_alpha, - &selfp->bg, selfp->bg_alpha, - &attribs, &plaintext); - - gtk_label_set_attributes(GTK_LABEL(self), attribs); - pango_attr_list_unref(attribs); - - g_assert(!g_strcmp0(plaintext, gtk_label_get_text(GTK_LABEL(self)))); - } - } - - public GtkWidget * - new(const gchar *str, gssize len) - { - Self *widget = GET_NEW; - - self_set_text(widget, str, len); - - return GTK_WIDGET(widget); - } - - private void - add_highlight_attribs(Pango:Attr:List *attribs, - Pango:Color *fg, guint16 fg_alpha, - Pango:Color *bg, guint16 bg_alpha, - guint index, gsize len) - { - PangoAttribute *attr; - - /* - * NOTE: Transparent foreground do not seem to work, - * 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; - pango_attr_list_insert(attribs, attr); - - attr = pango_attr_background_alpha_new(bg_alpha); - 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; - attr->end_index = index + len; - pango_attr_list_insert(attribs, attr); - - attr = pango_attr_background_new(bg->red, bg->green, bg->blue); - attr->start_index = index; - attr->end_index = index + len; - pango_attr_list_insert(attribs, attr); - } - - public void - parse_string(const gchar *str, gssize len, - Pango:Color *fg, guint16 fg_alpha, - Pango:Color *bg, guint16 bg_alpha, - Pango:Attr:List **attribs, gchar **text) - { - if (len < 0) - len = strlen(str); - - /* - * Approximate size of unformatted text. - */ - gsize text_len = 1; /* for trailing 0 */ - for (gint i = 0; i < len; i++) - text_len += TECO_IS_CTL(str[i]) ? 3 : 1; - - *attribs = pango_attr_list_new(); - *text = g_malloc(text_len); - - gint index = 0; - while (len > 0) { - /* - * NOTE: This mapping is similar to - * teco_view_set_presentations() - */ - switch (*str) { - case '\e': - self_add_highlight_attribs(*attribs, - fg, fg_alpha, - bg, bg_alpha, - index, 1); - (*text)[index++] = '$'; - break; - case '\r': - self_add_highlight_attribs(*attribs, - fg, fg_alpha, - bg, bg_alpha, - index, 2); - (*text)[index++] = 'C'; - (*text)[index++] = 'R'; - break; - case '\n': - self_add_highlight_attribs(*attribs, - fg, fg_alpha, - bg, bg_alpha, - index, 2); - (*text)[index++] = 'L'; - (*text)[index++] = 'F'; - break; - case '\t': - self_add_highlight_attribs(*attribs, - fg, fg_alpha, - bg, bg_alpha, - index, 3); - (*text)[index++] = 'T'; - (*text)[index++] = 'A'; - (*text)[index++] = 'B'; - break; - default: - if (TECO_IS_CTL(*str)) { - self_add_highlight_attribs(*attribs, - fg, fg_alpha, - bg, bg_alpha, - index, 2); - (*text)[index++] = '^'; - (*text)[index++] = TECO_CTL_ECHO(*str); - } else { - (*text)[index++] = *str; - } - break; - } - - str++; - len--; - } - - /* null-terminate generated text */ - (*text)[index] = '\0'; - } - - public void - set_text(self, const gchar *str, gssize len) - { - teco_string_clear(&selfp->string); - teco_string_init(&selfp->string, str, len < 0 ? strlen(str) : len); - - g_autofree gchar *plaintext = NULL; - - if (selfp->string.len > 0) { - PangoAttrList *attribs = NULL; - - self_parse_string(selfp->string.data, selfp->string.len, - &selfp->fg, selfp->fg_alpha, - &selfp->bg, selfp->bg_alpha, - &attribs, &plaintext); - - gtk_label_set_attributes(GTK_LABEL(self), attribs); - pango_attr_list_unref(attribs); - } - - gtk_label_set_text(GTK_LABEL(self), plaintext); - } -} |