/* * Copyright (C) 2012-2016 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 . */ requires 2.0.20 %ctop{ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #ifndef HAVE_GTK_FLOW_BOX_NEW #include "gtkflowbox.h" #endif %} %h{ #include %} enum GTK_INFO_POPUP { PLAIN, FILE, DIRECTORY } Gtk:Info:Popup:Entry:Type; class Gtk:Info:Popup from Gtk:Event:Box { public GtkAdjustment *hadjustment; public GtkAdjustment *vadjustment; private GtkWidget *flow_box; init(self) { GtkWidget *box, *viewport; /* * 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. */ box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); self->hadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0); self->vadjustment = gtk_adjustment_new(0, 0, 0, 0, 0, 0); GtkWidget *scrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, self->vadjustment); self->_priv->flow_box = gtk_flow_box_new(); /* take as little height as necessary */ gtk_orientable_set_orientation(GTK_ORIENTABLE(self->_priv->flow_box), GTK_ORIENTATION_HORIZONTAL); //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(self->_priv->flow_box), TRUE); /* this for focus handling only, not for scrolling */ gtk_flow_box_set_hadjustment(GTK_FLOW_BOX(self->_priv->flow_box), self->hadjustment); gtk_flow_box_set_vadjustment(GTK_FLOW_BOX(self->_priv->flow_box), self->vadjustment); gtk_container_set_border_width(GTK_CONTAINER(self->_priv->flow_box), 10); viewport = gtk_viewport_new(self->hadjustment, self->vadjustment); gtk_container_add(GTK_CONTAINER(viewport), self->_priv->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); /* * The top-level widget is a GtkEventBox, so it can have * a background. We assign a default color since the popup * will usually be put in an overlay and we don't want it * to be transparent. * It can also be styled with rounded corners etc. * FIXME: This method of setting a background color is * deprecated. We are supposed to use CSS style providers... */ gtk_container_add(GTK_CONTAINER(self), box); GdkRGBA color = {0.5, 0.5, 0.5, 1.0}; gtk_widget_override_background_color(GTK_WIDGET(self), GTK_STATE_FLAG_NORMAL, &color); } override (Gtk:Widget) void size_allocate(Gtk:Widget *widget, Gtk:Allocation *allocation) { GtkWidget *parent = gtk_widget_get_parent(widget); GtkAllocation parent_alloc; /* * Adjust the allocation of the popup to be within the * bounds of its parent widget which GtkOvelay does * not seem to do automatically. * Also Gtk does not seem to query this widget's * preferred height. * FIXME: Only works if the GtkInfoPopup is added * directly to the GtkOverlay. */ gtk_widget_get_allocation(parent, &parent_alloc); allocation->width = MIN(allocation->width, parent_alloc.width); if (allocation->height > parent_alloc.height) { allocation->y += allocation->height - parent_alloc.height; allocation->height = parent_alloc.height; } /* * Allocate the adjusted/clipped allocation */ PARENT_HANDLER(widget, allocation); } /* * Adapted from GtkScrolledWindow's gtk_scrolled_window_scroll_event() * since the viewport does not react to scroll events. * 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, GdkEventScroll *event) { Self *self = GTK_INFO_POPUP(widget); gdouble delta_x, delta_y; if (gdk_event_get_scroll_deltas((GdkEvent *)event, &delta_x, &delta_y)) { 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; } return FALSE; } public GtkWidget * new(void) { Self *widget = GET_NEW; return GTK_WIDGET(widget); } private GIcon * get_icon_for_path(const gchar *path, const gchar *fallback_name) { GFile *file; GFileInfo *info; GIcon *icon = NULL; file = g_file_new_for_path(path); info = g_file_query_info(file, "standard::icon", 0, NULL, NULL); if (info) { icon = g_file_info_get_icon(info); g_object_ref(icon); g_object_unref(info); } else { /* fall back to standard icon, but this can still return NULL! */ icon = g_icon_new_for_string(fallback_name, NULL); } g_object_unref(file); return icon; } public void add(self, Gtk:Info:Popup:Entry:Type type, const gchar *name, gboolean highlight) { GtkWidget *hbox; GtkWidget *label; gchar *markup; hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); if (type == GTK_INFO_POPUP_FILE || type == GTK_INFO_POPUP_DIRECTORY) { const gchar *fallback = type == GTK_INFO_POPUP_FILE ? "text-x-generic" : "folder"; GIcon *icon; icon = self_get_icon_for_path(name, fallback); if (icon) { GtkWidget *image; image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU); g_object_unref(icon); gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0); } } /* * FIXME: setting Pango attributes directly would be * much more efficient */ label = gtk_label_new(NULL); markup = g_markup_printf_escaped("%s", highlight ? "bold" : "normal", name); gtk_label_set_markup(GTK_LABEL(label), markup); g_free(markup); gtk_misc_set_alignment(GTK_MISC(label), 0., 0.5); gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0); gtk_widget_show_all(hbox); gtk_container_add(GTK_CONTAINER(self->_priv->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 */ GList *child_list; 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. */ child_list = gtk_container_get_children(GTK_CONTAINER(self->_priv->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); } public void clear(self) { GList *children; children = gtk_container_get_children(GTK_CONTAINER(self->_priv->flow_box)); for (GList *cur = g_list_first(children); cur != NULL; cur = g_list_next(cur)) gtk_widget_destroy(GTK_WIDGET(cur->data)); g_list_free(children); } }