aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/interface-gtk/teco-gtk-info-popup.gob
diff options
context:
space:
mode:
Diffstat (limited to 'src/interface-gtk/teco-gtk-info-popup.gob')
-rw-r--r--src/interface-gtk/teco-gtk-info-popup.gob446
1 files changed, 446 insertions, 0 deletions
diff --git a/src/interface-gtk/teco-gtk-info-popup.gob b/src/interface-gtk/teco-gtk-info-popup.gob
new file mode 100644
index 0000000..f08b0d7
--- /dev/null
+++ b/src/interface-gtk/teco-gtk-info-popup.gob
@@ -0,0 +1,446 @@
+/*
+ * 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);
+ }
+}