From ea0179e342343f5fbefb265bde4dea0d475f0781 Mon Sep 17 00:00:00 2001 From: Robin Haberkorn Date: Sun, 31 Jan 2016 05:04:50 +0100 Subject: interfaces have their own automake subdirectories and convenience libraries now * use libtool convenience libraries as much as possible (for all static libraries except Scintilla) * improves separation of language and user interface implementations (e.g. the Gtk widgets are not interesting for the rest of SciTECO) * the Curses popup widget can now be factored out of interface-curses.cpp * some common CPPFLAGS are now defined by ./configure via AM_CPPFLAGS, so they don't have to be repeated in each submodule. * fixed building the Curses UI: GTK_FLOW_BOX_FALLBACK conditional must always be defined. --- src/Makefile.am | 93 +- src/gtk-info-popup.gob | 269 -- src/gtkflowbox.c | 4795 ----------------------------- src/gtkflowbox.h | 180 -- src/interface-curses.cpp | 1548 ---------- src/interface-curses.h | 232 -- src/interface-curses/Makefile.am | 9 + src/interface-curses/interface-curses.cpp | 1548 ++++++++++ src/interface-curses/interface-curses.h | 232 ++ src/interface-gtk.cpp | 759 ----- src/interface-gtk.h | 171 - src/interface-gtk/Makefile.am | 23 + src/interface-gtk/gtk-info-popup.gob | 269 ++ src/interface-gtk/gtkflowbox.c | 4795 +++++++++++++++++++++++++++++ src/interface-gtk/gtkflowbox.h | 180 ++ src/interface-gtk/interface-gtk.cpp | 759 +++++ src/interface-gtk/interface-gtk.h | 171 + src/interface.h | 4 +- 18 files changed, 8024 insertions(+), 8013 deletions(-) delete mode 100644 src/gtk-info-popup.gob delete mode 100644 src/gtkflowbox.c delete mode 100644 src/gtkflowbox.h delete mode 100644 src/interface-curses.cpp delete mode 100644 src/interface-curses.h create mode 100644 src/interface-curses/Makefile.am create mode 100644 src/interface-curses/interface-curses.cpp create mode 100644 src/interface-curses/interface-curses.h delete mode 100644 src/interface-gtk.cpp delete mode 100644 src/interface-gtk.h create mode 100644 src/interface-gtk/Makefile.am create mode 100644 src/interface-gtk/gtk-info-popup.gob create mode 100644 src/interface-gtk/gtkflowbox.c create mode 100644 src/interface-gtk/gtkflowbox.h create mode 100644 src/interface-gtk/interface-gtk.cpp create mode 100644 src/interface-gtk/interface-gtk.h (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am index 3c60ba0..306bfb2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,21 +1,21 @@ +# The Gtk and Curses UIs have their own subdirectories. +# Either of them will build libsciteco-interface.a +if INTERFACE_GTK +SUBDIRS = interface-gtk +LIBSCITECO_INTERFACE = interface-gtk/libsciteco-interface.la +else +SUBDIRS = interface-curses +LIBSCITECO_INTERFACE = interface-curses/libsciteco-interface.la +endif + include $(top_srcdir)/bootstrap.am include $(top_srcdir)/scintilla.am -AM_CFLAGS = -Wall -std=c99 AM_CXXFLAGS = -Wall -Wno-char-subscripts if CLANG AM_CXXFLAGS += -Wno-mismatched-tags endif -# These paths can be changed at install-time and -# should not be written into config.h: -AM_CPPFLAGS = -D'SCITECODATADIR="$(pkgdatadir)"' \ - -D'SCITECOLIBDIR="@scitecolibdir@"' - -if NEED_COMPAT -AM_CPPFLAGS += -I@top_srcdir@/compat -endif - if STATIC_EXECUTABLES # AM_LDFLAGS are libtool flags, NOT compiler/linker flags AM_LDFLAGS = -all-static @@ -23,61 +23,43 @@ endif BUILT_SOURCES = -EXTRA_DIST = gtk-info-popup.gob \ - symbols-extract.tes \ +EXTRA_DIST = symbols-extract.tes \ sciteco.html -noinst_LIBRARIES = libsciteco-base.a -libsciteco_base_a_SOURCES = main.cpp sciteco.h \ - string-utils.cpp string-utils.h \ - error.cpp error.h \ - cmdline.cpp cmdline.h \ - undo.cpp undo.h \ - expressions.cpp expressions.h \ - document.cpp document.h \ - ioview.cpp ioview.h \ - qregisters.cpp qregisters.h \ - ring.cpp ring.h \ - parser.cpp parser.h \ - search.cpp search.h \ - spawn.cpp spawn.h \ - glob.cpp glob.h \ - goto.cpp goto.h \ - rbtree.cpp rbtree.h \ - symbols.cpp symbols.h \ - interface.cpp interface.h -nodist_libsciteco_base_a_SOURCES = - -if INTERFACE_GTK - -BUILT_SOURCES += gtk-info-popup.c \ - gtk-info-popup.h gtk-info-popup-private.h -nodist_libsciteco_base_a_SOURCES += gtk-info-popup.c - -libsciteco_base_a_SOURCES += interface-gtk.cpp interface-gtk.h -if GTK_FLOW_BOX_FALLBACK -libsciteco_base_a_SOURCES += gtkflowbox.c gtkflowbox.h -endif - -else -# else must be Curses interface - -libsciteco_base_a_SOURCES += interface-curses.cpp interface-curses.h - -endif +noinst_LTLIBRARIES = libsciteco-base.la +libsciteco_base_la_SOURCES = main.cpp sciteco.h \ + string-utils.cpp string-utils.h \ + error.cpp error.h \ + cmdline.cpp cmdline.h \ + undo.cpp undo.h \ + expressions.cpp expressions.h \ + document.cpp document.h \ + ioview.cpp ioview.h \ + qregisters.cpp qregisters.h \ + ring.cpp ring.h \ + parser.cpp parser.h \ + search.cpp search.h \ + spawn.cpp spawn.h \ + glob.cpp glob.h \ + goto.cpp goto.h \ + rbtree.cpp rbtree.h \ + symbols.cpp symbols.h \ + interface.cpp interface.h +# NOTE: We cannot link in Scintilla (static library) into +# a libtool convenience library +libsciteco_base_la_LIBADD = $(LIBSCITECO_INTERFACE) if BOOTSTRAP noinst_PROGRAMS = sciteco-minimal symbols-scintilla.cpp symbols-scilexer.cpp : sciteco-minimal$(EXEEXT) endif sciteco_minimal_SOURCES = symbols-minimal.cpp -sciteco_minimal_LDADD = libsciteco-base.a \ - @SCINTILLA_PATH@/bin/scintilla.a +sciteco_minimal_LDADD = libsciteco-base.la \ + @SCINTILLA_PATH@/bin/scintilla.a bin_PROGRAMS = sciteco sciteco_SOURCES = -nodist_sciteco_SOURCES = $(nodist_sciteco_minimal_SOURCES) \ - symbols-scintilla.cpp symbols-scilexer.cpp +nodist_sciteco_SOURCES = symbols-scintilla.cpp symbols-scilexer.cpp sciteco_LDADD = $(sciteco_minimal_LDADD) # For MinGW: Compile in resource (contains the icon) @@ -90,9 +72,6 @@ endif CLEANFILES = $(BUILT_SOURCES) \ symbols-scintilla.cpp symbols-scilexer.cpp -%.c %.h %-private.h : %.gob - @GOB2@ --gtk3 $< - symbols-scintilla.cpp : @SCINTILLA_PATH@/include/Scintilla.h \ symbols-extract.tes $(BOOTSTRAP_SCITECO) -m @srcdir@/symbols-extract.tes \ diff --git a/src/gtk-info-popup.gob b/src/gtk-info-popup.gob deleted file mode 100644 index 5082091..0000000 --- a/src/gtk-info-popup.gob +++ /dev/null @@ -1,269 +0,0 @@ -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); - } -} diff --git a/src/gtkflowbox.c b/src/gtkflowbox.c deleted file mode 100644 index 1a5c2e9..0000000 --- a/src/gtkflowbox.c +++ /dev/null @@ -1,4795 +0,0 @@ -/* - * Copyright (C) 2007-2010 Openismus GmbH - * Copyright (C) 2013 Red Hat, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library 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 - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, see . - * - * Authors: - * Tristan Van Berkom - * Matthias Clasen - * William Jon McCann - */ - -/* Preamble {{{1 */ - -/** - * SECTION:gtkflowbox - * @Short_Description: A container that allows reflowing its children - * @Title: GtkFlowBox - * - * A GtkFlowBox positions child widgets in sequence according to its - * orientation. - * - * For instance, with the horizontal orientation, the widgets will be - * arranged from left to right, starting a new row under the previous - * row when necessary. Reducing the width in this case will require more - * rows, so a larger height will be requested. - * - * Likewise, with the vertical orientation, the widgets will be arranged - * from top to bottom, starting a new column to the right when necessary. - * Reducing the height will require more columns, so a larger width will - * be requested. - * - * The children of a GtkFlowBox can be dynamically sorted and filtered. - * - * Although a GtkFlowBox must have only #GtkFlowBoxChild children, - * you can add any kind of widget to it via gtk_container_add(), and - * a GtkFlowBoxChild widget will automatically be inserted between - * the box and the widget. - * - * Also see #GtkListBox. - * - * GtkFlowBox was added in GTK+ 3.12. - */ -#ifdef HAVE_CONFIG_H -#include -#endif - -#include - -#include "gtkflowbox.h" - -/* Forward declarations and utilities {{{1 */ - -static void gtk_flow_box_update_cursor (GtkFlowBox *box, - GtkFlowBoxChild *child); -static void gtk_flow_box_select_and_activate (GtkFlowBox *box, - GtkFlowBoxChild *child); -static void gtk_flow_box_update_selection (GtkFlowBox *box, - GtkFlowBoxChild *child, - gboolean modify, - gboolean extend); -static void gtk_flow_box_apply_filter (GtkFlowBox *box, - GtkFlowBoxChild *child); -static void gtk_flow_box_apply_sort (GtkFlowBox *box, - GtkFlowBoxChild *child); -static gint gtk_flow_box_sort (GtkFlowBoxChild *a, - GtkFlowBoxChild *b, - GtkFlowBox *box); - -static void -get_current_selection_modifiers (GtkWidget *widget, - gboolean *modify, - gboolean *extend) -{ - GdkModifierType state = 0; - GdkModifierType mask; - - *modify = FALSE; - *extend = FALSE; - - if (gtk_get_current_event_state (&state)) - { - mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_MODIFY_SELECTION); - if ((state & mask) == mask) - *modify = TRUE; - mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_EXTEND_SELECTION); - if ((state & mask) == mask) - *extend = TRUE; - } -} - -static void -path_from_horizontal_line_rects (cairo_t *cr, - GdkRectangle *lines, - gint n_lines) -{ - gint start_line, end_line; - GdkRectangle *r; - gint i; - - /* Join rows vertically by extending to the middle */ - for (i = 0; i < n_lines - 1; i++) - { - GdkRectangle *r1 = &lines[i]; - GdkRectangle *r2 = &lines[i+1]; - gint gap, old; - - gap = r2->y - (r1->y + r1->height); - r1->height += gap / 2; - old = r2->y; - r2->y = r1->y + r1->height; - r2->height += old - r2->y; - } - - cairo_new_path (cr); - start_line = 0; - - do - { - for (i = start_line; i < n_lines; i++) - { - r = &lines[i]; - if (i == start_line) - cairo_move_to (cr, r->x + r->width, r->y); - else - cairo_line_to (cr, r->x + r->width, r->y); - cairo_line_to (cr, r->x + r->width, r->y + r->height); - - if (i < n_lines - 1 && - (r->x + r->width < lines[i+1].x || - r->x > lines[i+1].x + lines[i+1].width)) - { - i++; - break; - } - } - end_line = i; - for (i = end_line - 1; i >= start_line; i--) - { - r = &lines[i]; - cairo_line_to (cr, r->x, r->y + r->height); - cairo_line_to (cr, r->x, r->y); - } - cairo_close_path (cr); - start_line = end_line; - } - while (end_line < n_lines); -} - -static void -path_from_vertical_line_rects (cairo_t *cr, - GdkRectangle *lines, - gint n_lines) -{ - gint start_line, end_line; - GdkRectangle *r; - gint i; - - /* Join rows horizontally by extending to the middle */ - for (i = 0; i < n_lines - 1; i++) - { - GdkRectangle *r1 = &lines[i]; - GdkRectangle *r2 = &lines[i+1]; - gint gap, old; - - gap = r2->x - (r1->x + r1->width); - r1->width += gap / 2; - old = r2->x; - r2->x = r1->x + r1->width; - r2->width += old - r2->x; - } - - cairo_new_path (cr); - start_line = 0; - - do - { - for (i = start_line; i < n_lines; i++) - { - r = &lines[i]; - if (i == start_line) - cairo_move_to (cr, r->x, r->y + r->height); - else - cairo_line_to (cr, r->x, r->y + r->height); - cairo_line_to (cr, r->x + r->width, r->y + r->height); - - if (i < n_lines - 1 && - (r->y + r->height < lines[i+1].y || - r->y > lines[i+1].y + lines[i+1].height)) - { - i++; - break; - } - } - end_line = i; - for (i = end_line - 1; i >= start_line; i--) - { - r = &lines[i]; - cairo_line_to (cr, r->x + r->width, r->y); - cairo_line_to (cr, r->x, r->y); - } - cairo_close_path (cr); - start_line = end_line; - } - while (end_line < n_lines); -} - -/* GtkFlowBoxChild {{{1 */ - -/* GObject boilerplate {{{2 */ - -enum { - CHILD_ACTIVATE, - CHILD_LAST_SIGNAL -}; - -static guint child_signals[CHILD_LAST_SIGNAL] = { 0 }; - -typedef struct _GtkFlowBoxChildPrivate GtkFlowBoxChildPrivate; -struct _GtkFlowBoxChildPrivate -{ - GSequenceIter *iter; - gboolean selected; -}; - -#define CHILD_PRIV(child) ((GtkFlowBoxChildPrivate*)gtk_flow_box_child_get_instance_private ((GtkFlowBoxChild*)(child))) - -G_DEFINE_TYPE_WITH_PRIVATE (GtkFlowBoxChild, gtk_flow_box_child, GTK_TYPE_BIN) - -/* Internal API {{{2 */ - -static GtkFlowBox * -gtk_flow_box_child_get_box (GtkFlowBoxChild *child) -{ - GtkWidget *parent; - - parent = gtk_widget_get_parent (GTK_WIDGET (child)); - if (parent && GTK_IS_FLOW_BOX (parent)) - return GTK_FLOW_BOX (parent); - - return NULL; -} - -static void -gtk_flow_box_child_set_focus (GtkFlowBoxChild *child) -{ - GtkFlowBox *box = gtk_flow_box_child_get_box (child); - gboolean modify; - gboolean extend; - - get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend); - - if (modify) - gtk_flow_box_update_cursor (box, child); - else - gtk_flow_box_update_selection (box, child, FALSE, FALSE); -} - -/* GtkWidget implementation {{{2 */ - -static gboolean -gtk_flow_box_child_focus (GtkWidget *widget, - GtkDirectionType direction) -{ - gboolean had_focus = FALSE; - GtkWidget *child; - - child = gtk_bin_get_child (GTK_BIN (widget)); - - g_object_get (widget, "has-focus", &had_focus, NULL); - if (had_focus) - { - /* If on row, going right, enter into possible container */ - if (child && - (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD)) - { - if (gtk_widget_child_focus (GTK_WIDGET (child), direction)) - return TRUE; - } - - return FALSE; - } - else if (gtk_container_get_focus_child (GTK_CONTAINER (widget)) != NULL) - { - /* Child has focus, always navigate inside it first */ - if (gtk_widget_child_focus (child, direction)) - return TRUE; - - /* If exiting child container to the left, select child */ - if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD) - { - gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget)); - return TRUE; - } - - return FALSE; - } - else - { - /* If coming from the left, enter into possible container */ - if (child && - (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD)) - { - if (gtk_widget_child_focus (child, direction)) - return TRUE; - } - - gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget)); - return TRUE; - } -} - -static void -gtk_flow_box_child_activate (GtkFlowBoxChild *child) -{ - GtkFlowBox *box; - - box = gtk_flow_box_child_get_box (child); - if (box) - gtk_flow_box_select_and_activate (box, child); -} - -static gboolean -gtk_flow_box_child_draw (GtkWidget *widget, - cairo_t *cr) -{ - GtkAllocation allocation = {0}; - GtkStyleContext* context; - GtkStateFlags state; - GtkBorder border; - gint focus_pad; - - gtk_widget_get_allocation (widget, &allocation); - context = gtk_widget_get_style_context (widget); - state = gtk_widget_get_state_flags (widget); - - gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height); - gtk_render_frame (context, cr, 0, 0, allocation.width, allocation.height); - - if (gtk_widget_has_visible_focus (widget)) - { - gtk_style_context_get_border (context, state, &border); - - gtk_style_context_get_style (context, - "focus-padding", &focus_pad, - NULL); - gtk_render_focus (context, cr, border.left + focus_pad, border.top + focus_pad, - allocation.width - 2 * focus_pad - border.left - border.right, - allocation.height - 2 * focus_pad - border.top - border.bottom); - } - - GTK_WIDGET_CLASS (gtk_flow_box_child_parent_class)->draw (widget, cr); - - return TRUE; -} - -/* Size allocation {{{3 */ - -static void -gtk_flow_box_child_get_full_border (GtkFlowBoxChild *child, - GtkBorder *full_border) -{ - GtkWidget *widget = GTK_WIDGET (child); - GtkStyleContext *context; - GtkStateFlags state; - GtkBorder padding, border; - int focus_width, focus_pad; - - context = gtk_widget_get_style_context (widget); - state = gtk_style_context_get_state (context); - - gtk_style_context_get_padding (context, state, &padding); - gtk_style_context_get_border (context, state, &border); - gtk_style_context_get_style (context, - "focus-line-width", &focus_width, - "focus-padding", &focus_pad, - NULL); - - full_border->left = padding.left + border.left + focus_width + focus_pad; - full_border->right = padding.right + border.right + focus_width + focus_pad; - full_border->top = padding.top + border.top + focus_width + focus_pad; - full_border->bottom = padding.bottom + border.bottom + focus_width + focus_pad; -} - -static GtkSizeRequestMode -gtk_flow_box_child_get_request_mode (GtkWidget *widget) -{ - GtkFlowBox *box; - - box = gtk_flow_box_child_get_box (GTK_FLOW_BOX_CHILD (widget)); - if (box) - return gtk_widget_get_request_mode (GTK_WIDGET (box)); - else - return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; -} - -static void gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget, - gint height, - gint *minimum_width, - gint *natural_width); -static void gtk_flow_box_child_get_preferred_height (GtkWidget *widget, - gint *minimum_height, - gint *natural_height); - -static void -gtk_flow_box_child_get_preferred_height_for_width (GtkWidget *widget, - gint width, - gint *minimum_height, - gint *natural_height) -{ - if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) - { - GtkWidget *child; - gint child_min = 0, child_natural = 0; - GtkBorder full_border = { 0, }; - - gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); - child = gtk_bin_get_child (GTK_BIN (widget)); - if (child && gtk_widget_get_visible (child)) - gtk_widget_get_preferred_height_for_width (child, width - full_border.left - full_border.right, - &child_min, &child_natural); - - if (minimum_height) - *minimum_height = full_border.top + child_min + full_border.bottom; - if (natural_height) - *natural_height = full_border.top + child_natural + full_border.bottom; - } - else - { - gtk_flow_box_child_get_preferred_height (widget, minimum_height, natural_height); - } -} - -static void -gtk_flow_box_child_get_preferred_width (GtkWidget *widget, - gint *minimum_width, - gint *natural_width) -{ - if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) - { - GtkWidget *child; - gint child_min = 0, child_natural = 0; - GtkBorder full_border = { 0, }; - - gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); - child = gtk_bin_get_child (GTK_BIN (widget)); - if (child && gtk_widget_get_visible (child)) - gtk_widget_get_preferred_width (child, &child_min, &child_natural); - - if (minimum_width) - *minimum_width = full_border.left + child_min + full_border.right; - if (natural_width) - *natural_width = full_border.left + child_natural + full_border.right; - } - else - { - gint natural_height; - - gtk_flow_box_child_get_preferred_height (widget, NULL, &natural_height); - gtk_flow_box_child_get_preferred_width_for_height (widget, natural_height, - minimum_width, natural_width); - } -} - -static void -gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget, - gint height, - gint *minimum_width, - gint *natural_width) -{ - if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) - { - gtk_flow_box_child_get_preferred_width (widget, minimum_width, natural_width); - } - else - { - GtkWidget *child; - gint child_min = 0, child_natural = 0; - GtkBorder full_border = { 0, }; - - gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); - child = gtk_bin_get_child (GTK_BIN (widget)); - if (child && gtk_widget_get_visible (child)) - gtk_widget_get_preferred_width_for_height (child, height - full_border.top - full_border.bottom, - &child_min, &child_natural); - - if (minimum_width) - *minimum_width = full_border.left + child_min + full_border.right; - if (natural_width) - *natural_width = full_border.left + child_natural + full_border.right; - } -} - -static void -gtk_flow_box_child_get_preferred_height (GtkWidget *widget, - gint *minimum_height, - gint *natural_height) -{ - if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) - { - gint natural_width; - - gtk_flow_box_child_get_preferred_width (widget, NULL, &natural_width); - gtk_flow_box_child_get_preferred_height_for_width (widget, natural_width, - minimum_height, natural_height); - } - else - { - GtkWidget *child; - gint child_min = 0, child_natural = 0; - GtkBorder full_border = { 0, }; - - gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); - child = gtk_bin_get_child (GTK_BIN (widget)); - if (child && gtk_widget_get_visible (child)) - gtk_widget_get_preferred_height (child, &child_min, &child_natural); - - if (minimum_height) - *minimum_height = full_border.top + child_min + full_border.bottom; - if (natural_height) - *natural_height = full_border.top + child_natural + full_border.bottom; - } -} - -static void -gtk_flow_box_child_size_allocate (GtkWidget *widget, - GtkAllocation *allocation) -{ - GtkWidget *child; - - gtk_widget_set_allocation (widget, allocation); - - child = gtk_bin_get_child (GTK_BIN (widget)); - if (child && gtk_widget_get_visible (child)) - { - GtkAllocation child_allocation; - GtkBorder border = { 0, }; - - gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &border); - - child_allocation.x = allocation->x + border.left; - child_allocation.y = allocation->y + border.top; - child_allocation.width = allocation->width - border.left - border.right; - child_allocation.height = allocation->height - border.top - border.bottom; - - child_allocation.width = MAX (1, child_allocation.width); - child_allocation.height = MAX (1, child_allocation.height); - - gtk_widget_size_allocate (child, &child_allocation); - } -} - -/* GObject implementation {{{2 */ - -static void -gtk_flow_box_child_class_init (GtkFlowBoxChildClass *class) -{ - GObjectClass *object_class = G_OBJECT_CLASS (class); - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); - - widget_class->draw = gtk_flow_box_child_draw; - widget_class->get_request_mode = gtk_flow_box_child_get_request_mode; - widget_class->get_preferred_height = gtk_flow_box_child_get_preferred_height; - widget_class->get_preferred_height_for_width = gtk_flow_box_child_get_preferred_height_for_width; - widget_class->get_preferred_width = gtk_flow_box_child_get_preferred_width; - widget_class->get_preferred_width_for_height = gtk_flow_box_child_get_preferred_width_for_height; - widget_class->size_allocate = gtk_flow_box_child_size_allocate; - widget_class->focus = gtk_flow_box_child_focus; - - class->activate = gtk_flow_box_child_activate; - - /** - * GtkFlowBoxChild::activate: - * @child: The child on which the signal is emitted - * - * The ::activate signal is emitted when the user activates - * a child widget in a #GtkFlowBox, either by clicking or - * double-clicking, or by using the Space or Enter key. - * - * While this signal is used as a - * [keybinding signal][GtkBindingSignal], - * it can be used by applications for their own purposes. - */ - child_signals[CHILD_ACTIVATE] = - g_signal_new ("activate", - G_OBJECT_CLASS_TYPE (object_class), - G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (GtkFlowBoxChildClass, activate), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - widget_class->activate_signal = child_signals[CHILD_ACTIVATE]; - - gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_LIST_ITEM); -} - -static void -gtk_flow_box_child_init (GtkFlowBoxChild *child) -{ - GtkStyleContext *context; - - gtk_widget_set_can_focus (GTK_WIDGET (child), TRUE); - gtk_widget_set_redraw_on_allocate (GTK_WIDGET (child), TRUE); - - context = gtk_widget_get_style_context (GTK_WIDGET (child)); - gtk_style_context_add_class (context, "grid-child"); -} - -/* Public API {{{2 */ - -/** - * gtk_flow_box_child_new: - * - * Creates a new #GtkFlowBoxChild, to be used as a child - * of a #GtkFlowBox. - * - * Returns: a new #GtkFlowBoxChild - * - * Since: 3.12 - */ -GtkWidget * -gtk_flow_box_child_new (void) -{ - return g_object_new (GTK_TYPE_FLOW_BOX_CHILD, NULL); -} - -/** - * gtk_flow_box_child_get_index: - * @child: a #GtkFlowBoxChild - * - * Gets the current index of the @child in its #GtkFlowBox container. - * - * Returns: the index of the @child, or -1 if the @child is not - * in a flow box. - * - * Since: 3.12 - */ -gint -gtk_flow_box_child_get_index (GtkFlowBoxChild *child) -{ - GtkFlowBoxChildPrivate *priv; - - g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), -1); - - priv = CHILD_PRIV (child); - - if (priv->iter != NULL) - return g_sequence_iter_get_position (priv->iter); - - return -1; -} - -/** - * gtk_flow_box_child_is_selected: - * @child: a #GtkFlowBoxChild - * - * Returns whether the @child is currently selected in its - * #GtkFlowBox container. - * - * Returns: %TRUE if @child is selected - * - * Since: 3.12 - */ -gboolean -gtk_flow_box_child_is_selected (GtkFlowBoxChild *child) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), FALSE); - - return CHILD_PRIV (child)->selected; -} - -/** - * gtk_flow_box_child_changed: - * @child: a #GtkFlowBoxChild - * - * Marks @child as changed, causing any state that depends on this - * to be updated. This affects sorting and filtering. - * - * Note that calls to this method must be in sync with the data - * used for the sorting and filtering functions. For instance, if - * the list is mirroring some external data set, and *two* children - * changed in the external data set when you call - * gtk_flow_box_child_changed() on the first child, the sort function - * must only read the new data for the first of the two changed - * children, otherwise the resorting of the children will be wrong. - * - * This generally means that if you don’t fully control the data - * model, you have to duplicate the data that affects the sorting - * and filtering functions into the widgets themselves. Another - * alternative is to call gtk_flow_box_invalidate_sort() on any - * model change, but that is more expensive. - * - * Since: 3.12 - */ -void -gtk_flow_box_child_changed (GtkFlowBoxChild *child) -{ - GtkFlowBox *box; - - g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); - - box = gtk_flow_box_child_get_box (child); - - if (box == NULL) - return; - - gtk_flow_box_apply_sort (box, child); - gtk_flow_box_apply_filter (box, child); -} - -/* GtkFlowBox {{{1 */ - -/* Constants {{{2 */ - -#define DEFAULT_MAX_CHILDREN_PER_LINE 7 -#define RUBBERBAND_START_DISTANCE 32 -#define AUTOSCROLL_FAST_DISTANCE 32 -#define AUTOSCROLL_FACTOR 20 -#define AUTOSCROLL_FACTOR_FAST 10 - -/* GObject boilerplate {{{2 */ - -enum { - CHILD_ACTIVATED, - SELECTED_CHILDREN_CHANGED, - ACTIVATE_CURSOR_CHILD, - TOGGLE_CURSOR_CHILD, - MOVE_CURSOR, - SELECT_ALL, - UNSELECT_ALL, - LAST_SIGNAL -}; - -static guint signals[LAST_SIGNAL] = { 0 }; - -enum { - PROP_0, - PROP_ORIENTATION, - PROP_HOMOGENEOUS, - PROP_HALIGN_POLICY, - PROP_VALIGN_POLICY, - PROP_COLUMN_SPACING, - PROP_ROW_SPACING, - PROP_MIN_CHILDREN_PER_LINE, - PROP_MAX_CHILDREN_PER_LINE, - PROP_SELECTION_MODE, - PROP_ACTIVATE_ON_SINGLE_CLICK -}; - -typedef struct _GtkFlowBoxPrivate GtkFlowBoxPrivate; -struct _GtkFlowBoxPrivate { - GtkOrientation orientation; - gboolean homogeneous; - - guint row_spacing; - guint column_spacing; - - GtkFlowBoxChild *prelight_child; - GtkFlowBoxChild *cursor_child; - GtkFlowBoxChild *selected_child; - - gboolean active_child_active; - GtkFlowBoxChild *active_child; - - GtkSelectionMode selection_mode; - - GtkAdjustment *hadjustment; - GtkAdjustment *vadjustment; - gboolean activate_on_single_click; - - guint16 min_children_per_line; - guint16 max_children_per_line; - guint16 cur_children_per_line; - - GSequence *children; - - GtkFlowBoxFilterFunc filter_func; - gpointer filter_data; - GDestroyNotify filter_destroy; - - GtkFlowBoxSortFunc sort_func; - gpointer sort_data; - GDestroyNotify sort_destroy; - - gboolean track_motion; - gboolean rubberband_select; - GtkFlowBoxChild *rubberband_first; - GtkFlowBoxChild *rubberband_last; - gint button_down_x; - gint button_down_y; - gboolean rubberband_modify; - gboolean rubberband_extend; - GdkDevice *rubberband_device; - - GtkScrollType autoscroll_mode; - guint autoscroll_id; -}; - -#define BOX_PRIV(box) ((GtkFlowBoxPrivate*)gtk_flow_box_get_instance_private ((GtkFlowBox*)(box))) - -G_DEFINE_TYPE_WITH_CODE (GtkFlowBox, gtk_flow_box, GTK_TYPE_CONTAINER, - G_ADD_PRIVATE (GtkFlowBox) - G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) - -/* Internal API, utilities {{{2 */ - -#define ORIENTATION_ALIGN(box) \ - (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \ - ? gtk_widget_get_halign (GTK_WIDGET (box)) \ - : gtk_widget_get_valign (GTK_WIDGET (box))) - -#define OPPOSING_ORIENTATION_ALIGN(box) \ - (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \ - ? gtk_widget_get_valign (GTK_WIDGET (box)) \ - : gtk_widget_get_halign (GTK_WIDGET (box))) - -/* Children are visible if they are shown by the app (visible) - * and not filtered out (child_visible) by the box - */ -static inline gboolean -child_is_visible (GtkWidget *child) -{ - return gtk_widget_get_visible (child) && - gtk_widget_get_child_visible (child); -} - -static gint -get_visible_children (GtkFlowBox *box) -{ - GSequenceIter *iter; - gint i = 0; - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - - child = g_sequence_get (iter); - if (child_is_visible (child)) - i++; - } - - return i; -} - -static GtkFlowBoxChild * -gtk_flow_box_find_child_at_pos (GtkFlowBox *box, - gint x, - gint y) -{ - GtkWidget *child; - GSequenceIter *iter; - GtkAllocation allocation; - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - child = g_sequence_get (iter); - if (!child_is_visible (child)) - continue; - gtk_widget_get_allocation (child, &allocation); - if (x >= allocation.x && x < (allocation.x + allocation.width) && - y >= allocation.y && y < (allocation.y + allocation.height)) - return GTK_FLOW_BOX_CHILD (child); - } - - return NULL; -} - -static void -gtk_flow_box_update_prelight (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - if (child != priv->prelight_child) - { - priv->prelight_child = child; - gtk_widget_queue_draw (GTK_WIDGET (box)); - } -} - -static void -gtk_flow_box_update_active (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gboolean val; - - val = priv->active_child == child; - if (priv->active_child != NULL && - val != priv->active_child_active) - { - priv->active_child_active = val; - gtk_widget_queue_draw (GTK_WIDGET (box)); - } -} - -static void -gtk_flow_box_apply_filter (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gboolean do_show; - - do_show = TRUE; - if (priv->filter_func != NULL) - do_show = priv->filter_func (child, priv->filter_data); - - gtk_widget_set_child_visible (GTK_WIDGET (child), do_show); -} - -static void -gtk_flow_box_apply_filter_all (GtkFlowBox *box) -{ - GSequenceIter *iter; - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkFlowBoxChild *child; - - child = g_sequence_get (iter); - gtk_flow_box_apply_filter (box, child); - } - gtk_widget_queue_resize (GTK_WIDGET (box)); -} - -static void -gtk_flow_box_apply_sort (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - if (BOX_PRIV (box)->sort_func != NULL) - { - g_sequence_sort_changed (CHILD_PRIV (child)->iter, - (GCompareDataFunc)gtk_flow_box_sort, box); - gtk_widget_queue_resize (GTK_WIDGET (box)); - } -} - -/* Selection utilities {{{3 */ - -static gboolean -gtk_flow_box_child_set_selected (GtkFlowBoxChild *child, - gboolean selected) -{ - if (CHILD_PRIV (child)->selected != selected) - { - CHILD_PRIV (child)->selected = selected; - if (selected) - gtk_widget_set_state_flags (GTK_WIDGET (child), - GTK_STATE_FLAG_SELECTED, FALSE); - else - gtk_widget_unset_state_flags (GTK_WIDGET (child), - GTK_STATE_FLAG_SELECTED); - - gtk_widget_queue_draw (GTK_WIDGET (child)); - - return TRUE; - } - - return FALSE; -} - -static gboolean -gtk_flow_box_unselect_all_internal (GtkFlowBox *box) -{ - GtkFlowBoxChild *child; - GSequenceIter *iter; - gboolean dirty = FALSE; - - if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) - return FALSE; - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - child = g_sequence_get (iter); - dirty |= gtk_flow_box_child_set_selected (child, FALSE); - } - - return dirty; -} - -static void -gtk_flow_box_unselect_child_internal (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - if (!CHILD_PRIV (child)->selected) - return; - - if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) - return; - else if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) - gtk_flow_box_unselect_all_internal (box); - else - gtk_flow_box_child_set_selected (child, FALSE); - - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); -} - -static void -gtk_flow_box_update_cursor (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - BOX_PRIV (box)->cursor_child = child; - gtk_widget_grab_focus (GTK_WIDGET (child)); - gtk_widget_queue_draw (GTK_WIDGET (child)); -} - -static void -gtk_flow_box_select_child_internal (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - if (CHILD_PRIV (child)->selected) - return; - - if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) - return; - if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) - gtk_flow_box_unselect_all_internal (box); - - gtk_flow_box_child_set_selected (child, TRUE); - BOX_PRIV (box)->selected_child = child; - - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); -} - -static void -gtk_flow_box_select_all_between (GtkFlowBox *box, - GtkFlowBoxChild *child1, - GtkFlowBoxChild *child2, - gboolean modify) -{ - GSequenceIter *iter, *iter1, *iter2; - - if (child1) - iter1 = CHILD_PRIV (child1)->iter; - else - iter1 = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - - if (child2) - iter2 = CHILD_PRIV (child2)->iter; - else - iter2 = g_sequence_get_end_iter (BOX_PRIV (box)->children); - - if (g_sequence_iter_compare (iter2, iter1) < 0) - { - iter = iter1; - iter1 = iter2; - iter2 = iter; - } - - for (iter = iter1; - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - - child = g_sequence_get (iter); - if (child_is_visible (child)) - { - if (modify) - gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), !CHILD_PRIV (child)->selected); - else - gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), TRUE); - } - - if (g_sequence_iter_compare (iter, iter2) == 0) - break; - } -} - -static void -gtk_flow_box_update_selection (GtkFlowBox *box, - GtkFlowBoxChild *child, - gboolean modify, - gboolean extend) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - gtk_flow_box_update_cursor (box, child); - - if (priv->selection_mode == GTK_SELECTION_NONE) - return; - - if (priv->selection_mode == GTK_SELECTION_BROWSE) - { - gtk_flow_box_unselect_all_internal (box); - gtk_flow_box_child_set_selected (child, TRUE); - priv->selected_child = child; - } - else if (priv->selection_mode == GTK_SELECTION_SINGLE) - { - gboolean was_selected; - - was_selected = CHILD_PRIV (child)->selected; - gtk_flow_box_unselect_all_internal (box); - gtk_flow_box_child_set_selected (child, modify ? !was_selected : TRUE); - priv->selected_child = CHILD_PRIV (child)->selected ? child : NULL; - } - else /* GTK_SELECTION_MULTIPLE */ - { - if (extend) - { - gtk_flow_box_unselect_all_internal (box); - if (priv->selected_child == NULL) - { - gtk_flow_box_child_set_selected (child, TRUE); - priv->selected_child = child; - } - else - gtk_flow_box_select_all_between (box, priv->selected_child, child, FALSE); - } - else - { - if (modify) - { - gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected); - } - else - { - gtk_flow_box_unselect_all_internal (box); - gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected); - priv->selected_child = child; - } - } - } - - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); -} - -static void -gtk_flow_box_select_and_activate (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - if (child != NULL) - { - gtk_flow_box_select_child_internal (box, child); - gtk_flow_box_update_cursor (box, child); - g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child); - } -} - -/* Focus utilities {{{3 */ - -static GSequenceIter * -gtk_flow_box_get_previous_focusable (GtkFlowBox *box, - GSequenceIter *iter) -{ - GtkFlowBoxChild *child; - - while (!g_sequence_iter_is_begin (iter)) - { - iter = g_sequence_iter_prev (iter); - child = g_sequence_get (iter); - if (child_is_visible (GTK_WIDGET (child)) && - gtk_widget_is_sensitive (GTK_WIDGET (child))) - return iter; - } - - return NULL; -} - -static GSequenceIter * -gtk_flow_box_get_next_focusable (GtkFlowBox *box, - GSequenceIter *iter) -{ - GtkFlowBoxChild *child; - - while (TRUE) - { - iter = g_sequence_iter_next (iter); - if (g_sequence_iter_is_end (iter)) - return NULL; - child = g_sequence_get (iter); - if (child_is_visible (GTK_WIDGET (child)) && - gtk_widget_is_sensitive (GTK_WIDGET (child))) - return iter; - } - - return NULL; -} - -static GSequenceIter * -gtk_flow_box_get_first_focusable (GtkFlowBox *box) -{ - GSequenceIter *iter; - GtkFlowBoxChild *child; - - iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - if (g_sequence_iter_is_end (iter)) - return NULL; - - child = g_sequence_get (iter); - if (child_is_visible (GTK_WIDGET (child)) && - gtk_widget_is_sensitive (GTK_WIDGET (child))) - return iter; - - return gtk_flow_box_get_next_focusable (box, iter); -} - -static GSequenceIter * -gtk_flow_box_get_last_focusable (GtkFlowBox *box) -{ - GSequenceIter *iter; - - iter = g_sequence_get_end_iter (BOX_PRIV (box)->children); - return gtk_flow_box_get_previous_focusable (box, iter); -} - - -static GSequenceIter * -gtk_flow_box_get_above_focusable (GtkFlowBox *box, - GSequenceIter *iter) -{ - GtkFlowBoxChild *child = NULL; - gint i; - - while (TRUE) - { - i = 0; - while (i < BOX_PRIV (box)->cur_children_per_line) - { - if (g_sequence_iter_is_begin (iter)) - return NULL; - iter = g_sequence_iter_prev (iter); - child = g_sequence_get (iter); - if (child_is_visible (GTK_WIDGET (child))) - i++; - } - if (child && gtk_widget_get_sensitive (GTK_WIDGET (child))) - return iter; - } - - return NULL; -} - -static GSequenceIter * -gtk_flow_box_get_below_focusable (GtkFlowBox *box, - GSequenceIter *iter) -{ - GtkFlowBoxChild *child = NULL; - gint i; - - while (TRUE) - { - i = 0; - while (i < BOX_PRIV (box)->cur_children_per_line) - { - iter = g_sequence_iter_next (iter); - if (g_sequence_iter_is_end (iter)) - return NULL; - child = g_sequence_get (iter); - if (child_is_visible (GTK_WIDGET (child))) - i++; - } - if (child && gtk_widget_get_sensitive (GTK_WIDGET (child))) - return iter; - } - - return NULL; -} - -/* GtkWidget implementation {{{2 */ - -/* Size allocation {{{3 */ - -/* Used in columned modes where all items share at least their - * equal widths or heights - */ -static void -get_max_item_size (GtkFlowBox *box, - GtkOrientation orientation, - gint *min_size, - gint *nat_size) -{ - GSequenceIter *iter; - gint max_min_size = 0; - gint max_nat_size = 0; - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - gint child_min, child_nat; - - child = g_sequence_get (iter); - - if (!child_is_visible (child)) - continue; - - if (orientation == GTK_ORIENTATION_HORIZONTAL) - gtk_widget_get_preferred_width (child, &child_min, &child_nat); - else - gtk_widget_get_preferred_height (child, &child_min, &child_nat); - - max_min_size = MAX (max_min_size, child_min); - max_nat_size = MAX (max_nat_size, child_nat); - } - - if (min_size) - *min_size = max_min_size; - - if (nat_size) - *nat_size = max_nat_size; -} - - -/* Gets the largest minimum/natural size for a given size (used to get - * the largest item heights for a fixed item width and the opposite) - */ -static void -get_largest_size_for_opposing_orientation (GtkFlowBox *box, - GtkOrientation orientation, - gint item_size, - gint *min_item_size, - gint *nat_item_size) -{ - GSequenceIter *iter; - gint max_min_size = 0; - gint max_nat_size = 0; - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - gint child_min, child_nat; - - child = g_sequence_get (iter); - - if (!child_is_visible (child)) - continue; - - if (orientation == GTK_ORIENTATION_HORIZONTAL) - gtk_widget_get_preferred_height_for_width (child, - item_size, - &child_min, &child_nat); - else - gtk_widget_get_preferred_width_for_height (child, - item_size, - &child_min, &child_nat); - - max_min_size = MAX (max_min_size, child_min); - max_nat_size = MAX (max_nat_size, child_nat); - } - - if (min_item_size) - *min_item_size = max_min_size; - - if (nat_item_size) - *nat_item_size = max_nat_size; -} - -/* Gets the largest minimum/natural size on a single line for a given size - * (used to get the largest line heights for a fixed item width and the opposite - * while iterating over a list of children, note the new index is returned) - */ -static GSequenceIter * -get_largest_size_for_line_in_opposing_orientation (GtkFlowBox *box, - GtkOrientation orientation, - GSequenceIter *cursor, - gint line_length, - GtkRequestedSize *item_sizes, - gint extra_pixels, - gint *min_item_size, - gint *nat_item_size) -{ - GSequenceIter *iter; - gint max_min_size = 0; - gint max_nat_size = 0; - gint i; - - i = 0; - for (iter = cursor; - !g_sequence_iter_is_end (iter) && i < line_length; - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - gint child_min, child_nat, this_item_size; - - child = g_sequence_get (iter); - - if (!child_is_visible (child)) - continue; - - /* Distribute the extra pixels to the first children in the line - * (could be fancier and spread them out more evenly) */ - this_item_size = item_sizes[i].minimum_size; - if (extra_pixels > 0 && ORIENTATION_ALIGN (box) == GTK_ALIGN_FILL) - { - this_item_size++; - extra_pixels--; - } - - if (orientation == GTK_ORIENTATION_HORIZONTAL) - gtk_widget_get_preferred_height_for_width (child, - this_item_size, - &child_min, &child_nat); - else - gtk_widget_get_preferred_width_for_height (child, - this_item_size, - &child_min, &child_nat); - - max_min_size = MAX (max_min_size, child_min); - max_nat_size = MAX (max_nat_size, child_nat); - - i++; - } - - if (min_item_size) - *min_item_size = max_min_size; - - if (nat_item_size) - *nat_item_size = max_nat_size; - - /* Return next item in the list */ - return iter; -} - -/* fit_aligned_item_requests() helper */ -static gint -gather_aligned_item_requests (GtkFlowBox *box, - GtkOrientation orientation, - gint line_length, - gint item_spacing, - gint n_children, - GtkRequestedSize *item_sizes) -{ - GSequenceIter *iter; - gint i; - gint extra_items, natural_line_size = 0; - - extra_items = n_children % line_length; - - i = 0; - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - GtkAlign item_align; - gint child_min, child_nat; - gint position; - - child = g_sequence_get (iter); - - if (!child_is_visible (child)) - continue; - - if (orientation == GTK_ORIENTATION_HORIZONTAL) - gtk_widget_get_preferred_width (child, - &child_min, &child_nat); - else - gtk_widget_get_preferred_height (child, - &child_min, &child_nat); - - /* Get the index and push it over for the last line when spreading to the end */ - position = i % line_length; - - item_align = ORIENTATION_ALIGN (box); - if (item_align == GTK_ALIGN_END && i >= n_children - extra_items) - position += line_length - extra_items; - - /* Round up the size of every column/row */ - item_sizes[position].minimum_size = MAX (item_sizes[position].minimum_size, child_min); - item_sizes[position].natural_size = MAX (item_sizes[position].natural_size, child_nat); - - i++; - } - - for (i = 0; i < line_length; i++) - natural_line_size += item_sizes[i].natural_size; - - natural_line_size += (line_length - 1) * item_spacing; - - return natural_line_size; -} - -static GtkRequestedSize * -fit_aligned_item_requests (GtkFlowBox *box, - GtkOrientation orientation, - gint avail_size, - gint item_spacing, - gint *line_length, /* in-out */ - gint items_per_line, - gint n_children) -{ - GtkRequestedSize *sizes, *try_sizes; - gint try_line_size, try_length; - - sizes = g_new0 (GtkRequestedSize, *line_length); - - /* get the sizes for the initial guess */ - try_line_size = gather_aligned_item_requests (box, - orientation, - *line_length, - item_spacing, - n_children, - sizes); - - /* Try columnizing the whole thing and adding an item to the end of - * the line; try to fit as many columns into the available size as - * possible - */ - for (try_length = *line_length + 1; try_line_size < avail_size; try_length++) - { - try_sizes = g_new0 (GtkRequestedSize, try_length); - try_line_size = gather_aligned_item_requests (box, - orientation, - try_length, - item_spacing, - n_children, - try_sizes); - - if (try_line_size <= avail_size && - items_per_line >= try_length) - { - *line_length = try_length; - - g_free (sizes); - sizes = try_sizes; - } - else - { - /* oops, this one failed; stick to the last size that fit and then return */ - g_free (try_sizes); - break; - } - } - - return sizes; -} - -typedef struct { - GArray *requested; - gint extra_pixels; -} AllocatedLine; - -static gint -get_offset_pixels (GtkAlign align, - gint pixels) -{ - gint offset; - - switch (align) { - case GTK_ALIGN_START: - case GTK_ALIGN_FILL: - offset = 0; - break; - case GTK_ALIGN_CENTER: - offset = pixels / 2; - break; - case GTK_ALIGN_END: - offset = pixels; - break; - default: - g_assert_not_reached (); - break; - } - - return offset; -} - -static void -gtk_flow_box_size_allocate (GtkWidget *widget, - GtkAllocation *allocation) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - GtkAllocation child_allocation; - gint avail_size, avail_other_size, min_items, item_spacing, line_spacing; - GtkAlign item_align; - GtkAlign line_align; - GdkWindow *window; - GtkRequestedSize *line_sizes = NULL; - GtkRequestedSize *item_sizes = NULL; - gint min_item_size, nat_item_size; - gint line_length; - gint item_size = 0; - gint line_size = 0, min_fixed_line_size = 0, nat_fixed_line_size = 0; - gint line_offset, item_offset, n_children, n_lines, line_count; - gint extra_pixels = 0, extra_per_item = 0, extra_extra = 0; - gint extra_line_pixels = 0, extra_per_line = 0, extra_line_extra = 0; - gint i, this_line_size; - GSequenceIter *iter; - - child_allocation.x = 0; - child_allocation.y = 0; - child_allocation.width = 0; - child_allocation.height = 0; - - gtk_widget_set_allocation (widget, allocation); - window = gtk_widget_get_window (widget); - if (window != NULL) - gdk_window_move_resize (window, - allocation->x, allocation->y, - allocation->width, allocation->height); - - child_allocation.x = 0; - child_allocation.y = 0; - child_allocation.width = allocation->width; - - min_items = MAX (1, priv->min_children_per_line); - - if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) - { - avail_size = allocation->width; - avail_other_size = allocation->height; - item_spacing = priv->column_spacing; line_spacing = priv->row_spacing; - } - else /* GTK_ORIENTATION_VERTICAL */ - { - avail_size = allocation->height; - avail_other_size = allocation->width; - item_spacing = priv->row_spacing; - line_spacing = priv->column_spacing; - } - - item_align = ORIENTATION_ALIGN (box); - line_align = OPPOSING_ORIENTATION_ALIGN (box); - - /* Get how many lines we'll be needing to flow */ - n_children = get_visible_children (box); - if (n_children <= 0) - return; - - /* - * Deal with ALIGNED/HOMOGENEOUS modes first, start with - * initial guesses at item/line sizes - */ - get_max_item_size (box, priv->orientation, &min_item_size, &nat_item_size); - if (nat_item_size <= 0) - return; - - /* By default flow at the natural item width */ - line_length = avail_size / (nat_item_size + item_spacing); - - /* After the above aproximation, check if we cant fit one more on the line */ - if (line_length * item_spacing + (line_length + 1) * nat_item_size <= avail_size) - line_length++; - - /* Its possible we were allocated just less than the natural width of the - * minimum item flow length */ - line_length = MAX (min_items, line_length); - line_length = MIN (line_length, priv->max_children_per_line); - - /* Here we just use the largest height-for-width and use that for the height - * of all lines */ - if (priv->homogeneous) - { - n_lines = n_children / line_length; - if ((n_children % line_length) > 0) - n_lines++; - - n_lines = MAX (n_lines, 1); - - /* Now we need the real item allocation size */ - item_size = (avail_size - (line_length - 1) * item_spacing) / line_length; - - /* Cut out the expand space if we're not distributing any */ - if (item_align != GTK_ALIGN_FILL) - item_size = MIN (item_size, nat_item_size); - - get_largest_size_for_opposing_orientation (box, - priv->orientation, - item_size, - &min_fixed_line_size, - &nat_fixed_line_size); - - /* resolve a fixed 'line_size' */ - line_size = (avail_other_size - (n_lines - 1) * line_spacing) / n_lines; - - if (line_align != GTK_ALIGN_FILL) - line_size = MIN (line_size, nat_fixed_line_size); - - /* Get the real extra pixels incase of GTK_ALIGN_START lines */ - extra_pixels = avail_size - (line_length - 1) * item_spacing - item_size * line_length; - extra_line_pixels = avail_other_size - (n_lines - 1) * line_spacing - line_size * n_lines; - } - else - { - gboolean first_line = TRUE; - - /* Find the amount of columns that can fit aligned into the available space - * and collect their requests. - */ - item_sizes = fit_aligned_item_requests (box, - priv->orientation, - avail_size, - item_spacing, - &line_length, - priv->max_children_per_line, - n_children); - - /* Calculate the number of lines after determining the final line_length */ - n_lines = n_children / line_length; - if ((n_children % line_length) > 0) - n_lines++; - - n_lines = MAX (n_lines, 1); - line_sizes = g_new0 (GtkRequestedSize, n_lines); - - /* Get the available remaining size */ - avail_size -= (line_length - 1) * item_spacing; - for (i = 0; i < line_length; i++) - avail_size -= item_sizes[i].minimum_size; - - /* Perform a natural allocation on the columnized items and get the remaining pixels */ - if (avail_size > 0) - extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); - - /* Now that we have the size of each column of items find the size of each individual - * line based on the aligned item sizes. - */ - - for (i = 0, iter = g_sequence_get_begin_iter (priv->children); - !g_sequence_iter_is_end (iter) && i < n_lines; - i++) - { - iter = get_largest_size_for_line_in_opposing_orientation (box, - priv->orientation, - iter, - line_length, - item_sizes, - extra_pixels, - &line_sizes[i].minimum_size, - &line_sizes[i].natural_size); - - - /* Its possible a line is made of completely invisible children */ - if (line_sizes[i].natural_size > 0) - { - if (first_line) - first_line = FALSE; - else - avail_other_size -= line_spacing; - - avail_other_size -= line_sizes[i].minimum_size; - - line_sizes[i].data = GINT_TO_POINTER (i); - } - } - - /* Distribute space among lines naturally */ - if (avail_other_size > 0) - extra_line_pixels = gtk_distribute_natural_allocation (avail_other_size, n_lines, line_sizes); - } - - /* - * Initial sizes of items/lines guessed at this point, - * go on to distribute expand space if needed. - */ - - priv->cur_children_per_line = line_length; - - /* FIXME: This portion needs to consider which columns - * and rows asked for expand space and distribute those - * accordingly for the case of ALIGNED allocation. - * - * If at least one child in a column/row asked for expand; - * we should make that row/column expand entirely. - */ - - /* Calculate expand space per item */ - if (item_align == GTK_ALIGN_FILL) - { - extra_per_item = extra_pixels / line_length; - extra_extra = extra_pixels % line_length; - } - - /* Calculate expand space per line */ - if (line_align == GTK_ALIGN_FILL) - { - extra_per_line = extra_line_pixels / n_lines; - extra_line_extra = extra_line_pixels % n_lines; - } - - /* - * Prepare item/line initial offsets and jump into the - * real allocation loop. - */ - line_offset = item_offset = 0; - - /* prepend extra space to item_offset/line_offset for SPREAD_END */ - item_offset += get_offset_pixels (item_align, extra_pixels); - line_offset += get_offset_pixels (line_align, extra_line_pixels); - - /* Get the allocation size for the first line */ - if (priv->homogeneous) - this_line_size = line_size; - else - { - this_line_size = line_sizes[0].minimum_size; - - if (line_align == GTK_ALIGN_FILL) - { - this_line_size += extra_per_line; - - if (extra_line_extra > 0) - this_line_size++; - } - } - - i = 0; - line_count = 0; - for (iter = g_sequence_get_begin_iter (priv->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - gint position; - gint this_item_size; - - child = g_sequence_get (iter); - - if (!child_is_visible (child)) - continue; - - /* Get item position */ - position = i % line_length; - - /* adjust the line_offset/count at the beginning of each new line */ - if (i > 0 && position == 0) - { - /* Push the line_offset */ - line_offset += this_line_size + line_spacing; - - line_count++; - - /* Get the new line size */ - if (priv->homogeneous) - this_line_size = line_size; - else - { - this_line_size = line_sizes[line_count].minimum_size; - - if (line_align == GTK_ALIGN_FILL) - { - this_line_size += extra_per_line; - - if (line_count < extra_line_extra) - this_line_size++; - } - } - - item_offset = 0; - - if (item_align == GTK_ALIGN_CENTER) - { - item_offset += get_offset_pixels (item_align, extra_pixels); - } - else if (item_align == GTK_ALIGN_END) - { - item_offset += get_offset_pixels (item_align, extra_pixels); - - /* If we're on the last line, prepend the space for - * any leading items */ - if (line_count == n_lines -1) - { - gint extra_items = n_children % line_length; - - if (priv->homogeneous) - { - item_offset += item_size * (line_length - extra_items); - item_offset += item_spacing * (line_length - extra_items); - } - else - { - gint j; - - for (j = 0; j < (line_length - extra_items); j++) - { - item_offset += item_sizes[j].minimum_size; - item_offset += item_spacing; - } - } - } - } - } - - /* Push the index along for the last line when spreading to the end */ - if (item_align == GTK_ALIGN_END && line_count == n_lines -1) - { - gint extra_items = n_children % line_length; - - position += line_length - extra_items; - } - - if (priv->homogeneous) - this_item_size = item_size; - else - this_item_size = item_sizes[position].minimum_size; - - if (item_align == GTK_ALIGN_FILL) - { - this_item_size += extra_per_item; - - if (position < extra_extra) - this_item_size++; - } - - /* Do the actual allocation */ - if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) - { - child_allocation.x = item_offset; - child_allocation.y = line_offset; - child_allocation.width = this_item_size; - child_allocation.height = this_line_size; - } - else /* GTK_ORIENTATION_VERTICAL */ - { - child_allocation.x = line_offset; - child_allocation.y = item_offset; - child_allocation.width = this_line_size; - child_allocation.height = this_item_size; - } - - if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) - child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width; - gtk_widget_size_allocate (child, &child_allocation); - - item_offset += this_item_size; - item_offset += item_spacing; - - i++; - } - - g_free (item_sizes); - g_free (line_sizes); -} - -static GtkSizeRequestMode -gtk_flow_box_get_request_mode (GtkWidget *widget) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - - return (BOX_PRIV (box)->orientation == GTK_ORIENTATION_HORIZONTAL) ? - GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH : GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT; -} - -/* Gets the largest minimum and natural length of - * 'line_length' consecutive items when aligned into rows/columns */ -static void -get_largest_aligned_line_length (GtkFlowBox *box, - GtkOrientation orientation, - gint line_length, - gint *min_size, - gint *nat_size) -{ - GSequenceIter *iter; - gint max_min_size = 0; - gint max_nat_size = 0; - gint spacing, i; - GtkRequestedSize *aligned_item_sizes; - - if (orientation == GTK_ORIENTATION_HORIZONTAL) - spacing = BOX_PRIV (box)->column_spacing; - else - spacing = BOX_PRIV (box)->row_spacing; - - aligned_item_sizes = g_new0 (GtkRequestedSize, line_length); - - /* Get the largest sizes of each index in the line. - */ - i = 0; - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - gint child_min, child_nat; - - child = g_sequence_get (iter); - if (!child_is_visible (child)) - continue; - - if (orientation == GTK_ORIENTATION_HORIZONTAL) - gtk_widget_get_preferred_width (child, - &child_min, &child_nat); - else /* GTK_ORIENTATION_VERTICAL */ - gtk_widget_get_preferred_height (child, - &child_min, &child_nat); - - aligned_item_sizes[i % line_length].minimum_size = - MAX (aligned_item_sizes[i % line_length].minimum_size, child_min); - - aligned_item_sizes[i % line_length].natural_size = - MAX (aligned_item_sizes[i % line_length].natural_size, child_nat); - - i++; - } - - /* Add up the largest indexes */ - for (i = 0; i < line_length; i++) - { - max_min_size += aligned_item_sizes[i].minimum_size; - max_nat_size += aligned_item_sizes[i].natural_size; - } - - g_free (aligned_item_sizes); - - max_min_size += (line_length - 1) * spacing; - max_nat_size += (line_length - 1) * spacing; - - if (min_size) - *min_size = max_min_size; - - if (nat_size) - *nat_size = max_nat_size; -} - - -static void -gtk_flow_box_get_preferred_width (GtkWidget *widget, - gint *minimum_size, - gint *natural_size) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gint min_item_width, nat_item_width; - gint min_items, nat_items; - gint min_width, nat_width; - - min_items = MAX (1, priv->min_children_per_line); - nat_items = MAX (min_items, priv->max_children_per_line); - - if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) - { - min_width = nat_width = 0; - - if (!priv->homogeneous) - { - /* When not homogeneous; horizontally oriented boxes - * need enough width for the widest row */ - if (min_items == 1) - { - get_max_item_size (box, - GTK_ORIENTATION_HORIZONTAL, - &min_item_width, - &nat_item_width); - - min_width += min_item_width; - nat_width += nat_item_width; - } - else - { - gint min_line_length, nat_line_length; - - get_largest_aligned_line_length (box, - GTK_ORIENTATION_HORIZONTAL, - min_items, - &min_line_length, - &nat_line_length); - - if (nat_items > min_items) - get_largest_aligned_line_length (box, - GTK_ORIENTATION_HORIZONTAL, - nat_items, - NULL, - &nat_line_length); - - min_width += min_line_length; - nat_width += nat_line_length; - } - } - else /* In homogeneous mode; horizontally oriented boxs - * give the same width to all children */ - { - get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, - &min_item_width, &nat_item_width); - - min_width += min_item_width * min_items; - min_width += (min_items -1) * priv->column_spacing; - - nat_width += nat_item_width * nat_items; - nat_width += (nat_items -1) * priv->column_spacing; - } - } - else /* GTK_ORIENTATION_VERTICAL */ - { - /* Return the width for the minimum height */ - gint min_height; - - GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL); - GTK_WIDGET_GET_CLASS (widget)->get_preferred_width_for_height (widget, - min_height, - &min_width, - &nat_width); - - } - - if (minimum_size) - *minimum_size = min_width; - - if (natural_size) - *natural_size = nat_width; -} - -static void -gtk_flow_box_get_preferred_height (GtkWidget *widget, - gint *minimum_size, - gint *natural_size) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gint min_item_height, nat_item_height; - gint min_items, nat_items; - gint min_height, nat_height; - - min_items = MAX (1, priv->min_children_per_line); - nat_items = MAX (min_items, priv->max_children_per_line); - - if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) - { - /* Return the height for the minimum width */ - gint min_width; - - GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL); - GTK_WIDGET_GET_CLASS (widget)->get_preferred_height_for_width (widget, - min_width, - &min_height, - &nat_height); - } - else /* GTK_ORIENTATION_VERTICAL */ - { - min_height = nat_height = 0; - - if (! priv->homogeneous) - { - /* When not homogeneous; vertically oriented boxes - * need enough height for the tallest column */ - if (min_items == 1) - { - get_max_item_size (box, GTK_ORIENTATION_VERTICAL, - &min_item_height, &nat_item_height); - - min_height += min_item_height; - nat_height += nat_item_height; - } - else - { - gint min_line_length, nat_line_length; - - get_largest_aligned_line_length (box, - GTK_ORIENTATION_VERTICAL, - min_items, - &min_line_length, - &nat_line_length); - - if (nat_items > min_items) - get_largest_aligned_line_length (box, - GTK_ORIENTATION_VERTICAL, - nat_items, - NULL, - &nat_line_length); - - min_height += min_line_length; - nat_height += nat_line_length; - } - - } - else - { - /* In homogeneous mode; vertically oriented boxes - * give the same height to all children - */ - get_max_item_size (box, - GTK_ORIENTATION_VERTICAL, - &min_item_height, - &nat_item_height); - - min_height += min_item_height * min_items; - min_height += (min_items -1) * priv->row_spacing; - - nat_height += nat_item_height * nat_items; - nat_height += (nat_items -1) * priv->row_spacing; - } - } - - if (minimum_size) - *minimum_size = min_height; - - if (natural_size) - *natural_size = nat_height; -} - -static void -gtk_flow_box_get_preferred_height_for_width (GtkWidget *widget, - gint width, - gint *minimum_height, - gint *natural_height) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gint min_item_width, nat_item_width; - gint min_items; - gint min_height, nat_height; - gint avail_size, n_children; - - min_items = MAX (1, priv->min_children_per_line); - - min_height = 0; - nat_height = 0; - - if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) - { - gint min_width; - gint line_length; - gint item_size, extra_pixels; - - n_children = get_visible_children (box); - if (n_children <= 0) - goto out; - - /* Make sure its no smaller than the minimum */ - GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL); - - avail_size = MAX (width, min_width); - if (avail_size <= 0) - goto out; - - get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width); - if (nat_item_width <= 0) - goto out; - - /* By default flow at the natural item width */ - line_length = avail_size / (nat_item_width + priv->column_spacing); - - /* After the above aproximation, check if we cant fit one more on the line */ - if (line_length * priv->column_spacing + (line_length + 1) * nat_item_width <= avail_size) - line_length++; - - /* Its possible we were allocated just less than the natural width of the - * minimum item flow length - */ - line_length = MAX (min_items, line_length); - line_length = MIN (line_length, priv->max_children_per_line); - - /* Now we need the real item allocation size */ - item_size = (avail_size - (line_length - 1) * priv->column_spacing) / line_length; - - /* Cut out the expand space if we're not distributing any */ - if (gtk_widget_get_halign (widget) != GTK_ALIGN_FILL) - { - item_size = MIN (item_size, nat_item_width); - extra_pixels = 0; - } - else - /* Collect the extra pixels for expand children */ - extra_pixels = (avail_size - (line_length - 1) * priv->column_spacing) % line_length; - - if (priv->homogeneous) - { - gint min_item_height, nat_item_height; - gint lines; - - /* Here we just use the largest height-for-width and - * add up the size accordingly - */ - get_largest_size_for_opposing_orientation (box, - GTK_ORIENTATION_HORIZONTAL, - item_size, - &min_item_height, - &nat_item_height); - - /* Round up how many lines we need to allocate for */ - lines = n_children / line_length; - if ((n_children % line_length) > 0) - lines++; - - min_height = min_item_height * lines; - nat_height = nat_item_height * lines; - - min_height += (lines - 1) * priv->row_spacing; - nat_height += (lines - 1) * priv->row_spacing; - } - else - { - gint min_line_height, nat_line_height, i; - gboolean first_line = TRUE; - GtkRequestedSize *item_sizes; - GSequenceIter *iter; - - /* First get the size each set of items take to span the line - * when aligning the items above and below after flowping. - */ - item_sizes = fit_aligned_item_requests (box, - priv->orientation, - avail_size, - priv->column_spacing, - &line_length, - priv->max_children_per_line, - n_children); - - /* Get the available remaining size */ - avail_size -= (line_length - 1) * priv->column_spacing; - for (i = 0; i < line_length; i++) - avail_size -= item_sizes[i].minimum_size; - - if (avail_size > 0) - extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); - - for (iter = g_sequence_get_begin_iter (priv->children); - !g_sequence_iter_is_end (iter);) - { - iter = get_largest_size_for_line_in_opposing_orientation (box, - GTK_ORIENTATION_HORIZONTAL, - iter, - line_length, - item_sizes, - extra_pixels, - &min_line_height, - &nat_line_height); - /* Its possible the line only had invisible widgets */ - if (nat_line_height > 0) - { - if (first_line) - first_line = FALSE; - else - { - min_height += priv->row_spacing; - nat_height += priv->row_spacing; - } - - min_height += min_line_height; - nat_height += nat_line_height; - } - } - - g_free (item_sizes); - } - } - else /* GTK_ORIENTATION_VERTICAL */ - { - /* Return the minimum height */ - GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, &nat_height); - } - - out: - - if (minimum_height) - *minimum_height = min_height; - - if (natural_height) - *natural_height = nat_height; -} - -static void -gtk_flow_box_get_preferred_width_for_height (GtkWidget *widget, - gint height, - gint *minimum_width, - gint *natural_width) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gint min_item_height, nat_item_height; - gint min_items; - gint min_width, nat_width; - gint avail_size, n_children; - - min_items = MAX (1, priv->min_children_per_line); - - min_width = 0; - nat_width = 0; - - if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) - { - /* Return the minimum width */ - GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, &nat_width); - } - else /* GTK_ORIENTATION_VERTICAL */ - { - gint min_height; - gint line_length; - gint item_size, extra_pixels; - - n_children = get_visible_children (box); - if (n_children <= 0) - goto out; - - /* Make sure its no smaller than the minimum */ - GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL); - - avail_size = MAX (height, min_height); - if (avail_size <= 0) - goto out; - - get_max_item_size (box, GTK_ORIENTATION_VERTICAL, &min_item_height, &nat_item_height); - - /* By default flow at the natural item width */ - line_length = avail_size / (nat_item_height + priv->row_spacing); - - /* After the above aproximation, check if we cant fit one more on the line */ - if (line_length * priv->row_spacing + (line_length + 1) * nat_item_height <= avail_size) - line_length++; - - /* Its possible we were allocated just less than the natural width of the - * minimum item flow length - */ - line_length = MAX (min_items, line_length); - line_length = MIN (line_length, priv->max_children_per_line); - - /* Now we need the real item allocation size */ - item_size = (avail_size - (line_length - 1) * priv->row_spacing) / line_length; - - /* Cut out the expand space if we're not distributing any */ - if (gtk_widget_get_valign (widget) != GTK_ALIGN_FILL) - { - item_size = MIN (item_size, nat_item_height); - extra_pixels = 0; - } - else - /* Collect the extra pixels for expand children */ - extra_pixels = (avail_size - (line_length - 1) * priv->row_spacing) % line_length; - - if (priv->homogeneous) - { - gint min_item_width, nat_item_width; - gint lines; - - /* Here we just use the largest height-for-width and - * add up the size accordingly - */ - get_largest_size_for_opposing_orientation (box, - GTK_ORIENTATION_VERTICAL, - item_size, - &min_item_width, - &nat_item_width); - - /* Round up how many lines we need to allocate for */ - n_children = get_visible_children (box); - lines = n_children / line_length; - if ((n_children % line_length) > 0) - lines++; - - min_width = min_item_width * lines; - nat_width = nat_item_width * lines; - - min_width += (lines - 1) * priv->column_spacing; - nat_width += (lines - 1) * priv->column_spacing; - } - else - { - gint min_line_width, nat_line_width, i; - gboolean first_line = TRUE; - GtkRequestedSize *item_sizes; - GSequenceIter *iter; - - /* First get the size each set of items take to span the line - * when aligning the items above and below after flowping. - */ - item_sizes = fit_aligned_item_requests (box, - priv->orientation, - avail_size, - priv->row_spacing, - &line_length, - priv->max_children_per_line, - n_children); - - /* Get the available remaining size */ - avail_size -= (line_length - 1) * priv->column_spacing; - for (i = 0; i < line_length; i++) - avail_size -= item_sizes[i].minimum_size; - - if (avail_size > 0) - extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); - - for (iter = g_sequence_get_begin_iter (priv->children); - !g_sequence_iter_is_end (iter);) - { - iter = get_largest_size_for_line_in_opposing_orientation (box, - GTK_ORIENTATION_VERTICAL, - iter, - line_length, - item_sizes, - extra_pixels, - &min_line_width, - &nat_line_width); - - /* Its possible the last line only had invisible widgets */ - if (nat_line_width > 0) - { - if (first_line) - first_line = FALSE; - else - { - min_width += priv->column_spacing; - nat_width += priv->column_spacing; - } - - min_width += min_line_width; - nat_width += nat_line_width; - } - } - g_free (item_sizes); - } - } - - out: - if (minimum_width) - *minimum_width = min_width; - - if (natural_width) - *natural_width = nat_width; -} - -/* Drawing {{{3 */ - -static gboolean -gtk_flow_box_draw (GtkWidget *widget, - cairo_t *cr) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - GtkAllocation allocation = { 0, }; - GtkStyleContext* context; - - gtk_widget_get_allocation (GTK_WIDGET (box), &allocation); - context = gtk_widget_get_style_context (GTK_WIDGET (box)); - gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height); - - GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->draw (widget, cr); - - if (priv->rubberband_first && priv->rubberband_last) - { - GSequenceIter *iter, *iter1, *iter2; - GdkRectangle line_rect, rect; - GArray *lines; - gboolean vertical; - - vertical = priv->orientation == GTK_ORIENTATION_VERTICAL; - - cairo_save (cr); - - context = gtk_widget_get_style_context (widget); - gtk_style_context_save (context); - gtk_style_context_add_class (context, GTK_STYLE_CLASS_RUBBERBAND); - - iter1 = CHILD_PRIV (priv->rubberband_first)->iter; - iter2 = CHILD_PRIV (priv->rubberband_last)->iter; - - if (g_sequence_iter_compare (iter2, iter1) < 0) - { - iter = iter1; - iter1 = iter2; - iter2 = iter; - } - - line_rect.width = 0; - lines = g_array_new (FALSE, FALSE, sizeof (GdkRectangle)); - - for (iter = iter1; - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - GtkWidget *child; - - child = g_sequence_get (iter); - gtk_widget_get_allocation (GTK_WIDGET (child), &rect); - if (line_rect.width == 0) - line_rect = rect; - else - { - if ((vertical && rect.x == line_rect.x) || - (!vertical && rect.y == line_rect.y)) - gdk_rectangle_union (&rect, &line_rect, &line_rect); - else - { - g_array_append_val (lines, line_rect); - line_rect = rect; - } - } - - if (g_sequence_iter_compare (iter, iter2) == 0) - break; - } - - if (line_rect.width != 0) - g_array_append_val (lines, line_rect); - - if (lines->len > 0) - { - GtkStateFlags state; - cairo_path_t *path; - GtkBorder border; - GdkRGBA border_color; - - if (vertical) - path_from_vertical_line_rects (cr, (GdkRectangle *)lines->data, lines->len); - else - path_from_horizontal_line_rects (cr, (GdkRectangle *)lines->data, lines->len); - - /* For some reason we need to copy and reapply the path, - * or it gets eaten by gtk_render_background() - */ - path = cairo_copy_path (cr); - - cairo_save (cr); - cairo_clip (cr); - gtk_widget_get_allocation (widget, &allocation); - gtk_render_background (context, cr, - 0, 0, - allocation.width, allocation.height); - cairo_restore (cr); - - cairo_append_path (cr, path); - cairo_path_destroy (path); - - state = gtk_widget_get_state_flags (widget); - gtk_style_context_get_border_color (context, state, &border_color); - gtk_style_context_get_border (context, state, &border); - - cairo_set_line_width (cr, border.left); - gdk_cairo_set_source_rgba (cr, &border_color); - cairo_stroke (cr); - } - g_array_free (lines, TRUE); - - gtk_style_context_restore (context); - cairo_restore (cr); - } - - return TRUE; -} - -/* Autoscrolling {{{3 */ - -static void -remove_autoscroll (GtkFlowBox *box) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - if (priv->autoscroll_id) - { - gtk_widget_remove_tick_callback (GTK_WIDGET (box), priv->autoscroll_id); - priv->autoscroll_id = 0; - } - - priv->autoscroll_mode = GTK_SCROLL_NONE; -} - -static gboolean -autoscroll_cb (GtkWidget *widget, - GdkFrameClock *frame_clock, - gpointer data) -{ - GtkFlowBox *box = GTK_FLOW_BOX (data); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - GtkAdjustment *adjustment; - gdouble factor; - gdouble increment; - gdouble value; - - if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) - adjustment = priv->vadjustment; - else - adjustment = priv->hadjustment; - - switch (priv->autoscroll_mode) - { - case GTK_SCROLL_STEP_FORWARD: - factor = AUTOSCROLL_FACTOR; - break; - case GTK_SCROLL_STEP_BACKWARD: - factor = - AUTOSCROLL_FACTOR; - break; - case GTK_SCROLL_PAGE_FORWARD: - factor = AUTOSCROLL_FACTOR_FAST; - break; - case GTK_SCROLL_PAGE_BACKWARD: - factor = - AUTOSCROLL_FACTOR_FAST; - break; - default: - g_assert_not_reached (); - } - - increment = gtk_adjustment_get_step_increment (adjustment) / factor; - - value = gtk_adjustment_get_value (adjustment); - value += increment; - gtk_adjustment_set_value (adjustment, value); - - if (priv->rubberband_select) - { - gint x, y; - GtkFlowBoxChild *child; - - gdk_window_get_device_position (gtk_widget_get_window (widget), - priv->rubberband_device, - &x, &y, NULL); - - child = gtk_flow_box_find_child_at_pos (box, x, y); - - gtk_flow_box_update_prelight (box, child); - gtk_flow_box_update_active (box, child); - - if (child != NULL) - priv->rubberband_last = child; - } - - return G_SOURCE_CONTINUE; -} - -static void -add_autoscroll (GtkFlowBox *box) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - if (priv->autoscroll_id != 0 || - priv->autoscroll_mode == GTK_SCROLL_NONE) - return; - - priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (box), - (GtkTickCallback)autoscroll_cb, - box, - NULL); -} - -static gboolean -get_view_rect (GtkFlowBox *box, - GdkRectangle *rect) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - GtkWidget *parent; - GdkWindow *view; - - parent = gtk_widget_get_parent (GTK_WIDGET (box)); - if (GTK_IS_VIEWPORT (parent)) - { - view = gtk_viewport_get_view_window (GTK_VIEWPORT (parent)); - rect->x = gtk_adjustment_get_value (priv->hadjustment); - rect->y = gtk_adjustment_get_value (priv->vadjustment); - rect->width = gdk_window_get_width (view); - rect->height = gdk_window_get_height (view); - return TRUE; - } - - return FALSE; -} - -static void -update_autoscroll_mode (GtkFlowBox *box, - gint x, - gint y) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - GtkScrollType mode = GTK_SCROLL_NONE; - GdkRectangle rect; - gint size, pos; - - if (priv->rubberband_select && get_view_rect (box, &rect)) - { - if (priv->orientation == GTK_ORIENTATION_VERTICAL) - { - size = rect.width; - pos = x - rect.x; - } - else - { - size = rect.height; - pos = y - rect.y; - } - - if (pos < 0 - AUTOSCROLL_FAST_DISTANCE) - mode = GTK_SCROLL_PAGE_BACKWARD; - else if (pos > size + AUTOSCROLL_FAST_DISTANCE) - mode = GTK_SCROLL_PAGE_FORWARD; - else if (pos < 0) - mode = GTK_SCROLL_STEP_BACKWARD; - else if (pos > size) - mode = GTK_SCROLL_STEP_FORWARD; - } - - if (mode != priv->autoscroll_mode) - { - remove_autoscroll (box); - priv->autoscroll_mode = mode; - add_autoscroll (box); - } -} - -/* Event handling {{{3 */ - -static gboolean -gtk_flow_box_enter_notify_event (GtkWidget *widget, - GdkEventCrossing *event) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxChild *child; - - if (event->window != gtk_widget_get_window (GTK_WIDGET (box))) - return FALSE; - - child = gtk_flow_box_find_child_at_pos (box, event->x, event->y); - gtk_flow_box_update_prelight (box, child); - gtk_flow_box_update_active (box, child); - - return FALSE; -} - -static gboolean -gtk_flow_box_leave_notify_event (GtkWidget *widget, - GdkEventCrossing *event) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxChild *child = NULL; - - if (event->window != gtk_widget_get_window (GTK_WIDGET (box))) - return FALSE; - - if (event->detail != GDK_NOTIFY_INFERIOR) - child = NULL; - else - child = gtk_flow_box_find_child_at_pos (box, event->x, event->y); - - gtk_flow_box_update_prelight (box, child); - gtk_flow_box_update_active (box, child); - - return FALSE; -} - -static gboolean -gtk_flow_box_motion_notify_event (GtkWidget *widget, - GdkEventMotion *event) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - GtkFlowBoxChild *child; - GdkWindow *window; - GdkWindow *event_window; - gint relative_x; - gint relative_y; - gdouble parent_x; - gdouble parent_y; - - window = gtk_widget_get_window (GTK_WIDGET (box)); - event_window = event->window; - relative_x = event->x; - relative_y = event->y; - - while ((event_window != NULL) && (event_window != window)) - { - gdk_window_coords_to_parent (event_window, - relative_x, relative_y, - &parent_x, &parent_y); - relative_x = parent_x; - relative_y = parent_y; - event_window = gdk_window_get_effective_parent (event_window); - } - - child = gtk_flow_box_find_child_at_pos (box, relative_x, relative_y); - gtk_flow_box_update_prelight (box, child); - gtk_flow_box_update_active (box, child); - - if (priv->track_motion) - { - if (!priv->rubberband_select && - (event->x - priv->button_down_x) * (event->x - priv->button_down_x) + - (event->y - priv->button_down_y) * (event->y - priv->button_down_y) > RUBBERBAND_START_DISTANCE * RUBBERBAND_START_DISTANCE) - { - priv->rubberband_select = TRUE; - priv->rubberband_first = gtk_flow_box_find_child_at_pos (box, priv->button_down_x, priv->button_down_y); - - /* Grab focus here, so Escape-to-stop-rubberband works */ - gtk_flow_box_update_cursor (box, priv->rubberband_first); - } - - if (priv->rubberband_select) - { - if (priv->rubberband_first == NULL) - priv->rubberband_first = child; - if (child != NULL) - priv->rubberband_last = child; - - update_autoscroll_mode (box, event->x, event->y); - } - } - - return FALSE; -} - -static gboolean -gtk_flow_box_button_press_event (GtkWidget *widget, - GdkEventButton *event) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - GtkFlowBoxChild *child; - - if (event->button == GDK_BUTTON_PRIMARY) - { - child = gtk_flow_box_find_child_at_pos (box, event->x, event->y); - if (child != NULL) - { - priv->active_child = child; - priv->active_child_active = TRUE; - gtk_widget_queue_draw (GTK_WIDGET (box)); - if (event->type == GDK_2BUTTON_PRESS && - !priv->activate_on_single_click) - { - g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child); - return TRUE; - } - } - - if (priv->selection_mode == GTK_SELECTION_MULTIPLE) - { - priv->track_motion = TRUE; - priv->rubberband_select = FALSE; - priv->rubberband_first = NULL; - priv->rubberband_last = NULL; - priv->button_down_x = event->x; - priv->button_down_y = event->y; - priv->rubberband_device = gdk_event_get_device ((GdkEvent*)event); - get_current_selection_modifiers (widget, &priv->rubberband_modify, &priv->rubberband_extend); - } - } - - return FALSE; -} - -static void -gtk_flow_box_stop_rubberband (GtkFlowBox *box) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - priv->rubberband_select = FALSE; - priv->rubberband_first = NULL; - priv->rubberband_last = NULL; - priv->rubberband_device = NULL; - - remove_autoscroll (box); - - gtk_widget_queue_draw (GTK_WIDGET (box)); -} - -static gboolean -gtk_flow_box_button_release_event (GtkWidget *widget, - GdkEventButton *event) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - if (event->button == GDK_BUTTON_PRIMARY) - { - if (priv->active_child != NULL && priv->active_child_active) - { - if (priv->activate_on_single_click) - gtk_flow_box_select_and_activate (box, priv->active_child); - else - { - gboolean modify; - gboolean extend; - GdkDevice *device; - - get_current_selection_modifiers (widget, &modify, &extend); - - /* With touch, we default to modifying the selection. - * You can still clear the selection and start over - * by holding Ctrl. - */ - device = gdk_event_get_source_device ((GdkEvent *)event); - if (gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN) - modify = !modify; - - gtk_flow_box_update_selection (box, priv->active_child, modify, extend); - } - } - - priv->active_child = NULL; - priv->active_child_active = FALSE; - gtk_widget_queue_draw (GTK_WIDGET (box)); - } - - priv->track_motion = FALSE; - if (priv->rubberband_select) - { - if (!priv->rubberband_extend && !priv->rubberband_modify) - gtk_flow_box_unselect_all_internal (box); - gtk_flow_box_select_all_between (box, priv->rubberband_first, priv->rubberband_last, priv->rubberband_modify); - - gtk_flow_box_stop_rubberband (box); - - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); - - } - - return FALSE; -} - -static gboolean -gtk_flow_box_key_press_event (GtkWidget *widget, - GdkEventKey *event) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - if (priv->rubberband_select) - { - if (event->keyval == GDK_KEY_Escape) - { - gtk_flow_box_stop_rubberband (box); - return TRUE; - } - } - - return GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->key_press_event (widget, event); -} - -static void -gtk_flow_box_grab_notify (GtkWidget *widget, - gboolean was_grabbed) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - if (!was_grabbed) - { - if (priv->rubberband_select) - gtk_flow_box_stop_rubberband (box); - } -} - -/* Realize and map {{{3 */ - -static void -gtk_flow_box_realize (GtkWidget *widget) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkAllocation allocation; - GdkWindowAttr attributes = {0}; - GdkWindow *window; - - gtk_widget_get_allocation (GTK_WIDGET (box), &allocation); - gtk_widget_set_realized (GTK_WIDGET (box), TRUE); - - attributes.x = allocation.x; - attributes.y = allocation.y; - attributes.width = allocation.width; - attributes.height = allocation.height; - attributes.window_type = GDK_WINDOW_CHILD; - attributes.event_mask = gtk_widget_get_events (GTK_WIDGET (box)) - | GDK_ENTER_NOTIFY_MASK - | GDK_LEAVE_NOTIFY_MASK - | GDK_POINTER_MOTION_MASK - | GDK_EXPOSURE_MASK - | GDK_KEY_PRESS_MASK - | GDK_BUTTON_PRESS_MASK - | GDK_BUTTON_RELEASE_MASK; - attributes.wclass = GDK_INPUT_OUTPUT; - - window = gdk_window_new (gtk_widget_get_parent_window (GTK_WIDGET (box)), - &attributes, GDK_WA_X | GDK_WA_Y); - gtk_widget_register_window (GTK_WIDGET (box), window); - gtk_widget_set_window (GTK_WIDGET (box), window); - gtk_style_context_set_background (gtk_widget_get_style_context (GTK_WIDGET (box)), window); -} - -static void -gtk_flow_box_unmap (GtkWidget *widget) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - - remove_autoscroll (box); - - GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->unmap (widget); -} - -/* GtkContainer implementation {{{2 */ - -static void -gtk_flow_box_add (GtkContainer *container, - GtkWidget *child) -{ - gtk_flow_box_insert (GTK_FLOW_BOX (container), child, -1); -} - -static void -gtk_flow_box_remove (GtkContainer *container, - GtkWidget *widget) -{ - GtkFlowBox *box = GTK_FLOW_BOX (container); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gboolean was_visible; - gboolean was_selected; - GtkFlowBoxChild *child; - - if (GTK_IS_FLOW_BOX_CHILD (widget)) - child = GTK_FLOW_BOX_CHILD (widget); - else - { - child = (GtkFlowBoxChild*)gtk_widget_get_parent (widget); - if (!GTK_IS_FLOW_BOX_CHILD (child)) - { - g_warning ("Tried to remove non-child %p\n", widget); - return; - } - } - - was_visible = child_is_visible (GTK_WIDGET (child)); - was_selected = CHILD_PRIV (child)->selected; - - if (child == priv->prelight_child) - priv->prelight_child = NULL; - if (child == priv->active_child) - priv->active_child = NULL; - if (child == priv->selected_child) - priv->selected_child = NULL; - - gtk_widget_unparent (GTK_WIDGET (child)); - g_sequence_remove (CHILD_PRIV (child)->iter); - - if (was_visible && gtk_widget_get_visible (GTK_WIDGET (box))) - gtk_widget_queue_resize (GTK_WIDGET (box)); - - if (was_selected) - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); -} - -static void -gtk_flow_box_forall (GtkContainer *container, - gboolean include_internals, - GtkCallback callback, - gpointer callback_target) -{ - GSequenceIter *iter; - GtkWidget *child; - - iter = g_sequence_get_begin_iter (BOX_PRIV (container)->children); - while (!g_sequence_iter_is_end (iter)) - { - child = g_sequence_get (iter); - iter = g_sequence_iter_next (iter); - callback (child, callback_target); - } -} - -static GType -gtk_flow_box_child_type (GtkContainer *container) -{ - return GTK_TYPE_FLOW_BOX_CHILD; -} - -/* Keynav {{{2 */ - -static gboolean -gtk_flow_box_focus (GtkWidget *widget, - GtkDirectionType direction) -{ - GtkFlowBox *box = GTK_FLOW_BOX (widget); - GtkWidget *focus_child; - GSequenceIter *iter; - GtkFlowBoxChild *next_focus_child; - - focus_child = gtk_container_get_focus_child (GTK_CONTAINER (box)); - next_focus_child = NULL; - - if (focus_child != NULL) - { - if (gtk_widget_child_focus (focus_child, direction)) - return TRUE; - - iter = CHILD_PRIV (focus_child)->iter; - - if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD) - iter = gtk_flow_box_get_previous_focusable (box, iter); - else if (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD) - iter = gtk_flow_box_get_next_focusable (box, iter); - else if (direction == GTK_DIR_UP) - iter = gtk_flow_box_get_above_focusable (box, iter); - else if (direction == GTK_DIR_DOWN) - iter = gtk_flow_box_get_below_focusable (box, iter); - - if (iter != NULL) - next_focus_child = g_sequence_get (iter); - } - else - { - if (BOX_PRIV (box)->selected_child) - next_focus_child = BOX_PRIV (box)->selected_child; - else - { - if (direction == GTK_DIR_UP || direction == GTK_DIR_TAB_BACKWARD) - iter = gtk_flow_box_get_last_focusable (box); - else - iter = gtk_flow_box_get_first_focusable (box); - - if (iter != NULL) - next_focus_child = g_sequence_get (iter); - } - } - - if (next_focus_child == NULL) - { - if (direction == GTK_DIR_UP || direction == GTK_DIR_DOWN || - direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT) - { - if (gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) - return TRUE; - } - - return FALSE; - } - - if (gtk_widget_child_focus (GTK_WIDGET (next_focus_child), direction)) - return TRUE; - - return TRUE; -} - -static void -gtk_flow_box_add_move_binding (GtkBindingSet *binding_set, - guint keyval, - GdkModifierType modmask, - GtkMovementStep step, - gint count) -{ - GdkDisplay *display; - GdkModifierType extend_mod_mask = GDK_SHIFT_MASK; - GdkModifierType modify_mod_mask = GDK_CONTROL_MASK; - - display = gdk_display_get_default (); - if (display) - { - extend_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), - GDK_MODIFIER_INTENT_EXTEND_SELECTION); - modify_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), - GDK_MODIFIER_INTENT_MODIFY_SELECTION); - } - - gtk_binding_entry_add_signal (binding_set, keyval, modmask, - "move-cursor", 2, - GTK_TYPE_MOVEMENT_STEP, step, - G_TYPE_INT, count, - NULL); - gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask, - "move-cursor", 2, - GTK_TYPE_MOVEMENT_STEP, step, - G_TYPE_INT, count, - NULL); - gtk_binding_entry_add_signal (binding_set, keyval, modmask | modify_mod_mask, - "move-cursor", 2, - GTK_TYPE_MOVEMENT_STEP, step, - G_TYPE_INT, count, - NULL); - gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask | modify_mod_mask, - "move-cursor", 2, - GTK_TYPE_MOVEMENT_STEP, step, - G_TYPE_INT, count, - NULL); -} - -static void -gtk_flow_box_activate_cursor_child (GtkFlowBox *box) -{ - gtk_flow_box_select_and_activate (box, BOX_PRIV (box)->cursor_child); -} - -static void -gtk_flow_box_toggle_cursor_child (GtkFlowBox *box) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - if (priv->cursor_child == NULL) - return; - - if ((priv->selection_mode == GTK_SELECTION_SINGLE || - priv->selection_mode == GTK_SELECTION_MULTIPLE) && - CHILD_PRIV (priv->cursor_child)->selected) - gtk_flow_box_unselect_child_internal (box, priv->cursor_child); - else - gtk_flow_box_select_and_activate (box, priv->cursor_child); -} - -static void -gtk_flow_box_move_cursor (GtkFlowBox *box, - GtkMovementStep step, - gint count) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - gboolean modify; - gboolean extend; - GtkFlowBoxChild *child; - GtkFlowBoxChild *prev; - GtkFlowBoxChild *next; - GtkAllocation allocation; - gint page_size; - GSequenceIter *iter; - gint start; - GtkAdjustment *adjustment; - gboolean vertical; - - vertical = priv->orientation == GTK_ORIENTATION_VERTICAL; - - if (vertical) - { - switch (step) - { - case GTK_MOVEMENT_VISUAL_POSITIONS: - step = GTK_MOVEMENT_DISPLAY_LINES; - break; - case GTK_MOVEMENT_DISPLAY_LINES: - step = GTK_MOVEMENT_VISUAL_POSITIONS; - break; - default: ; - } - } - - child = NULL; - switch (step) - { - case GTK_MOVEMENT_VISUAL_POSITIONS: - if (priv->cursor_child != NULL) - { - iter = CHILD_PRIV (priv->cursor_child)->iter; - if (gtk_widget_get_direction (GTK_WIDGET (box)) == GTK_TEXT_DIR_RTL) - count = - count; - - while (count < 0 && iter != NULL) - { - iter = gtk_flow_box_get_previous_focusable (box, iter); - count = count + 1; - } - while (count > 0 && iter != NULL) - { - iter = gtk_flow_box_get_next_focusable (box, iter); - count = count - 1; - } - - if (iter != NULL && !g_sequence_iter_is_end (iter)) - child = g_sequence_get (iter); - } - break; - - case GTK_MOVEMENT_BUFFER_ENDS: - if (count < 0) - iter = gtk_flow_box_get_first_focusable (box); - else - iter = gtk_flow_box_get_last_focusable (box); - if (iter != NULL) - child = g_sequence_get (iter); - break; - - case GTK_MOVEMENT_DISPLAY_LINES: - if (priv->cursor_child != NULL) - { - iter = CHILD_PRIV (priv->cursor_child)->iter; - - while (count < 0 && iter != NULL) - { - iter = gtk_flow_box_get_above_focusable (box, iter); - count = count + 1; - } - while (count > 0 && iter != NULL) - { - iter = gtk_flow_box_get_below_focusable (box, iter); - count = count - 1; - } - - if (iter != NULL) - child = g_sequence_get (iter); - } - break; - - case GTK_MOVEMENT_PAGES: - page_size = 100; - adjustment = vertical ? priv->hadjustment : priv->vadjustment; - if (adjustment) - page_size = gtk_adjustment_get_page_increment (adjustment); - - if (priv->cursor_child != NULL) - { - child = priv->cursor_child; - iter = CHILD_PRIV (child)->iter; - gtk_widget_get_allocation (GTK_WIDGET (child), &allocation); - start = vertical ? allocation.x : allocation.y; - - if (count < 0) - { - gint i = 0; - - /* Up */ - while (iter != NULL) - { - iter = gtk_flow_box_get_previous_focusable (box, iter); - if (iter == NULL) - break; - - prev = g_sequence_get (iter); - - /* go up an even number of rows */ - if (i % priv->cur_children_per_line == 0) - { - gtk_widget_get_allocation (GTK_WIDGET (prev), &allocation); - if ((vertical ? allocation.x : allocation.y) < start - page_size) - break; - } - - child = prev; - i++; - } - } - else - { - gint i = 0; - - /* Down */ - while (!g_sequence_iter_is_end (iter)) - { - iter = gtk_flow_box_get_next_focusable (box, iter); - if (g_sequence_iter_is_end (iter)) - break; - - next = g_sequence_get (iter); - - if (i % priv->cur_children_per_line == 0) - { - gtk_widget_get_allocation (GTK_WIDGET (next), &allocation); - if ((vertical ? allocation.x : allocation.y) > start + page_size) - break; - } - - child = next; - i++; - } - } - gtk_widget_get_allocation (GTK_WIDGET (child), &allocation); - } - break; - - default: - g_assert_not_reached (); - } - - if (child == NULL || child == priv->cursor_child) - { - GtkDirectionType direction = count < 0 ? GTK_DIR_UP : GTK_DIR_DOWN; - - if (!gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) - { - GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (box)); - - if (toplevel) - gtk_widget_child_focus (toplevel, - direction == GTK_DIR_UP ? - GTK_DIR_TAB_BACKWARD : - GTK_DIR_TAB_FORWARD); - - } - - return; - } - - get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend); - - gtk_flow_box_update_cursor (box, child); - if (!modify) - gtk_flow_box_update_selection (box, child, FALSE, extend); -} - -/* Selection {{{2 */ - -static void -gtk_flow_box_selected_children_changed (GtkFlowBox *box) -{ -} - -/* GObject implementation {{{2 */ - -static void -gtk_flow_box_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) -{ - GtkFlowBox *box = GTK_FLOW_BOX (object); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - switch (prop_id) - { - case PROP_ORIENTATION: - g_value_set_enum (value, priv->orientation); - break; - case PROP_HOMOGENEOUS: - g_value_set_boolean (value, priv->homogeneous); - break; - case PROP_COLUMN_SPACING: - g_value_set_uint (value, priv->column_spacing); - break; - case PROP_ROW_SPACING: - g_value_set_uint (value, priv->row_spacing); - break; - case PROP_MIN_CHILDREN_PER_LINE: - g_value_set_uint (value, priv->min_children_per_line); - break; - case PROP_MAX_CHILDREN_PER_LINE: - g_value_set_uint (value, priv->max_children_per_line); - break; - case PROP_SELECTION_MODE: - g_value_set_enum (value, priv->selection_mode); - break; - case PROP_ACTIVATE_ON_SINGLE_CLICK: - g_value_set_boolean (value, priv->activate_on_single_click); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gtk_flow_box_set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) -{ - GtkFlowBox *box = GTK_FLOW_BOX (object); - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - switch (prop_id) - { - case PROP_ORIENTATION: - priv->orientation = g_value_get_enum (value); - /* Re-box the children in the new orientation */ - gtk_widget_queue_resize (GTK_WIDGET (box)); - break; - case PROP_HOMOGENEOUS: - gtk_flow_box_set_homogeneous (box, g_value_get_boolean (value)); - break; - case PROP_COLUMN_SPACING: - gtk_flow_box_set_column_spacing (box, g_value_get_uint (value)); - break; - case PROP_ROW_SPACING: - gtk_flow_box_set_row_spacing (box, g_value_get_uint (value)); - break; - case PROP_MIN_CHILDREN_PER_LINE: - gtk_flow_box_set_min_children_per_line (box, g_value_get_uint (value)); - break; - case PROP_MAX_CHILDREN_PER_LINE: - gtk_flow_box_set_max_children_per_line (box, g_value_get_uint (value)); - break; - case PROP_SELECTION_MODE: - gtk_flow_box_set_selection_mode (box, g_value_get_enum (value)); - break; - case PROP_ACTIVATE_ON_SINGLE_CLICK: - gtk_flow_box_set_activate_on_single_click (box, g_value_get_boolean (value)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -static void -gtk_flow_box_finalize (GObject *obj) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (obj); - - if (priv->filter_destroy != NULL) - priv->filter_destroy (priv->filter_data); - if (priv->sort_destroy != NULL) - priv->sort_destroy (priv->sort_data); - - g_sequence_free (priv->children); - g_clear_object (&priv->hadjustment); - g_clear_object (&priv->vadjustment); - - G_OBJECT_CLASS (gtk_flow_box_parent_class)->finalize (obj); -} - -static void -gtk_flow_box_class_init (GtkFlowBoxClass *class) -{ - GObjectClass *object_class = G_OBJECT_CLASS (class); - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); - GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class); - GtkBindingSet *binding_set; - - object_class->finalize = gtk_flow_box_finalize; - object_class->get_property = gtk_flow_box_get_property; - object_class->set_property = gtk_flow_box_set_property; - - widget_class->enter_notify_event = gtk_flow_box_enter_notify_event; - widget_class->leave_notify_event = gtk_flow_box_leave_notify_event; - widget_class->motion_notify_event = gtk_flow_box_motion_notify_event; - widget_class->size_allocate = gtk_flow_box_size_allocate; - widget_class->realize = gtk_flow_box_realize; - widget_class->unmap = gtk_flow_box_unmap; - widget_class->focus = gtk_flow_box_focus; - widget_class->draw = gtk_flow_box_draw; - widget_class->button_press_event = gtk_flow_box_button_press_event; - widget_class->button_release_event = gtk_flow_box_button_release_event; - widget_class->key_press_event = gtk_flow_box_key_press_event; - widget_class->grab_notify = gtk_flow_box_grab_notify; - widget_class->get_request_mode = gtk_flow_box_get_request_mode; - widget_class->get_preferred_width = gtk_flow_box_get_preferred_width; - widget_class->get_preferred_height = gtk_flow_box_get_preferred_height; - widget_class->get_preferred_height_for_width = gtk_flow_box_get_preferred_height_for_width; - widget_class->get_preferred_width_for_height = gtk_flow_box_get_preferred_width_for_height; - - container_class->add = gtk_flow_box_add; - container_class->remove = gtk_flow_box_remove; - container_class->forall = gtk_flow_box_forall; - container_class->child_type = gtk_flow_box_child_type; - gtk_container_class_handle_border_width (container_class); - - class->activate_cursor_child = gtk_flow_box_activate_cursor_child; - class->toggle_cursor_child = gtk_flow_box_toggle_cursor_child; - class->move_cursor = gtk_flow_box_move_cursor; - class->select_all = gtk_flow_box_select_all; - class->unselect_all = gtk_flow_box_unselect_all; - class->selected_children_changed = gtk_flow_box_selected_children_changed; - - g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation"); - - /** - * GtkFlowBox:selection-mode: - * - * The selection mode used by the flow box. - */ - g_object_class_install_property (object_class, - PROP_SELECTION_MODE, - g_param_spec_enum ("selection-mode", - "Selection mode", - "The selection mode", - GTK_TYPE_SELECTION_MODE, - GTK_SELECTION_SINGLE, - G_PARAM_READWRITE)); - - /** - * GtkFlowBox:activate-on-single-click: - * - * Determines whether children can be activated with a single - * click, or require a double-click. - */ - g_object_class_install_property (object_class, - PROP_ACTIVATE_ON_SINGLE_CLICK, - g_param_spec_boolean ("activate-on-single-click", - "Activate on Single Click", - "Activate row on a single click", - TRUE, - G_PARAM_READWRITE)); - - /** - * GtkFlowBox:homogeneous: - * - * Determines whether all children should be allocated the - * same size. - */ - g_object_class_install_property (object_class, - PROP_HOMOGENEOUS, - g_param_spec_boolean ("homogeneous", - "Homogeneous", - "Whether the children should all be the same size", - FALSE, - G_PARAM_READWRITE)); - - /** - * GtkFlowBox:min-children-per-line: - * - * The minimum number of children to allocate consecutively - * in the given orientation. - * - * Setting the minimum children per line ensures - * that a reasonably small height will be requested - * for the overall minimum width of the box. - */ - g_object_class_install_property (object_class, - PROP_MIN_CHILDREN_PER_LINE, - g_param_spec_uint ("min-children-per-line", - "Minimum Children Per Line", - "The minimum number of children to allocate " - "consecutively in the given orientation.", - 0, - G_MAXUINT, - 0, - G_PARAM_READWRITE)); - - /** - * GtkFlowBox:max-children-per-line: - * - * The maximum amount of children to request space for consecutively - * in the given orientation. - */ - g_object_class_install_property (object_class, - PROP_MAX_CHILDREN_PER_LINE, - g_param_spec_uint ("max-children-per-line", - "Maximum Children Per Line", - "The maximum amount of children to request space for " - "consecutively in the given orientation.", - 0, - G_MAXUINT, - DEFAULT_MAX_CHILDREN_PER_LINE, - G_PARAM_READWRITE)); - - /** - * GtkFlowBox:row-spacing: - * - * The amount of vertical space between two children. - */ - g_object_class_install_property (object_class, - PROP_ROW_SPACING, - g_param_spec_uint ("row-spacing", - "Vertical spacing", - "The amount of vertical space between two children", - 0, - G_MAXUINT, - 0, - G_PARAM_READWRITE)); - - /** - * GtkFlowBox:column-spacing: - * - * The amount of horizontal space between two children. - */ - g_object_class_install_property (object_class, - PROP_COLUMN_SPACING, - g_param_spec_uint ("column-spacing", - "Horizontal spacing", - "The amount of horizontal space between two children", - 0, - G_MAXUINT, - 0, - G_PARAM_READWRITE)); - - /** - * GtkFlowBox::child-activated: - * @box: the #GtkFlowBox on which the signal is emitted - * @child: the child that is activated - * - * The ::child-activated signal is emitted when a child has been - * activated by the user. - */ - signals[CHILD_ACTIVATED] = g_signal_new ("child-activated", - GTK_TYPE_FLOW_BOX, - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (GtkFlowBoxClass, child_activated), - NULL, NULL, - g_cclosure_marshal_VOID__OBJECT, - G_TYPE_NONE, 1, - GTK_TYPE_FLOW_BOX_CHILD); - - /** - * GtkFlowBox::selected-children-changed: - * @box: the #GtkFlowBox on wich the signal is emitted - * - * The ::selected-children-changed signal is emitted when the - * set of selected children changes. - * - * Use gtk_flow_box_selected_foreach() or - * gtk_flow_box_get_selected_children() to obtain the - * selected children. - */ - signals[SELECTED_CHILDREN_CHANGED] = g_signal_new ("selected-children-changed", - GTK_TYPE_FLOW_BOX, - G_SIGNAL_RUN_FIRST, - G_STRUCT_OFFSET (GtkFlowBoxClass, selected_children_changed), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - /** - * GtkFlowBox::activate-cursor-child: - * @box: the #GtkFlowBox on which the signal is emitted - * - * The ::activate-cursor-child signal is a - * [keybinding signal][GtkBindingSignal] - * which gets emitted when the user activates the @box. - */ - signals[ACTIVATE_CURSOR_CHILD] = g_signal_new ("activate-cursor-child", - GTK_TYPE_FLOW_BOX, - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (GtkFlowBoxClass, activate_cursor_child), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - /** - * GtkFlowBox::toggle-cursor-child: - * @box: the #GtkFlowBox on which the signal is emitted - * - * The ::toggle-cursor-child signal is a - * [keybinding signal][GtkBindingSignal] - * which toggles the selection of the child that has the focus. - * - * The default binding for this signal is Ctrl-Space. - */ - signals[TOGGLE_CURSOR_CHILD] = g_signal_new ("toggle-cursor-child", - GTK_TYPE_FLOW_BOX, - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (GtkFlowBoxClass, toggle_cursor_child), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - /** - * GtkFlowBox::move-cursor: - * @box: the #GtkFlowBox on which the signal is emitted - * @step: the granularity fo the move, as a #GtkMovementStep - * @count: the number of @step units to move - * - * The ::move-cursor signal is a - * [keybinding signal][GtkBindingSignal] - * which gets emitted when the user initiates a cursor movement. - * If the cursor is not visible in @text_view, this signal causes - * the viewport to be moved instead. - * - * Applications should not connect to it, but may emit it with - * g_signal_emit_by_name() if they need to control the cursor - * programmatically. - * - * The default bindings for this signal come in two variants, - * the variant with the Shift modifier extends the selection, - * the variant without the Shift modifer does not. - * There are too many key combinations to list them all here. - * - Arrow keys move by individual children - * - Home/End keys move to the ends of the box - * - PageUp/PageDown keys move vertically by pages - */ - signals[MOVE_CURSOR] = g_signal_new ("move-cursor", - GTK_TYPE_FLOW_BOX, - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (GtkFlowBoxClass, move_cursor), - NULL, NULL, - NULL, - G_TYPE_NONE, 2, - GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT); - /** - * GtkFlowBox::select-all: - * @box: the #GtkFlowBox on which the signal is emitted - * - * The ::select-all signal is a - * [keybinding signal][GtkBindingSignal] - * which gets emitted to select all children of the box, if - * the selection mode permits it. - * - * The default bindings for this signal is Ctrl-a. - */ - signals[SELECT_ALL] = g_signal_new ("select-all", - GTK_TYPE_FLOW_BOX, - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (GtkFlowBoxClass, select_all), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - /** - * GtkFlowBox::unselect-all: - * @box: the #GtkFlowBox on which the signal is emitted - * - * The ::unselect-all signal is a - * [keybinding signal][GtkBindingSignal] - * which gets emitted to unselect all children of the box, if - * the selection mode permits it. - * - * The default bindings for this signal is Ctrl-Shift-a. - */ - signals[UNSELECT_ALL] = g_signal_new ("unselect-all", - GTK_TYPE_FLOW_BOX, - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (GtkFlowBoxClass, unselect_all), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); - - widget_class->activate_signal = signals[ACTIVATE_CURSOR_CHILD]; - - binding_set = gtk_binding_set_by_class (class); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Home, 0, - GTK_MOVEMENT_BUFFER_ENDS, -1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Home, 0, - GTK_MOVEMENT_BUFFER_ENDS, -1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_End, 0, - GTK_MOVEMENT_BUFFER_ENDS, 1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_End, 0, - GTK_MOVEMENT_BUFFER_ENDS, 1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Up, 0, - GTK_MOVEMENT_DISPLAY_LINES, -1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Up, 0, - GTK_MOVEMENT_DISPLAY_LINES, -1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Down, 0, - GTK_MOVEMENT_DISPLAY_LINES, 1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Down, 0, - GTK_MOVEMENT_DISPLAY_LINES, 1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Up, 0, - GTK_MOVEMENT_PAGES, -1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Up, 0, - GTK_MOVEMENT_PAGES, -1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Down, 0, - GTK_MOVEMENT_PAGES, 1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Down, 0, - GTK_MOVEMENT_PAGES, 1); - - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Right, 0, - GTK_MOVEMENT_VISUAL_POSITIONS, 1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Right, 0, - GTK_MOVEMENT_VISUAL_POSITIONS, 1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Left, 0, - GTK_MOVEMENT_VISUAL_POSITIONS, -1); - gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Left, 0, - GTK_MOVEMENT_VISUAL_POSITIONS, -1); - - gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, GDK_CONTROL_MASK, - "toggle-cursor-child", 0, NULL); - gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space, GDK_CONTROL_MASK, - "toggle-cursor-child", 0, NULL); - - gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK, - "select-all", 0); - gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK, - "unselect-all", 0); -} - -static void -gtk_flow_box_init (GtkFlowBox *box) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - gtk_widget_set_has_window (GTK_WIDGET (box), TRUE); - gtk_widget_set_redraw_on_allocate (GTK_WIDGET (box), TRUE); - - priv->orientation = GTK_ORIENTATION_HORIZONTAL; - priv->selection_mode = GTK_SELECTION_SINGLE; - priv->max_children_per_line = DEFAULT_MAX_CHILDREN_PER_LINE; - priv->column_spacing = 0; - priv->row_spacing = 0; - priv->activate_on_single_click = TRUE; - - priv->children = g_sequence_new (NULL); -} - - /* Public API {{{2 */ - -/** - * gtk_flow_box_new: - * - * Creates a GtkFlowBox. - * - * Returns: a new #GtkFlowBox container - * - * Since: 3.12 - */ -GtkWidget * -gtk_flow_box_new (void) -{ - return (GtkWidget *)g_object_new (GTK_TYPE_FLOW_BOX, NULL); -} - -/** - * gtk_flow_box_insert: - * @box: a #GtkFlowBox - * @widget: the #GtkWidget to add - * @position: the position to insert @child in - * - * Inserts the @widget into @box at @position. - * - * If a sort function is set, the widget will actually be inserted - * at the calculated position and this function has the same effect - * as gtk_container_add(). - * - * If @position is -1, or larger than the total number of children - * in the @box, then the @widget will be appended to the end. - * - * Since: 3.12 - */ -void -gtk_flow_box_insert (GtkFlowBox *box, - GtkWidget *widget, - gint position) -{ - GtkFlowBoxPrivate *priv; - GtkFlowBoxChild *child; - GSequenceIter *iter; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - g_return_if_fail (GTK_IS_WIDGET (widget)); - - priv = BOX_PRIV (box); - - if (GTK_IS_FLOW_BOX_CHILD (widget)) - child = GTK_FLOW_BOX_CHILD (widget); - else - { - child = GTK_FLOW_BOX_CHILD (gtk_flow_box_child_new ()); - gtk_widget_show (GTK_WIDGET (child)); - gtk_container_add (GTK_CONTAINER (child), widget); - } - - if (priv->sort_func != NULL) - iter = g_sequence_insert_sorted (priv->children, child, - (GCompareDataFunc)gtk_flow_box_sort, box); - else if (position == 0) - iter = g_sequence_prepend (priv->children, child); - else if (position == -1) - iter = g_sequence_append (priv->children, child); - else - { - GSequenceIter *pos; - pos = g_sequence_get_iter_at_pos (priv->children, position); - iter = g_sequence_insert_before (pos, child); - } - - CHILD_PRIV (child)->iter = iter; - gtk_widget_set_parent (GTK_WIDGET (child), GTK_WIDGET (box)); - gtk_flow_box_apply_filter (box, child); -} - -/** - * gtk_flow_box_get_child_at_index: - * @box: a #GtkFlowBox - * @idx: the position of the child - * - * Gets the nth child in the @box. - * - * Returns: (transfer none): the child widget, which will - * always be a #GtkFlowBoxChild - * - * Since: 3.12 - */ -GtkFlowBoxChild * -gtk_flow_box_get_child_at_index (GtkFlowBox *box, - gint idx) -{ - GSequenceIter *iter; - - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL); - - iter = g_sequence_get_iter_at_pos (BOX_PRIV (box)->children, idx); - if (iter) - return g_sequence_get (iter); - - return NULL; -} - -/** - * gtk_flow_box_set_hadjustment: - * @box: a #GtkFlowBox - * @adjustment: an adjustment which should be adjusted - * when the focus is moved among the descendents of @container - * - * Hooks up an adjustment to focus handling in @box. - * The adjustment is also used for autoscrolling during - * rubberband selection. See gtk_scrolled_window_get_hadjustment() - * for a typical way of obtaining the adjustment, and - * gtk_flow_box_set_vadjustment()for setting the vertical - * adjustment. - * - * The adjustments have to be in pixel units and in the same - * coordinate system as the allocation for immediate children - * of the box. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_hadjustment (GtkFlowBox *box, - GtkAdjustment *adjustment) -{ - GtkFlowBoxPrivate *priv; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); - - priv = BOX_PRIV (box); - - g_object_ref (adjustment); - if (priv->hadjustment) - g_object_unref (priv->hadjustment); - priv->hadjustment = adjustment; - gtk_container_set_focus_hadjustment (GTK_CONTAINER (box), adjustment); -} - -/** - * gtk_flow_box_set_vadjustment: - * @box: a #GtkFlowBox - * @adjustment: an adjustment which should be adjusted - * when the focus is moved among the descendents of @container - * - * Hooks up an adjustment to focus handling in @box. - * The adjustment is also used for autoscrolling during - * rubberband selection. See gtk_scrolled_window_get_vadjustment() - * for a typical way of obtaining the adjustment, and - * gtk_flow_box_set_hadjustment()for setting the horizontal - * adjustment. - * - * The adjustments have to be in pixel units and in the same - * coordinate system as the allocation for immediate children - * of the box. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_vadjustment (GtkFlowBox *box, - GtkAdjustment *adjustment) -{ - GtkFlowBoxPrivate *priv; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); - - priv = BOX_PRIV (box); - - g_object_ref (adjustment); - if (priv->vadjustment) - g_object_unref (priv->vadjustment); - priv->vadjustment = adjustment; - gtk_container_set_focus_vadjustment (GTK_CONTAINER (box), adjustment); -} - -/* Setters and getters {{{2 */ - -/** - * gtk_flow_box_get_homogeneous: - * @box: a #GtkFlowBox - * - * Returns whether the box is homogeneous (all children are the - * same size). See gtk_box_set_homogeneous(). - * - * Returns: %TRUE if the box is homogeneous. - * - * Since: 3.12 - */ -gboolean -gtk_flow_box_get_homogeneous (GtkFlowBox *box) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); - - return BOX_PRIV (box)->homogeneous; -} - -/** - * gtk_flow_box_set_homogeneous: - * @box: a #GtkFlowBox - * @homogeneous: %TRUE to create equal allotments, - * %FALSE for variable allotments - * - * Sets the #GtkFlowBox:homogeneous property of @box, controlling - * whether or not all children of @box are given equal space - * in the box. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_homogeneous (GtkFlowBox *box, - gboolean homogeneous) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - homogeneous = homogeneous != FALSE; - - if (BOX_PRIV (box)->homogeneous != homogeneous) - { - BOX_PRIV (box)->homogeneous = homogeneous; - - g_object_notify (G_OBJECT (box), "homogeneous"); - gtk_widget_queue_resize (GTK_WIDGET (box)); - } -} - -/** - * gtk_flow_box_set_row_spacing: - * @box: a #GtkFlowBox - * @spacing: the spacing to use - * - * Sets the vertical space to add between children. - * See the #GtkFlowBox:row-spacing property. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_row_spacing (GtkFlowBox *box, - guint spacing) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (BOX_PRIV (box)->row_spacing != spacing) - { - BOX_PRIV (box)->row_spacing = spacing; - - gtk_widget_queue_resize (GTK_WIDGET (box)); - g_object_notify (G_OBJECT (box), "row-spacing"); - } -} - -/** - * gtk_flow_box_get_row_spacing: - * @box: a #GtkFlowBox - * - * Gets the vertical spacing. - * - * Returns: the vertical spacing - * - * Since: 3.12 - */ -guint -gtk_flow_box_get_row_spacing (GtkFlowBox *box) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); - - return BOX_PRIV (box)->row_spacing; -} - -/** - * gtk_flow_box_set_column_spacing: - * @box: a #GtkFlowBox - * @spacing: the spacing to use - * - * Sets the horizontal space to add between children. - * See the #GtkFlowBox:column-spacing property. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_column_spacing (GtkFlowBox *box, - guint spacing) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (BOX_PRIV (box)->column_spacing != spacing) - { - BOX_PRIV (box)->column_spacing = spacing; - - gtk_widget_queue_resize (GTK_WIDGET (box)); - g_object_notify (G_OBJECT (box), "column-spacing"); - } -} - -/** - * gtk_flow_box_get_column_spacing: - * @box: a #GtkFlowBox - * - * Gets the horizontal spacing. - * - * Returns: the horizontal spacing - * - * Since: 3.12 - */ -guint -gtk_flow_box_get_column_spacing (GtkFlowBox *box) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); - - return BOX_PRIV (box)->column_spacing; -} - -/** - * gtk_flow_box_set_min_children_per_line: - * @box: a #GtkFlowBox - * @n_children: the minimum number of children per line - * - * Sets the minimum number of children to line up - * in @box’s orientation before flowing. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_min_children_per_line (GtkFlowBox *box, - guint n_children) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (BOX_PRIV (box)->min_children_per_line != n_children) - { - BOX_PRIV (box)->min_children_per_line = n_children; - - gtk_widget_queue_resize (GTK_WIDGET (box)); - g_object_notify (G_OBJECT (box), "min-children-per-line"); - } -} - -/** - * gtk_flow_box_get_min_children_per_line: - * @box: a #GtkFlowBox - * - * Gets the minimum number of children per line. - * - * Returns: the minimum number of children per line - * - * Since: 3.12 - */ -guint -gtk_flow_box_get_min_children_per_line (GtkFlowBox *box) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); - - return BOX_PRIV (box)->min_children_per_line; -} - -/** - * gtk_flow_box_set_max_children_per_line: - * @box: a #GtkFlowBox - * @n_children: the maximum number of children per line - * - * Sets the maximum number of children to request and - * allocate space for in @box’s orientation. - * - * Setting the maximum number of children per line - * limits the overall natural size request to be no more - * than @n_children children long in the given orientation. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_max_children_per_line (GtkFlowBox *box, - guint n_children) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (BOX_PRIV (box)->max_children_per_line != n_children) - { - BOX_PRIV (box)->max_children_per_line = n_children; - - gtk_widget_queue_resize (GTK_WIDGET (box)); - g_object_notify (G_OBJECT (box), "max-children-per-line"); - } -} - -/** - * gtk_flow_box_get_max_children_per_line: - * @box: a #GtkFlowBox - * - * Gets the maximum number of children per line. - * - * Returns: the maximum number of children per line - * - * Since: 3.12 - */ -guint -gtk_flow_box_get_max_children_per_line (GtkFlowBox *box) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); - - return BOX_PRIV (box)->max_children_per_line; -} - -/** - * gtk_flow_box_set_activate_on_single_click: - * @box: a #GtkFlowBox - * @single: %TRUE to emit child-activated on a single click - * - * If @single is %TRUE, children will be activated when you click - * on them, otherwise you need to double-click. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box, - gboolean single) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - single = single != FALSE; - - if (BOX_PRIV (box)->activate_on_single_click != single) - { - BOX_PRIV (box)->activate_on_single_click = single; - g_object_notify (G_OBJECT (box), "activate-on-single-click"); - } -} - -/** - * gtk_flow_box_get_activate_on_single_click: - * @box: a #GtkFlowBox - * - * Returns whether children activate on single clicks. - * - * Returns: %TRUE if children are activated on single click, - * %FALSE otherwise - * - * Since: 3.12 - */ -gboolean -gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); - - return BOX_PRIV (box)->activate_on_single_click; -} - - /* Selection handling {{{2 */ - -/** - * gtk_flow_box_get_selected_children: - * @box: a #GtkFlowBox - * - * Creates a list of all selected children. - * - * Returns: (element-type GtkFlowBoxChild) (transfer container): - * A #GList containing the #GtkWidget for each selected child. - * Free with g_list_free() when done. - * - * Since: 3.12 - */ -GList * -gtk_flow_box_get_selected_children (GtkFlowBox *box) -{ - GtkFlowBoxChild *child; - GSequenceIter *iter; - GList *selected = NULL; - - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL); - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - child = g_sequence_get (iter); - if (CHILD_PRIV (child)->selected) - selected = g_list_prepend (selected, child); - } - - return g_list_reverse (selected); -} - -/** - * gtk_flow_box_select_child: - * @box: a #GtkFlowBox - * @child: a child of @box - * - * Selects a single child of @box, if the selection - * mode allows it. - * - * Since: 3.12 - */ -void -gtk_flow_box_select_child (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); - - gtk_flow_box_select_child_internal (box, child); -} - -/** - * gtk_flow_box_unselect_child: - * @box: a #GtkFlowBox - * @child: a child of @box - * - * Unselects a single child of @box, if the selection - * mode allows it. - * - * Since: 3.12 - */ -void -gtk_flow_box_unselect_child (GtkFlowBox *box, - GtkFlowBoxChild *child) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); - - gtk_flow_box_unselect_child_internal (box, child); -} - -/** - * gtk_flow_box_select_all: - * @box: a #GtkFlowBox - * - * Select all children of @box, if the selection - * mode allows it. - * - * Since: 3.12 - */ -void -gtk_flow_box_select_all (GtkFlowBox *box) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) - return; - - if (g_sequence_get_length (BOX_PRIV (box)->children) > 0) - { - gtk_flow_box_select_all_between (box, NULL, NULL, FALSE); - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); - } -} - -/** - * gtk_flow_box_unselect_all: - * @box: a #GtkFlowBox - * - * Unselect all children of @box, if the selection - * mode allows it. - * - * Since: 3.12 - */ -void -gtk_flow_box_unselect_all (GtkFlowBox *box) -{ - gboolean dirty = FALSE; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_BROWSE) - return; - - dirty = gtk_flow_box_unselect_all_internal (box); - - if (dirty) - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); -} - -/** - * GtkFlowBoxForeachFunc: - * @box: a #GtkFlowBox - * @child: a #GtkFlowBoxChild - * @user_data: (closure): user data - * - * A function used by gtk_flow_box_selected_foreach(). - * It will be called on every selected child of the @box. - * - * Since: 3.12 - */ - -/** - * gtk_flow_box_selected_foreach: - * @box: a #GtkFlowBox - * @func: (scope call): the function to call for each selected child - * @data: user data to pass to the function - * - * Calls a function for each selected child. - * - * Note that the selection cannot be modified from within - * this function. - * - * Since: 3.12 - */ -void -gtk_flow_box_selected_foreach (GtkFlowBox *box, - GtkFlowBoxForeachFunc func, - gpointer data) -{ - GtkFlowBoxChild *child; - GSequenceIter *iter; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); - !g_sequence_iter_is_end (iter); - iter = g_sequence_iter_next (iter)) - { - child = g_sequence_get (iter); - if (CHILD_PRIV (child)->selected) - (*func) (box, child, data); - } -} - -/** - * gtk_flow_box_set_selection_mode: - * @box: a #GtkFlowBox - * @mode: the new selection mode - * - * Sets how selection works in @box. - * See #GtkSelectionMode for details. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_selection_mode (GtkFlowBox *box, - GtkSelectionMode mode) -{ - gboolean dirty = FALSE; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (mode == BOX_PRIV (box)->selection_mode) - return; - - if (mode == GTK_SELECTION_NONE || - BOX_PRIV (box)->selection_mode == GTK_SELECTION_MULTIPLE) - { - dirty = gtk_flow_box_unselect_all_internal (box); - BOX_PRIV (box)->selected_child = NULL; - } - - BOX_PRIV (box)->selection_mode = mode; - - g_object_notify (G_OBJECT (box), "selection-mode"); - - if (dirty) - g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); -} - -/** - * gtk_flow_box_get_selection_mode: - * @box: a #GtkFlowBox - * - * Gets the selection mode of @box. - * - * Returns: the #GtkSelectionMode - * - * Since: 3.12 - */ -GtkSelectionMode -gtk_flow_box_get_selection_mode (GtkFlowBox *box) -{ - g_return_val_if_fail (GTK_IS_FLOW_BOX (box), GTK_SELECTION_SINGLE); - - return BOX_PRIV (box)->selection_mode; -} - -/* Filtering {{{2 */ - -/** - * GtkFlowBoxFilterFunc: - * @child: a #GtkFlowBoxChild that may be filtered - * @user_data: (closure): user data - * - * A function that will be called whenrever a child changes - * or is added. It lets you control if the child should be - * visible or not. - * - * Returns: %TRUE if the row should be visible, %FALSE otherwise - * - * Since: 3.12 - */ - -/** - * gtk_flow_box_set_filter_func: - * @box: a #GtkFlowBox - * @filter_func: (closure user_data) (allow-none): callback that - * lets you filter which children to show - * @user_data: user data passed to @filter_func - * @destroy: destroy notifier for @user_data - * - * By setting a filter function on the @box one can decide dynamically - * which of the children to show. For instance, to implement a search - * function that only shows the children matching the search terms. - * - * The @filter_func will be called for each child after the call, and - * it will continue to be called each time a child changes (via - * gtk_flow_box_child_changed()) or when gtk_flow_box_invalidate_filter() - * is called. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_filter_func (GtkFlowBox *box, - GtkFlowBoxFilterFunc filter_func, - gpointer user_data, - GDestroyNotify destroy) -{ - GtkFlowBoxPrivate *priv; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - priv = BOX_PRIV (box); - - if (priv->filter_destroy != NULL) - priv->filter_destroy (priv->filter_data); - - priv->filter_func = filter_func; - priv->filter_data = user_data; - priv->filter_destroy = destroy; - - gtk_flow_box_apply_filter_all (box); -} - -/** - * gtk_flow_box_invalidate_filter: - * @box: a #GtkFlowBox - * - * Updates the filtering for all children. - * - * Call this function when the result of the filter - * function on the @box is changed due ot an external - * factor. For instance, this would be used if the - * filter function just looked for a specific search - * term, and the entry with the string has changed. - * - * Since: 3.12 - */ -void -gtk_flow_box_invalidate_filter (GtkFlowBox *box) -{ - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - if (BOX_PRIV (box)->filter_func != NULL) - gtk_flow_box_apply_filter_all (box); -} - -/* Sorting {{{2 */ - -/** - * GtkFlowBoxSortFunc: - * @child1: the first child - * @child2: the second child - * @user_data: (closure): user data - * - * A function to compare two children to determine which - * should come first. - * - * Returns: < 0 if @child1 should be before @child2, 0 if - * the are equal, and > 0 otherwise - * - * Since: 3.12 - */ - -/** - * gtk_flow_box_set_sort_func: - * @box: a #GtkFlowBox - * @sort_func: (closure user_data) (allow-none): the sort function - * @user_data: user data passed to @sort_func - * @destroy: destroy notifier for @user_data - * - * By setting a sort function on the @box, one can dynamically - * reorder the children of the box, based on the contents of - * the children. - * - * The @sort_func will be called for each child after the call, - * and will continue to be called each time a child changes (via - * gtk_flow_box_child_changed()) and when gtk_flow_box_invalidate_sort() - * is called. - * - * Since: 3.12 - */ -void -gtk_flow_box_set_sort_func (GtkFlowBox *box, - GtkFlowBoxSortFunc sort_func, - gpointer user_data, - GDestroyNotify destroy) -{ - GtkFlowBoxPrivate *priv; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - priv = BOX_PRIV (box); - - if (priv->sort_destroy != NULL) - priv->sort_destroy (priv->sort_data); - - priv->sort_func = sort_func; - priv->sort_data = user_data; - priv->sort_destroy = destroy; - - gtk_flow_box_invalidate_sort (box); -} - -static gint -gtk_flow_box_sort (GtkFlowBoxChild *a, - GtkFlowBoxChild *b, - GtkFlowBox *box) -{ - GtkFlowBoxPrivate *priv = BOX_PRIV (box); - - return priv->sort_func (a, b, priv->sort_data); -} - -/** - * gtk_flow_box_invalidate_sort: - * @box: a #GtkFlowBox - * - * Updates the sorting for all children. - * - * Call this when the result of the sort function on - * @box is changed due to an external factor. - * - * Since: 3.12 - */ -void -gtk_flow_box_invalidate_sort (GtkFlowBox *box) -{ - GtkFlowBoxPrivate *priv; - - g_return_if_fail (GTK_IS_FLOW_BOX (box)); - - priv = BOX_PRIV (box); - - if (priv->sort_func != NULL) - { - g_sequence_sort (priv->children, - (GCompareDataFunc)gtk_flow_box_sort, box); - gtk_widget_queue_resize (GTK_WIDGET (box)); - } -} - -/* vim:set foldmethod=marker expandtab: */ diff --git a/src/gtkflowbox.h b/src/gtkflowbox.h deleted file mode 100644 index 6f0549f..0000000 --- a/src/gtkflowbox.h +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2010 Openismus GmbH - * Copyright (C) 2013 Red Hat, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library 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 - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, see . - - * - * Authors: - * Tristan Van Berkom - * Matthias Clasen - * William Jon McCann - */ - -#ifndef __GTK_FLOW_BOX_H__ -#define __GTK_FLOW_BOX_H__ - -#include - -G_BEGIN_DECLS - - -#define GTK_TYPE_FLOW_BOX (gtk_flow_box_get_type ()) -#define GTK_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBox)) -#define GTK_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass)) -#define GTK_IS_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX)) -#define GTK_IS_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX)) -#define GTK_FLOW_BOX_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass)) - -typedef struct _GtkFlowBox GtkFlowBox; -typedef struct _GtkFlowBoxClass GtkFlowBoxClass; - -typedef struct _GtkFlowBoxChild GtkFlowBoxChild; -typedef struct _GtkFlowBoxChildClass GtkFlowBoxChildClass; - -struct _GtkFlowBox -{ - GtkContainer container; -}; - -struct _GtkFlowBoxClass -{ - GtkContainerClass parent_class; - - void (*child_activated) (GtkFlowBox *box, - GtkFlowBoxChild *child); - void (*selected_children_changed) (GtkFlowBox *box); - void (*activate_cursor_child) (GtkFlowBox *box); - void (*toggle_cursor_child) (GtkFlowBox *box); - void (*move_cursor) (GtkFlowBox *box, - GtkMovementStep step, - gint count); - void (*select_all) (GtkFlowBox *box); - void (*unselect_all) (GtkFlowBox *box); - - /* Padding for future expansion */ - void (*_gtk_reserved1) (void); - void (*_gtk_reserved2) (void); - void (*_gtk_reserved3) (void); - void (*_gtk_reserved4) (void); - void (*_gtk_reserved5) (void); - void (*_gtk_reserved6) (void); -}; - -#define GTK_TYPE_FLOW_BOX_CHILD (gtk_flow_box_child_get_type ()) -#define GTK_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChild)) -#define GTK_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass)) -#define GTK_IS_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX_CHILD)) -#define GTK_IS_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX_CHILD)) -#define GTK_FLOW_BOX_CHILD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EG_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass)) - -struct _GtkFlowBoxChild -{ - GtkBin parent_instance; -}; - -struct _GtkFlowBoxChildClass -{ - GtkBinClass parent_class; - - void (* activate) (GtkFlowBoxChild *child); - - /* Padding for future expansion */ - void (*_gtk_reserved1) (void); - void (*_gtk_reserved2) (void); -}; - -GType gtk_flow_box_child_get_type (void) G_GNUC_CONST; -GtkWidget* gtk_flow_box_child_new (void); -gint gtk_flow_box_child_get_index (GtkFlowBoxChild *child); -gboolean gtk_flow_box_child_is_selected (GtkFlowBoxChild *child); -void gtk_flow_box_child_changed (GtkFlowBoxChild *child); - - -GType gtk_flow_box_get_type (void) G_GNUC_CONST; - -GtkWidget *gtk_flow_box_new (void); -void gtk_flow_box_set_homogeneous (GtkFlowBox *box, - gboolean homogeneous); -gboolean gtk_flow_box_get_homogeneous (GtkFlowBox *box); -void gtk_flow_box_set_row_spacing (GtkFlowBox *box, - guint spacing); -guint gtk_flow_box_get_row_spacing (GtkFlowBox *box); - -void gtk_flow_box_set_column_spacing (GtkFlowBox *box, - guint spacing); -guint gtk_flow_box_get_column_spacing (GtkFlowBox *box); - -void gtk_flow_box_set_min_children_per_line (GtkFlowBox *box, - guint n_children); -guint gtk_flow_box_get_min_children_per_line (GtkFlowBox *box); - -void gtk_flow_box_set_max_children_per_line (GtkFlowBox *box, - guint n_children); -guint gtk_flow_box_get_max_children_per_line (GtkFlowBox *box); -void gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box, - gboolean single); -gboolean gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box); - -void gtk_flow_box_insert (GtkFlowBox *box, - GtkWidget *widget, - gint position); -GtkFlowBoxChild *gtk_flow_box_get_child_at_index (GtkFlowBox *box, - gint idx); - -typedef void (* GtkFlowBoxForeachFunc) (GtkFlowBox *box, - GtkFlowBoxChild *child, - gpointer user_data); - -void gtk_flow_box_selected_foreach (GtkFlowBox *box, - GtkFlowBoxForeachFunc func, - gpointer data); -GList *gtk_flow_box_get_selected_children (GtkFlowBox *box); -void gtk_flow_box_select_child (GtkFlowBox *box, - GtkFlowBoxChild *child); -void gtk_flow_box_unselect_child (GtkFlowBox *box, - GtkFlowBoxChild *child); -void gtk_flow_box_select_all (GtkFlowBox *box); -void gtk_flow_box_unselect_all (GtkFlowBox *box); -void gtk_flow_box_set_selection_mode (GtkFlowBox *box, - GtkSelectionMode mode); -GtkSelectionMode gtk_flow_box_get_selection_mode (GtkFlowBox *box); -void gtk_flow_box_set_hadjustment (GtkFlowBox *box, - GtkAdjustment *adjustment); -void gtk_flow_box_set_vadjustment (GtkFlowBox *box, - GtkAdjustment *adjustment); - -typedef gboolean (*GtkFlowBoxFilterFunc) (GtkFlowBoxChild *child, - gpointer user_data); - -void gtk_flow_box_set_filter_func (GtkFlowBox *box, - GtkFlowBoxFilterFunc filter_func, - gpointer user_data, - GDestroyNotify destroy); -void gtk_flow_box_invalidate_filter (GtkFlowBox *box); - -typedef gint (*GtkFlowBoxSortFunc) (GtkFlowBoxChild *child1, - GtkFlowBoxChild *child2, - gpointer user_data); - -void gtk_flow_box_set_sort_func (GtkFlowBox *box, - GtkFlowBoxSortFunc sort_func, - gpointer user_data, - GDestroyNotify destroy); -void gtk_flow_box_invalidate_sort (GtkFlowBox *box); - -G_END_DECLS - - -#endif /* __GTK_FLOW_BOX_H__ */ diff --git a/src/interface-curses.cpp b/src/interface-curses.cpp deleted file mode 100644 index 49339d4..0000000 --- a/src/interface-curses.cpp +++ /dev/null @@ -1,1548 +0,0 @@ -/* - * 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 . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#ifdef HAVE_TIGETSTR -#include - -/* - * Some macros in term.h interfere with our code. - */ -#undef lines -#endif - -#include -#include - -#ifdef EMSCRIPTEN -#include -#endif - -#include "sciteco.h" -#include "string-utils.h" -#include "cmdline.h" -#include "qregisters.h" -#include "ring.h" -#include "interface.h" -#include "interface-curses.h" - -#ifdef HAVE_WINDOWS_H -/* here it shouldn't cause conflicts with other headers */ -#define WIN32_LEAN_AND_MEAN -#include -#endif - -/** - * Whether we have PDCurses-only routines: - * Could be 0, even on PDCurses - */ -#ifndef PDCURSES -#define PDCURSES 0 -#endif - -/** - * Whether we're on PDCurses/win32 - */ -#if defined(__PDCURSES__) && defined(G_OS_WIN32) && \ - !defined(PDCURSES_WIN32A) -#define PDCURSES_WIN32 - -/* - * A_UNDERLINE is not supported by PDCurses/win32 - * and causes weird colors, so we simply disable it globally. - */ -#undef A_UNDERLINE -#define A_UNDERLINE 0 -#endif - -#ifdef NCURSES_VERSION -#if defined(G_OS_UNIX) || defined(G_OS_HAIKU) -/** - * Whether we're on ncurses/UNIX. - * Haiku has a UNIX-like terminal and is largely - * POSIX compliant, so we can handle it like a - * UNIX ncurses. - */ -#define NCURSES_UNIX -#elif defined(G_OS_WIN32) -/** - * Whether we're on ncurses/win32 console - */ -#define NCURSES_WIN32 -#endif -#endif - -namespace SciTECO { - -extern "C" { - -/* - * PDCurses/win32a by default assigns functions to certain - * keys like CTRL+V, CTRL++, CTRL+- and CTRL+=. - * This conflicts with SciTECO that must remain in control - * of keyboard processing. - * Unfortunately, the default mapping can only be disabled - * or changed via the internal PDC_set_function_key() in - * pdcwin.h. Therefore we declare it manually here. - */ -#ifdef PDCURSES_WIN32A -int PDC_set_function_key(const unsigned function, const int new_key); - -#define N_FUNCTION_KEYS 5 -#define FUNCTION_KEY_SHUT_DOWN 0 -#define FUNCTION_KEY_PASTE 1 -#define FUNCTION_KEY_ENLARGE_FONT 2 -#define FUNCTION_KEY_SHRINK_FONT 3 -#define FUNCTION_KEY_CHOOSE_FONT 4 -#endif - -static void scintilla_notify(Scintilla *sci, int idFrom, - void *notify, void *user_data); - -#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32) - -/** - * This handler is the Windows-analogue of a signal - * handler. MinGW provides signal(), but it's not - * reliable. - * This may also be used to handle CTRL_CLOSE_EVENTs. - * NOTE: Unlike signal handlers, this is executed in a - * separate thread. - */ -static BOOL WINAPI -console_ctrl_handler(DWORD type) -{ - switch (type) { - case CTRL_C_EVENT: - sigint_occurred = TRUE; - return TRUE; - } - - return FALSE; -} - -#endif - -} /* extern "C" */ - -#define UNNAMED_FILE "(Unnamed)" - -/** - * Get bright variant of one of the 8 standard - * curses colors. - * On 8 color terminals, this returns the non-bright - * color - but you __may__ get a bright version using - * the A_BOLD attribute. - * NOTE: This references `COLORS` and is thus not a - * constant expression. - */ -#define COLOR_LIGHT(C) \ - (COLORS < 16 ? (C) : (C) + 8) - -/* - * The 8 bright colors (if terminal supports at - * least 16 colors), else they are identical to - * the non-bright colors (default curses colors). - */ -#define COLOR_LBLACK COLOR_LIGHT(COLOR_BLACK) -#define COLOR_LRED COLOR_LIGHT(COLOR_RED) -#define COLOR_LGREEN COLOR_LIGHT(COLOR_GREEN) -#define COLOR_LYELLOW COLOR_LIGHT(COLOR_YELLOW) -#define COLOR_LBLUE COLOR_LIGHT(COLOR_BLUE) -#define COLOR_LMAGENTA COLOR_LIGHT(COLOR_MAGENTA) -#define COLOR_LCYAN COLOR_LIGHT(COLOR_CYAN) -#define COLOR_LWHITE COLOR_LIGHT(COLOR_WHITE) - -/** - * Curses attribute for the color combination - * `f` (foreground) and `b` (background) - * according to the color pairs initialized by - * Scinterm. - * NOTE: This depends on the global variable - * `COLORS` and is thus not a constant expression. - */ -#define SCI_COLOR_ATTR(f, b) \ - ((attr_t)COLOR_PAIR(SCI_COLOR_PAIR(f, b))) - -/** - * Translate a Scintilla-compatible RGB color value - * (0xBBGGRR) to a Curses color triple (0 to 1000 - * for each component). - */ -static inline void -rgb2curses(guint32 rgb, short &r, short &g, short &b) -{ - /* NOTE: We could also use 200/51 */ - r = ((rgb & 0x0000FF) >> 0)*1000/0xFF; - g = ((rgb & 0x00FF00) >> 8)*1000/0xFF; - b = ((rgb & 0xFF0000) >> 16)*1000/0xFF; -} - -/** - * Convert a Scintilla-compatible RGB color value - * (0xBBGGRR) to a Curses color code (e.g. COLOR_BLACK). - * This does not work with arbitrary RGB values but - * only the 16 RGB color values defined by Scinterm - * corresponding to the 16 terminal colors. - * It is equivalent to Scinterm's internal `term_color` - * function. - */ -static short -rgb2curses(guint32 rgb) -{ - switch (rgb) { - case 0x000000: return COLOR_BLACK; - case 0x000080: return COLOR_RED; - case 0x008000: return COLOR_GREEN; - case 0x008080: return COLOR_YELLOW; - case 0x800000: return COLOR_BLUE; - case 0x800080: return COLOR_MAGENTA; - case 0x808000: return COLOR_CYAN; - case 0xC0C0C0: return COLOR_WHITE; - case 0x404040: return COLOR_LBLACK; - case 0x0000FF: return COLOR_LRED; - case 0x00FF00: return COLOR_LGREEN; - case 0x00FFFF: return COLOR_LYELLOW; - case 0xFF0000: return COLOR_LBLUE; - case 0xFF00FF: return COLOR_LMAGENTA; - case 0xFFFF00: return COLOR_LCYAN; - case 0xFFFFFF: return COLOR_LWHITE; - } - - return COLOR_WHITE; -} - -static gsize -format_str(WINDOW *win, const gchar *str, - gssize len = -1, gint max_width = -1) -{ - int old_x, old_y; - gint chars_added = 0; - - getyx(win, old_y, old_x); - - if (len < 0) - len = strlen(str); - if (max_width < 0) - max_width = getmaxx(win) - old_x; - - while (len > 0) { - /* - * NOTE: This mapping is similar to - * View::set_representations() - */ - switch (*str) { - case CTL_KEY_ESC: - chars_added++; - if (chars_added > max_width) - goto truncate; - waddch(win, '$' | A_REVERSE); - break; - case '\r': - chars_added += 2; - if (chars_added > max_width) - goto truncate; - waddch(win, 'C' | A_REVERSE); - waddch(win, 'R' | A_REVERSE); - break; - case '\n': - chars_added += 2; - if (chars_added > max_width) - goto truncate; - waddch(win, 'L' | A_REVERSE); - waddch(win, 'F' | A_REVERSE); - break; - case '\t': - chars_added += 3; - if (chars_added > max_width) - goto truncate; - waddch(win, 'T' | A_REVERSE); - waddch(win, 'A' | A_REVERSE); - waddch(win, 'B' | A_REVERSE); - break; - default: - if (IS_CTL(*str)) { - chars_added += 2; - if (chars_added > max_width) - goto truncate; - waddch(win, '^' | A_REVERSE); - waddch(win, CTL_ECHO(*str) | A_REVERSE); - } else { - chars_added++; - if (chars_added > max_width) - goto truncate; - waddch(win, *str); - } - } - - str++; - len--; - } - - return getcurx(win) - old_x; - -truncate: - if (max_width >= 3) { - /* - * Truncate string - */ - wattron(win, A_UNDERLINE | A_BOLD); - mvwaddstr(win, old_y, old_x + max_width - 3, "..."); - wattroff(win, A_UNDERLINE | A_BOLD); - } - - return getcurx(win) - old_x; -} - -static gsize -format_filename(WINDOW *win, const gchar *filename, - gint max_width = -1) -{ - int old_x = getcurx(win); - - gchar *filename_canon = String::canonicalize_ctl(filename); - size_t filename_len = strlen(filename_canon); - - if (max_width < 0) - max_width = getmaxx(win) - old_x; - - if (filename_len <= (size_t)max_width) { - waddstr(win, filename_canon); - } else { - const gchar *keep_post = filename_canon + filename_len - - max_width + 3; - -#ifdef G_OS_WIN32 - const gchar *keep_pre = g_path_skip_root(filename_canon); - if (keep_pre) { - waddnstr(win, filename_canon, - keep_pre - filename_canon); - keep_post += keep_pre - filename_canon; - } -#endif - wattron(win, A_UNDERLINE | A_BOLD); - waddstr(win, "..."); - wattroff(win, A_UNDERLINE | A_BOLD); - waddstr(win, keep_post); - } - - g_free(filename_canon); - return getcurx(win) - old_x; -} - -void -ViewCurses::initialize_impl(void) -{ - sci = scintilla_new(scintilla_notify); - setup(); -} - -InterfaceCurses::InterfaceCurses() : stdout_orig(-1), stderr_orig(-1), - screen(NULL), - screen_tty(NULL), - info_window(NULL), - info_type(INFO_TYPE_BUFFER), - info_current(NULL), - msg_window(NULL), - cmdline_window(NULL), cmdline_pad(NULL), - cmdline_len(0), cmdline_rubout_len(0) -{ - for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) - color_table[i] = -1; - for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) - orig_color_table[i].r = -1; -} - -void -InterfaceCurses::Popup::add(PopupEntryType type, - const gchar *name, bool highlight) -{ - size_t name_len = strlen(name); - Entry *entry = (Entry *)g_malloc(sizeof(Entry) + name_len + 1); - - entry->type = type; - entry->highlight = highlight; - strcpy(entry->name, name); - - longest = MAX(longest, (gint)name_len); - length++; - - /* - * Entries are added in reverse (constant time for GSList), - * so they will later have to be reversed. - */ - list = g_slist_prepend(list, entry); -} - -void -InterfaceCurses::Popup::init_pad(attr_t attr) -{ - int cols = getmaxx(stdscr); /* screen width */ - int pad_lines; /* pad height */ - gint pad_cols; /* entry columns */ - gint pad_colwidth; /* width per entry column */ - - gint cur_col; - - /* reserve 2 spaces between columns */ - pad_colwidth = MIN(longest + 2, cols - 2); - - /* pad_cols = floor((cols - 2) / pad_colwidth) */ - pad_cols = (cols - 2) / pad_colwidth; - /* pad_lines = ceil(length / pad_cols) */ - pad_lines = (length+pad_cols-1) / pad_cols; - - /* - * Render the entire autocompletion list into a pad - * which can be higher than the physical screen. - * The pad uses two columns less than the screen since - * it will be drawn into the popup window which has left - * and right borders. - */ - pad = newpad(pad_lines, cols - 2); - - wbkgd(pad, ' ' | attr); - - /* - * cur_col is the row currently written. - * It does not wrap but grows indefinitely. - * Therefore the real current row is (cur_col % popup_cols) - */ - cur_col = 0; - for (GSList *cur = list; cur != NULL; cur = g_slist_next(cur)) { - Entry *entry = (Entry *)cur->data; - gint cur_line = cur_col/pad_cols + 1; - - wmove(pad, cur_line-1, - (cur_col % pad_cols)*pad_colwidth); - - wattrset(pad, entry->highlight ? A_BOLD : A_NORMAL); - - switch (entry->type) { - case POPUP_FILE: - case POPUP_DIRECTORY: - format_filename(pad, entry->name); - break; - default: - format_str(pad, entry->name); - break; - } - - cur_col++; - } -} - -void -InterfaceCurses::Popup::show(attr_t attr) -{ - int lines, cols; /* screen dimensions */ - gint pad_lines; - gint popup_lines; - gint bar_height, bar_y; - - if (!length) - /* nothing to display */ - return; - - getmaxyx(stdscr, lines, cols); - - if (window) - delwin(window); - else - /* reverse list only once */ - list = g_slist_reverse(list); - - if (!pad) - init_pad(attr); - pad_lines = getmaxy(pad); - - /* - * Popup window can cover all but one screen row. - * Another row is reserved for the top border. - */ - popup_lines = MIN(pad_lines + 1, lines - 1); - - /* window covers message, scintilla and info windows */ - window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0); - - wbkgdset(window, ' ' | attr); - - wborder(window, - ACS_VLINE, - ACS_VLINE, /* may be overwritten with scrollbar */ - ACS_HLINE, - ' ', /* no bottom line */ - ACS_ULCORNER, ACS_URCORNER, - ACS_VLINE, ACS_VLINE); - - copywin(pad, window, - pad_first_line, 0, - 1, 1, popup_lines - 1, cols - 2, FALSE); - - if (pad_lines <= popup_lines - 1) - /* no need for scrollbar */ - return; - - /* bar_height = ceil((popup_lines-1)/pad_lines * (popup_lines-2)) */ - bar_height = ((popup_lines-1)*(popup_lines-2) + pad_lines-1) / - pad_lines; - /* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */ - bar_y = pad_first_line*(popup_lines-2) / pad_lines + 1; - - mvwvline(window, 1, cols-1, ACS_CKBOARD, popup_lines-2); - /* - * We do not use ACS_BLOCK here since it will not - * always be drawn as a solid block (e.g. xterm). - * Instead, simply draw reverse blanks. - */ - wmove(window, bar_y, cols-1); - wattron(window, A_REVERSE); - wvline(window, ' ', bar_height); - - /* progress scroll position */ - pad_first_line += popup_lines - 1; - /* wrap on last shown page */ - pad_first_line %= pad_lines; - if (pad_lines - pad_first_line < popup_lines - 1) - /* show last page */ - pad_first_line = pad_lines - (popup_lines - 1); -} - -void -InterfaceCurses::Popup::clear(void) -{ - g_slist_free_full(list, g_free); - list = NULL; - length = 0; - longest = 0; - - pad_first_line = 0; - - if (window) { - delwin(window); - window = NULL; - } - - if (pad) { - delwin(pad); - pad = NULL; - } -} - -InterfaceCurses::Popup::~Popup() -{ - if (window) - delwin(window); - if (pad) - delwin(pad); - if (list) - g_slist_free_full(list, g_free); -} - -void -InterfaceCurses::main_impl(int &argc, char **&argv) -{ - /* - * We must register this handler to handle - * asynchronous interruptions via CTRL+C - * reliably. The signal handler we already - * have won't do. - */ -#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32) - SetConsoleCtrlHandler(console_ctrl_handler, TRUE); -#endif - - /* - * Make sure we have a string for the info line - * even if info_update() is never called. - */ - info_current = g_strdup(PACKAGE_NAME); -} - -void -InterfaceCurses::init_color_safe(guint color, guint32 rgb) -{ - short r, g, b; - -#ifdef PDCURSES_WIN32 - if (orig_color_table[color].r < 0) { - color_content((short)color, - &orig_color_table[color].r, - &orig_color_table[color].g, - &orig_color_table[color].b); - } -#endif - - rgb2curses(rgb, r, g, b); - ::init_color((short)color, r, g, b); -} - -#ifdef PDCURSES_WIN32 - -/* - * On PDCurses/win32, color_content() will actually return - * the real console color palette - or at least the default - * palette when the console started. - */ -void -InterfaceCurses::restore_colors(void) -{ - if (!can_change_color()) - return; - - for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) { - if (orig_color_table[i].r < 0) - continue; - - ::init_color((short)i, - orig_color_table[i].r, - orig_color_table[i].g, - orig_color_table[i].b); - } -} - -#elif defined(NCURSES_UNIX) - -/* - * FIXME: On UNIX/ncurses init_color_safe() __may__ change the - * terminal's palette permanently and there does not appear to be - * any portable way of restoring the original one. - * Curses has color_content(), but there is actually no terminal - * that allows querying the current palette and so color_content() - * will return bogus "default" values and only for the first 8 colors. - * It would do more damage to restore the palette returned by - * color_content() than it helps. - * xterm has the escape sequence "\e]104\x07" which restores - * the palette from Xdefaults but not all terminal emulators - * claiming to be "xterm" via $TERM support this escape sequence. - * lxterminal for instance will print gibberish instead. - * So we try to look whether $XTERM_VERSION is set. - * There are hardly any other terminal emulators that support palette - * resets. - * The only emulator I'm aware of which can be identified reliably - * by $TERM supporting a palette reset is the Linux console - * (see console_codes(4)). The escape sequence "\e]R" is already - * part of its terminfo description (orig_colors capability) - * which is apparently sent by endwin(), so the palette is - * already properly restored on endwin(). - * Welcome in Curses hell. - */ -void -InterfaceCurses::restore_colors(void) -{ - if (g_str_has_prefix(g_getenv("TERM") ? : "", "xterm") && - g_getenv("XTERM_VERSION")) { - /* - * Looks like a real xterm. $TERM alone is not - * sufficient to tell. - */ - fputs("\e]104\x07", screen_tty); - fflush(screen_tty); - } -} - -#else /* !PDCURSES_WIN32 && !NCURSES_UNIX */ - -void -InterfaceCurses::restore_colors(void) -{ - /* - * No way to restore the palette, or it's - * unnecessary (e.g. XCurses) - */ -} - -#endif - -void -InterfaceCurses::init_color(guint color, guint32 rgb) -{ - if (color >= G_N_ELEMENTS(color_table)) - return; - -#if defined(__PDCURSES__) && !defined(PDC_RGB) - /* - * PDCurses will usually number color codes differently - * (least significant bit is the blue component) while - * SciTECO macros will assume a standard terminal color - * code numbering with red as the LSB. - * Therefore we have to swap the bit order of the least - * significant 3 bits here. - */ - color = (color & ~0x5) | - ((color & 0x1) << 2) | ((color & 0x4) >> 2); -#endif - - if (cmdline_window) { - /* interactive mode */ - if (!can_change_color()) - return; - - init_color_safe(color, rgb); - } else { - /* - * batch mode: store colors, - * they can only be initialized after start_color() - * which is called by Scinterm when interactive - * mode is initialized - */ - color_table[color] = (gint32)rgb; - } -} - -#ifdef NCURSES_UNIX - -void -InterfaceCurses::init_screen(void) -{ - screen_tty = g_fopen("/dev/tty", "r+"); - /* should never fail */ - g_assert(screen_tty != NULL); - - screen = newterm(NULL, screen_tty, screen_tty); - if (!screen) { - g_fprintf(stderr, "Error initializing interactive mode. " - "$TERM may be incorrect.\n"); - exit(EXIT_FAILURE); - } - - /* - * If stdout or stderr would go to the terminal, - * redirect it. Otherwise, they are already redirected - * (e.g. to a file) and writing to them does not - * interrupt terminal interaction. - */ - if (isatty(1)) { - FILE *stdout_new; - stdout_orig = dup(1); - g_assert(stdout_orig >= 0); - stdout_new = g_freopen("/dev/null", "a+", stdout); - g_assert(stdout_new != NULL); - } - if (isatty(2)) { - FILE *stderr_new; - stderr_orig = dup(2); - g_assert(stderr_orig >= 0); - stderr_new = g_freopen("/dev/null", "a+", stderr); - g_assert(stderr_new != NULL); - } -} - -#elif defined(XCURSES) - -void -InterfaceCurses::init_screen(void) -{ - const char *argv[] = {PACKAGE_NAME, NULL}; - - /* - * This sets the program name to "SciTECO" - * which may then also be used as the X11 class name - * for overwriting X11 resources in .Xdefaults - * FIXME: We could support passing in resource - * overrides via the SciTECO command line. - * But unfortunately, Xinitscr() is called too - * late to modify argc/argv for command-line parsing. - * Therefore this could only be supported by - * adding a special option like --resource. - */ - Xinitscr(1, (char **)argv); -} - -#else - -void -InterfaceCurses::init_screen(void) -{ - initscr(); -} - -#endif - -void -InterfaceCurses::init_interactive(void) -{ - /* - * Curses accesses many environment variables - * internally. In order to be able to modify them in - * the SciTECO profile, we must update the process - * environment before initscr()/newterm(). - * This is safe to do here since there are no threads. - */ - QRegisters::globals.update_environ(); - - /* - * On UNIX terminals, the escape key is usually - * delivered as the escape character even though function - * keys are delivered as escape sequences as well. - * That's why there has to be a timeout for detecting - * escape presses if function key handling is enabled. - * This timeout can be controlled using $ESCDELAY on - * ncurses but its default is much too long. - * We set it to 25ms as Vim does. In the very rare cases - * this won't suffice, $ESCDELAY can still be set explicitly. - * - * NOTE: The only terminal emulator I'm aware of that lets - * us send an escape sequence for the escape key is Mintty - * (see "\e[?7727h"). - */ -#ifdef NCURSES_UNIX - if (!g_getenv("ESCDELAY")) - set_escdelay(25); -#endif - - /* - * $TERM must be unset or "#win32con" for the win32 - * driver to load. - * So we always ignore any $TERM changes by the user. - */ -#ifdef NCURSES_WIN32 - g_setenv("TERM", "#win32con", TRUE); -#endif - -#ifdef PDCURSES_WIN32A - /* - * Necessary to enable window resizing in Win32a port - */ - PDC_set_resize_limits(25, 0xFFFF, 80, 0xFFFF); - - /* - * Disable all magic function keys. - * NOTE: This could also be used to assign - * a "shutdown" key when program termination is requested. - */ - for (int i = 0; i < N_FUNCTION_KEYS; i++) - PDC_set_function_key(i, 0); - - /* - * Register the special shutdown function with the - * CLOSE key, so closing the window behaves similar as on - * GTK+. - */ - PDC_set_function_key(FUNCTION_KEY_SHUT_DOWN, KEY_CLOSE); -#endif - - /* for displaying UTF-8 characters properly */ - setlocale(LC_CTYPE, ""); - - init_screen(); - - cbreak(); - noecho(); - /* Scintilla draws its own cursor */ - curs_set(0); - - info_window = newwin(1, 0, 0, 0); - - msg_window = newwin(1, 0, LINES - 2, 0); - - cmdline_window = newwin(0, 0, LINES - 1, 0); - keypad(cmdline_window, TRUE); - -#ifdef EMSCRIPTEN - nodelay(cmdline_window, TRUE); -#endif - - /* - * Will also initialize Scinterm, Curses color pairs - * and resizes the current view. - */ - if (current_view) - show_view(current_view); - - /* - * Only now it's safe to redefine the 16 default colors. - */ - if (can_change_color()) { - for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) { - /* - * init_color() may still fail if COLORS < 16 - */ - if (color_table[i] >= 0) - init_color_safe(i, (guint32)color_table[i]); - } - } -} - -void -InterfaceCurses::restore_batch(void) -{ - /* - * Set window title to a reasonable default, - * in case it is not reset immediately by the - * shell. - * FIXME: See set_window_title() why this - * is necessary. - */ -#if defined(NCURSES_UNIX) && defined(HAVE_TIGETSTR) - set_window_title(g_getenv("TERM") ? : ""); -#endif - - /* - * Restore ordinary terminal behaviour - * (i.e. return to batch mode) - */ - endwin(); - restore_colors(); - - /* - * Restore stdout and stderr, so output goes to - * the terminal again in case we "muted" them. - */ -#ifdef NCURSES_UNIX - if (stdout_orig >= 0) { - int fd = dup2(stdout_orig, 1); - g_assert(fd == 1); - } - if (stderr_orig >= 0) { - int fd = dup2(stderr_orig, 2); - g_assert(fd == 2); - } -#endif - - /* - * See vmsg_impl(): It looks at msg_win to determine - * whether we're in batch mode. - */ - if (msg_window) { - delwin(msg_window); - msg_window = NULL; - } -} - -void -InterfaceCurses::resize_all_windows(void) -{ - int lines, cols; /* screen dimensions */ - - getmaxyx(stdscr, lines, cols); - - wresize(info_window, 1, cols); - wresize(current_view->get_window(), - lines - 3, cols); - wresize(msg_window, 1, cols); - mvwin(msg_window, lines - 2, 0); - wresize(cmdline_window, 1, cols); - mvwin(cmdline_window, lines - 1, 0); - - draw_info(); - msg_clear(); /* FIXME: use saved message */ - popup_clear(); - draw_cmdline(); -} - -void -InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap) -{ - short fg, bg; - - if (!msg_window) { /* batch mode */ - stdio_vmsg(type, fmt, ap); - return; - } - - /* - * On most platforms we can write to stdout/stderr - * even in interactive mode. - */ -#if defined(XCURSES) || defined(PDCURSES_WIN32A) || \ - defined(NCURSES_UNIX) || defined(NCURSES_WIN32) - va_list aq; - va_copy(aq, ap); - stdio_vmsg(type, fmt, aq); - va_end(aq); -#endif - - fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); - - switch (type) { - default: - case MSG_USER: - bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); - break; - case MSG_INFO: - bg = COLOR_GREEN; - break; - case MSG_WARNING: - bg = COLOR_YELLOW; - break; - case MSG_ERROR: - bg = COLOR_RED; - beep(); - break; - } - - wmove(msg_window, 0, 0); - wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg)); - vw_printw(msg_window, fmt, ap); - wclrtoeol(msg_window); -} - -void -InterfaceCurses::msg_clear(void) -{ - short fg, bg; - - if (!msg_window) /* batch mode */ - return; - - fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); - bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); - - wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg)); - werase(msg_window); -} - -void -InterfaceCurses::show_view_impl(ViewCurses *view) -{ - int lines, cols; /* screen dimensions */ - WINDOW *current_view_win; - - current_view = view; - - if (!cmdline_window) /* batch mode */ - return; - - current_view_win = current_view->get_window(); - - /* - * screen size might have changed since - * this view's WINDOW was last active - */ - getmaxyx(stdscr, lines, cols); - wresize(current_view_win, lines - 3, cols); - /* Set up window position: never changes */ - mvwin(current_view_win, 1, 0); -} - -#if PDCURSES - -void -InterfaceCurses::set_window_title(const gchar *title) -{ - static gchar *last_title = NULL; - - /* - * PDC_set_title() can result in flickering - * even when executed only once per pressed key, - * so we check whether it is really necessary to change - * the title. - * This is an issue at least with PDCurses/win32. - */ - if (!g_strcmp0(title, last_title)) - return; - - PDC_set_title(title); - - g_free(last_title); - last_title = g_strdup(title); -} - -#elif defined(NCURSES_UNIX) && defined(HAVE_TIGETSTR) - -void -InterfaceCurses::set_window_title(const gchar *title) -{ - if (!has_status_line || !to_status_line || !from_status_line) - return; - - /* - * Modern terminal emulators map the window title to - * the historic status line. - * This feature is not standardized in ncurses, - * so we query the terminfo database. - * This feature may make problems with terminal emulators - * that do support a status line but do not map them - * to the window title. Some emulators (like xterm) - * support setting the window title via custom escape - * sequences and via the status line but their - * terminfo entry does not say so. (xterm can also - * save and restore window titles but there is not - * even a terminfo capability defined for this.) - * Taken the different emulator incompatibilites - * it may be best to make this configurable. - * Once we support configurable status lines, - * there could be a special status line that's sent - * to the terminal that may be set up in the profile - * depending on $TERM. - * - * NOTE: The terminfo manpage advises us to use putp() - * but on ncurses/UNIX (where terminfo is available), - * we do not let curses write to stdout. - * NOTE: This leaves the title set after we quit. - */ - fputs(to_status_line, screen_tty); - fputs(title, screen_tty); - fputs(from_status_line, screen_tty); - fflush(screen_tty); -} - -#else - -void -InterfaceCurses::set_window_title(const gchar *title) -{ - /* no way to set window title */ -} - -#endif - -void -InterfaceCurses::draw_info(void) -{ - short fg, bg; - const gchar *info_type_str; - gchar *info_current_canon, *title; - - if (!info_window) /* batch mode */ - return; - - /* - * The info line is printed in reverse colors of - * the current buffer's STYLE_DEFAULT. - * The same style is used for MSG_USER messages. - */ - fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); - bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); - - wmove(info_window, 0, 0); - wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(fg, bg)); - - switch (info_type) { - case INFO_TYPE_QREGISTER: - info_type_str = PACKAGE_NAME " - "; - waddstr(info_window, info_type_str); - /* same formatting as in command lines */ - format_str(info_window, info_current); - break; - - case INFO_TYPE_BUFFER: - info_type_str = PACKAGE_NAME " - "; - waddstr(info_window, info_type_str); - format_filename(info_window, info_current); - break; - - default: - g_assert_not_reached(); - } - - wclrtoeol(info_window); - - /* - * Make sure the title will consist only of printable - * characters - */ - info_current_canon = String::canonicalize_ctl(info_current); - title = g_strconcat(info_type_str, info_current_canon, NIL); - g_free(info_current_canon); - set_window_title(title); - g_free(title); -} - -void -InterfaceCurses::info_update_impl(const QRegister *reg) -{ - g_free(info_current); - /* NOTE: will contain control characters */ - info_type = INFO_TYPE_QREGISTER; - info_current = g_strdup(reg->name); - /* NOTE: drawn in event_loop_iter() */ -} - -void -InterfaceCurses::info_update_impl(const Buffer *buffer) -{ - g_free(info_current); - info_type = INFO_TYPE_BUFFER; - info_current = g_strconcat(buffer->filename ? : UNNAMED_FILE, - buffer->dirty ? "*" : " ", NIL); - /* NOTE: drawn in event_loop_iter() */ -} - -void -InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline) -{ - short fg, bg; - int max_cols = 1; - - /* - * Replace entire pre-formatted command-line. - * We don't know if it is similar to the last one, - * so resizing makes no sense. - * We approximate the size of the new formatted command-line, - * wasting a few bytes for control characters. - */ - if (cmdline_pad) - delwin(cmdline_pad); - for (guint i = 0; i < cmdline->len+cmdline->rubout_len; i++) - max_cols += IS_CTL((*cmdline)[i]) ? 3 : 1; - cmdline_pad = newpad(1, max_cols); - - fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); - bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); - wcolor_set(cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL); - - /* format effective command line */ - cmdline_len = format_str(cmdline_pad, cmdline->str, cmdline->len); - - /* - * A_BOLD should result in either a bold font or a brighter - * color both on 8 and 16 color terminals. - * This is not quite color-scheme-agnostic, but works - * with both the `terminal` and `solarized` themes. - * This problem will be gone once we use a Scintilla view - * as command line, since we can then define a style - * for rubbed out parts of the command line which will - * be user-configurable. - */ - wattron(cmdline_pad, A_UNDERLINE | A_BOLD); - - /* - * Format rubbed-out command line. - * NOTE: This formatting will never be truncated since we're - * writing into the pad which is large enough. - */ - cmdline_rubout_len = format_str(cmdline_pad, cmdline->str + cmdline->len, - cmdline->rubout_len); - - /* highlight cursor after effective command line */ - if (cmdline_rubout_len) { - attr_t attr; - short pair; - - wmove(cmdline_pad, 0, cmdline_len); - wattr_get(cmdline_pad, &attr, &pair, NULL); - wchgat(cmdline_pad, 1, - (attr & A_UNDERLINE) | A_REVERSE, pair, NULL); - } else { - cmdline_len++; - wattroff(cmdline_pad, A_UNDERLINE | A_BOLD); - waddch(cmdline_pad, ' ' | A_REVERSE); - } - - draw_cmdline(); -} - -void -InterfaceCurses::draw_cmdline(void) -{ - short fg, bg; - /* total width available for command line */ - guint total_width = getmaxx(cmdline_window) - 1; - /* beginning of command line to show */ - guint disp_offset; - /* length of command line to show */ - guint disp_len; - - disp_offset = cmdline_len - - MIN(cmdline_len, - total_width/2 + cmdline_len % MAX(total_width/2, 1)); - /* - * NOTE: we do not use getmaxx(cmdline_pad) here since it may be - * larger than the text the pad contains. - */ - disp_len = MIN(total_width, cmdline_len+cmdline_rubout_len - disp_offset); - - fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); - bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); - - wbkgdset(cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg)); - werase(cmdline_window); - mvwaddch(cmdline_window, 0, 0, '*' | A_BOLD); - copywin(cmdline_pad, cmdline_window, - 0, disp_offset, 0, 1, 0, disp_len, FALSE); -} - -void -InterfaceCurses::popup_show_impl(void) -{ - short fg, bg; - - if (!cmdline_window) - /* batch mode */ - return; - - fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_CALLTIP)); - bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_CALLTIP)); - - popup.show(SCI_COLOR_ATTR(fg, bg)); -} - -void -InterfaceCurses::popup_clear_impl(void) -{ -#ifdef __PDCURSES__ - /* - * PDCurses will not redraw all windows that may be - * overlapped by the popup window correctly - at least - * not the info window. - * The Scintilla window is apparently always touched by - * scintilla_noutrefresh(). - * Actually we would expect this to be necessary on any curses, - * but ncurses doesn't require this. - */ - if (popup.is_shown()) { - touchwin(info_window); - touchwin(msg_window); - } -#endif - - popup.clear(); -} - -/** - * One iteration of the event loop. - * - * This is a global function, so it may - * be used as an Emscripten callback. - * - * @bug - * Can probably be defined as a static method, - * so we can avoid declaring it a fried function of - * InterfaceCurses. - */ -void -event_loop_iter() -{ - int key; - - /* - * On PDCurses/win32, raw() and cbreak() does - * not disable and enable CTRL+C handling properly. - * Since I don't want to patch PDCurses/win32, - * we do this manually here. - * NOTE: This exploits the fact that PDCurses uses - * STD_INPUT_HANDLE internally! - */ -#ifdef PDCURSES_WIN32 - HANDLE console_hnd = GetStdHandle(STD_INPUT_HANDLE); - DWORD console_mode; - GetConsoleMode(console_hnd, &console_mode); -#endif - - /* - * Setting function key processing is important - * on Unix Curses, as ESCAPE is handled as the beginning - * of a escape sequence when terminal emulators are - * involved. - * On some Curses variants (XCurses) however, keypad - * must always be TRUE so we receive KEY_RESIZE. - */ -#ifdef NCURSES_UNIX - keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS); -#endif - - /* no special handling */ - raw(); -#ifdef PDCURSES_WIN32 - SetConsoleMode(console_hnd, console_mode & ~ENABLE_PROCESSED_INPUT); -#endif - key = wgetch(interface.cmdline_window); - /* allow asynchronous interruptions on */ - sigint_occurred = FALSE; - noraw(); /* FIXME: necessary because of NCURSES_WIN32 bug */ - cbreak(); -#ifdef PDCURSES_WIN32 - SetConsoleMode(console_hnd, console_mode | ENABLE_PROCESSED_INPUT); -#endif - if (key == ERR) - return; - - switch (key) { -#ifdef KEY_RESIZE - case KEY_RESIZE: -#if PDCURSES - resize_term(0, 0); -#endif - interface.resize_all_windows(); - break; -#endif - case CTL_KEY('H'): - case 0x7F: /* ^? */ - case KEY_BACKSPACE: - /* - * For historic reasons terminals can send - * ASCII 8 (^H) or 127 (^?) for backspace. - * Curses also defines KEY_BACKSPACE, probably - * for terminals that send an escape sequence for - * backspace. - * In SciTECO backspace is normalized to ^H. - */ - cmdline.keypress(CTL_KEY('H')); - break; - case KEY_ENTER: - case '\r': - case '\n': - cmdline.keypress('\n'); - break; - - /* - * Function key macros - */ -#define FN(KEY) case KEY_##KEY: cmdline.fnmacro(#KEY); break -#define FNS(KEY) FN(KEY); FN(S##KEY) - FN(DOWN); FN(UP); FNS(LEFT); FNS(RIGHT); - FNS(HOME); - case KEY_F(0)...KEY_F(63): { - gchar macro_name[3+1]; - - g_snprintf(macro_name, sizeof(macro_name), - "F%d", key - KEY_F0); - cmdline.fnmacro(macro_name); - break; - } - FNS(DC); - FNS(IC); - FN(NPAGE); FN(PPAGE); - FNS(PRINT); - FN(A1); FN(A3); FN(B2); FN(C1); FN(C3); - FNS(END); - FNS(HELP); - FN(CLOSE); -#undef FNS -#undef FN - - /* - * Control keys and keys with printable representation - */ - default: - if (key <= 0xFF) - cmdline.keypress((gchar)key); - } - - /* - * Info window is updated very often which is very - * costly, especially when using PDC_set_title(), - * so we redraw it here, where the overhead does - * not matter much. - */ - interface.draw_info(); - wnoutrefresh(interface.info_window); - interface.current_view->noutrefresh(); - wnoutrefresh(interface.msg_window); - wnoutrefresh(interface.cmdline_window); - interface.popup.noutrefresh(); - doupdate(); -} - -void -InterfaceCurses::event_loop_impl(void) -{ - static const Cmdline empty_cmdline; - - /* - * Initialize Curses for interactive mode - */ - init_interactive(); - - /* initial refresh */ - draw_info(); - wnoutrefresh(info_window); - current_view->noutrefresh(); - msg_clear(); - wnoutrefresh(msg_window); - cmdline_update(&empty_cmdline); - wnoutrefresh(cmdline_window); - doupdate(); - -#ifdef EMSCRIPTEN - PDC_emscripten_set_handler(event_loop_iter, TRUE); - /* - * We must not block emscripten's main loop, - * instead event_loop_iter() is called asynchronously. - * We also must not exit the event_loop() method, since - * SciTECO would assume ordinary program termination. - * We also must not call exit() since that would run - * the global destructors. - * The following exits the main() function immediately - * while keeping the "runtime" alive. - */ - emscripten_exit_with_live_runtime(); -#else - try { - for (;;) - event_loop_iter(); - } catch (Quit) { - /* SciTECO termination (e.g. EX$$) */ - } - - restore_batch(); -#endif -} - -InterfaceCurses::~InterfaceCurses() -{ - if (info_window) - delwin(info_window); - g_free(info_current); - if (cmdline_window) - delwin(cmdline_window); - if (cmdline_pad) - delwin(cmdline_pad); - if (msg_window) - delwin(msg_window); - - /* - * PDCurses (win32) crashes if initscr() wasn't called. - * Others (XCurses) crash if we try to use isendwin() here. - * Perhaps Curses cleanup should be in restore_batch() - * instead. - */ -#ifndef XCURSES - if (info_window && !isendwin()) - endwin(); -#endif - - if (screen) - delscreen(screen); - if (screen_tty) - fclose(screen_tty); - if (stderr_orig >= 0) - close(stderr_orig); - if (stdout_orig >= 0) - close(stdout_orig); -} - -/* - * Callbacks - */ - -static void -scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data) -{ - interface.process_notify((SCNotification *)notify); -} - -} /* namespace SciTECO */ diff --git a/src/interface-curses.h b/src/interface-curses.h deleted file mode 100644 index a6b0e1c..0000000 --- a/src/interface-curses.h +++ /dev/null @@ -1,232 +0,0 @@ -/* - * 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 . - */ - -#ifndef __INTERFACE_CURSES_H -#define __INTERFACE_CURSES_H - -#include - -#include - -#include - -#include -#include - -#include "interface.h" - -namespace SciTECO { - -typedef class ViewCurses : public View { - Scintilla *sci; - -public: - ViewCurses() : sci(NULL) {} - - /* implementation of View::initialize() */ - void initialize_impl(void); - - inline ~ViewCurses() - { - /* - * NOTE: This deletes/frees the view's - * curses WINDOW, despite of what old versions - * of the Scinterm documentation claim. - */ - if (sci) - scintilla_delete(sci); - } - - inline void - noutrefresh(void) - { - scintilla_noutrefresh(sci); - } - - inline void - refresh(void) - { - scintilla_refresh(sci); - } - - inline WINDOW * - get_window(void) - { - return scintilla_get_window(sci); - } - - /* implementation of View::ssm() */ - inline sptr_t - ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0) - { - return scintilla_send_message(sci, iMessage, wParam, lParam); - } -} ViewCurrent; - -typedef class InterfaceCurses : public Interface { - /** - * Mapping of the first 16 curses color codes (that may or may not - * correspond with the standard terminal color codes) to - * Scintilla-compatible RGB values (red is LSB) to initialize after - * Curses startup. - * Negative values mean no color redefinition (keep the original - * palette entry). - */ - gint32 color_table[16]; - - /** - * Mapping of the first 16 curses color codes to their - * original values for restoring them on shutdown. - * Unfortunately, this may not be supported on all - * curses ports, so this array may be unused. - */ - struct { - short r, g, b; - } orig_color_table[16]; - - int stdout_orig, stderr_orig; - SCREEN *screen; - FILE *screen_tty; - - WINDOW *info_window; - enum { - INFO_TYPE_BUFFER = 0, - INFO_TYPE_QREGISTER - } info_type; - gchar *info_current; - - WINDOW *msg_window; - - WINDOW *cmdline_window, *cmdline_pad; - gsize cmdline_len, cmdline_rubout_len; - - class Popup { - WINDOW *window; /**! window showing part of pad */ - WINDOW *pad; /**! full-height entry list */ - - struct Entry { - PopupEntryType type; - bool highlight; - gchar name[]; - }; - - GSList *list; /**! list of popup entries */ - gint longest; /**! size of longest entry */ - gint length; /**! total number of popup entries */ - - gint pad_first_line; /**! first line in pad to show */ - - public: - Popup() : window(NULL), pad(NULL), - list(NULL), longest(0), length(0), - pad_first_line(0) {} - - void add(PopupEntryType type, - const gchar *name, bool highlight = false); - - void show(attr_t attr); - inline bool - is_shown(void) - { - return window != NULL; - } - - void clear(void); - - inline void - noutrefresh(void) - { - if (window) - wnoutrefresh(window); - } - - ~Popup(); - - private: - void init_pad(attr_t attr); - } popup; - -public: - InterfaceCurses(); - ~InterfaceCurses(); - - /* implementation of Interface::main() */ - void main_impl(int &argc, char **&argv); - - /* override of Interface::init_color() */ - void init_color(guint color, guint32 rgb); - - /* implementation of Interface::vmsg() */ - void vmsg_impl(MessageType type, const gchar *fmt, va_list ap); - /* override of Interface::msg_clear() */ - void msg_clear(void); - - /* implementation of Interface::show_view() */ - void show_view_impl(ViewCurses *view); - - /* implementation of Interface::info_update() */ - void info_update_impl(const QRegister *reg); - void info_update_impl(const Buffer *buffer); - - /* implementation of Interface::cmdline_update() */ - void cmdline_update_impl(const Cmdline *cmdline); - - /* implementation of Interface::popup_add() */ - inline void - popup_add_impl(PopupEntryType type, - const gchar *name, bool highlight = false) - { - if (cmdline_window) - /* interactive mode */ - popup.add(type, name, highlight); - } - - /* implementation of Interface::popup_show() */ - void popup_show_impl(void); - /* implementation of Interface::popup_is_shown() */ - inline bool - popup_is_shown_impl(void) - { - return popup.is_shown(); - } - - /* implementation of Interface::popup_clear() */ - void popup_clear_impl(void); - - /* main entry point (implementation) */ - void event_loop_impl(void); - -private: - void init_color_safe(guint color, guint32 rgb); - void restore_colors(void); - - void init_screen(void); - void init_interactive(void); - void restore_batch(void); - - void resize_all_windows(void); - - void set_window_title(const gchar *title); - void draw_info(void); - void draw_cmdline(void); - - friend void event_loop_iter(); -} InterfaceCurrent; - -} /* namespace SciTECO */ - -#endif diff --git a/src/interface-curses/Makefile.am b/src/interface-curses/Makefile.am new file mode 100644 index 0000000..a64694c --- /dev/null +++ b/src/interface-curses/Makefile.am @@ -0,0 +1,9 @@ +AM_CPPFLAGS += -I$(top_srcdir)/src + +AM_CXXFLAGS = -Wall -Wno-char-subscripts +if CLANG +AM_CXXFLAGS += -Wno-mismatched-tags +endif + +noinst_LTLIBRARIES = libsciteco-interface.la +libsciteco_interface_la_SOURCES = interface-curses.cpp interface-curses.h diff --git a/src/interface-curses/interface-curses.cpp b/src/interface-curses/interface-curses.cpp new file mode 100644 index 0000000..49339d4 --- /dev/null +++ b/src/interface-curses/interface-curses.cpp @@ -0,0 +1,1548 @@ +/* + * 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 . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef HAVE_TIGETSTR +#include + +/* + * Some macros in term.h interfere with our code. + */ +#undef lines +#endif + +#include +#include + +#ifdef EMSCRIPTEN +#include +#endif + +#include "sciteco.h" +#include "string-utils.h" +#include "cmdline.h" +#include "qregisters.h" +#include "ring.h" +#include "interface.h" +#include "interface-curses.h" + +#ifdef HAVE_WINDOWS_H +/* here it shouldn't cause conflicts with other headers */ +#define WIN32_LEAN_AND_MEAN +#include +#endif + +/** + * Whether we have PDCurses-only routines: + * Could be 0, even on PDCurses + */ +#ifndef PDCURSES +#define PDCURSES 0 +#endif + +/** + * Whether we're on PDCurses/win32 + */ +#if defined(__PDCURSES__) && defined(G_OS_WIN32) && \ + !defined(PDCURSES_WIN32A) +#define PDCURSES_WIN32 + +/* + * A_UNDERLINE is not supported by PDCurses/win32 + * and causes weird colors, so we simply disable it globally. + */ +#undef A_UNDERLINE +#define A_UNDERLINE 0 +#endif + +#ifdef NCURSES_VERSION +#if defined(G_OS_UNIX) || defined(G_OS_HAIKU) +/** + * Whether we're on ncurses/UNIX. + * Haiku has a UNIX-like terminal and is largely + * POSIX compliant, so we can handle it like a + * UNIX ncurses. + */ +#define NCURSES_UNIX +#elif defined(G_OS_WIN32) +/** + * Whether we're on ncurses/win32 console + */ +#define NCURSES_WIN32 +#endif +#endif + +namespace SciTECO { + +extern "C" { + +/* + * PDCurses/win32a by default assigns functions to certain + * keys like CTRL+V, CTRL++, CTRL+- and CTRL+=. + * This conflicts with SciTECO that must remain in control + * of keyboard processing. + * Unfortunately, the default mapping can only be disabled + * or changed via the internal PDC_set_function_key() in + * pdcwin.h. Therefore we declare it manually here. + */ +#ifdef PDCURSES_WIN32A +int PDC_set_function_key(const unsigned function, const int new_key); + +#define N_FUNCTION_KEYS 5 +#define FUNCTION_KEY_SHUT_DOWN 0 +#define FUNCTION_KEY_PASTE 1 +#define FUNCTION_KEY_ENLARGE_FONT 2 +#define FUNCTION_KEY_SHRINK_FONT 3 +#define FUNCTION_KEY_CHOOSE_FONT 4 +#endif + +static void scintilla_notify(Scintilla *sci, int idFrom, + void *notify, void *user_data); + +#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32) + +/** + * This handler is the Windows-analogue of a signal + * handler. MinGW provides signal(), but it's not + * reliable. + * This may also be used to handle CTRL_CLOSE_EVENTs. + * NOTE: Unlike signal handlers, this is executed in a + * separate thread. + */ +static BOOL WINAPI +console_ctrl_handler(DWORD type) +{ + switch (type) { + case CTRL_C_EVENT: + sigint_occurred = TRUE; + return TRUE; + } + + return FALSE; +} + +#endif + +} /* extern "C" */ + +#define UNNAMED_FILE "(Unnamed)" + +/** + * Get bright variant of one of the 8 standard + * curses colors. + * On 8 color terminals, this returns the non-bright + * color - but you __may__ get a bright version using + * the A_BOLD attribute. + * NOTE: This references `COLORS` and is thus not a + * constant expression. + */ +#define COLOR_LIGHT(C) \ + (COLORS < 16 ? (C) : (C) + 8) + +/* + * The 8 bright colors (if terminal supports at + * least 16 colors), else they are identical to + * the non-bright colors (default curses colors). + */ +#define COLOR_LBLACK COLOR_LIGHT(COLOR_BLACK) +#define COLOR_LRED COLOR_LIGHT(COLOR_RED) +#define COLOR_LGREEN COLOR_LIGHT(COLOR_GREEN) +#define COLOR_LYELLOW COLOR_LIGHT(COLOR_YELLOW) +#define COLOR_LBLUE COLOR_LIGHT(COLOR_BLUE) +#define COLOR_LMAGENTA COLOR_LIGHT(COLOR_MAGENTA) +#define COLOR_LCYAN COLOR_LIGHT(COLOR_CYAN) +#define COLOR_LWHITE COLOR_LIGHT(COLOR_WHITE) + +/** + * Curses attribute for the color combination + * `f` (foreground) and `b` (background) + * according to the color pairs initialized by + * Scinterm. + * NOTE: This depends on the global variable + * `COLORS` and is thus not a constant expression. + */ +#define SCI_COLOR_ATTR(f, b) \ + ((attr_t)COLOR_PAIR(SCI_COLOR_PAIR(f, b))) + +/** + * Translate a Scintilla-compatible RGB color value + * (0xBBGGRR) to a Curses color triple (0 to 1000 + * for each component). + */ +static inline void +rgb2curses(guint32 rgb, short &r, short &g, short &b) +{ + /* NOTE: We could also use 200/51 */ + r = ((rgb & 0x0000FF) >> 0)*1000/0xFF; + g = ((rgb & 0x00FF00) >> 8)*1000/0xFF; + b = ((rgb & 0xFF0000) >> 16)*1000/0xFF; +} + +/** + * Convert a Scintilla-compatible RGB color value + * (0xBBGGRR) to a Curses color code (e.g. COLOR_BLACK). + * This does not work with arbitrary RGB values but + * only the 16 RGB color values defined by Scinterm + * corresponding to the 16 terminal colors. + * It is equivalent to Scinterm's internal `term_color` + * function. + */ +static short +rgb2curses(guint32 rgb) +{ + switch (rgb) { + case 0x000000: return COLOR_BLACK; + case 0x000080: return COLOR_RED; + case 0x008000: return COLOR_GREEN; + case 0x008080: return COLOR_YELLOW; + case 0x800000: return COLOR_BLUE; + case 0x800080: return COLOR_MAGENTA; + case 0x808000: return COLOR_CYAN; + case 0xC0C0C0: return COLOR_WHITE; + case 0x404040: return COLOR_LBLACK; + case 0x0000FF: return COLOR_LRED; + case 0x00FF00: return COLOR_LGREEN; + case 0x00FFFF: return COLOR_LYELLOW; + case 0xFF0000: return COLOR_LBLUE; + case 0xFF00FF: return COLOR_LMAGENTA; + case 0xFFFF00: return COLOR_LCYAN; + case 0xFFFFFF: return COLOR_LWHITE; + } + + return COLOR_WHITE; +} + +static gsize +format_str(WINDOW *win, const gchar *str, + gssize len = -1, gint max_width = -1) +{ + int old_x, old_y; + gint chars_added = 0; + + getyx(win, old_y, old_x); + + if (len < 0) + len = strlen(str); + if (max_width < 0) + max_width = getmaxx(win) - old_x; + + while (len > 0) { + /* + * NOTE: This mapping is similar to + * View::set_representations() + */ + switch (*str) { + case CTL_KEY_ESC: + chars_added++; + if (chars_added > max_width) + goto truncate; + waddch(win, '$' | A_REVERSE); + break; + case '\r': + chars_added += 2; + if (chars_added > max_width) + goto truncate; + waddch(win, 'C' | A_REVERSE); + waddch(win, 'R' | A_REVERSE); + break; + case '\n': + chars_added += 2; + if (chars_added > max_width) + goto truncate; + waddch(win, 'L' | A_REVERSE); + waddch(win, 'F' | A_REVERSE); + break; + case '\t': + chars_added += 3; + if (chars_added > max_width) + goto truncate; + waddch(win, 'T' | A_REVERSE); + waddch(win, 'A' | A_REVERSE); + waddch(win, 'B' | A_REVERSE); + break; + default: + if (IS_CTL(*str)) { + chars_added += 2; + if (chars_added > max_width) + goto truncate; + waddch(win, '^' | A_REVERSE); + waddch(win, CTL_ECHO(*str) | A_REVERSE); + } else { + chars_added++; + if (chars_added > max_width) + goto truncate; + waddch(win, *str); + } + } + + str++; + len--; + } + + return getcurx(win) - old_x; + +truncate: + if (max_width >= 3) { + /* + * Truncate string + */ + wattron(win, A_UNDERLINE | A_BOLD); + mvwaddstr(win, old_y, old_x + max_width - 3, "..."); + wattroff(win, A_UNDERLINE | A_BOLD); + } + + return getcurx(win) - old_x; +} + +static gsize +format_filename(WINDOW *win, const gchar *filename, + gint max_width = -1) +{ + int old_x = getcurx(win); + + gchar *filename_canon = String::canonicalize_ctl(filename); + size_t filename_len = strlen(filename_canon); + + if (max_width < 0) + max_width = getmaxx(win) - old_x; + + if (filename_len <= (size_t)max_width) { + waddstr(win, filename_canon); + } else { + const gchar *keep_post = filename_canon + filename_len - + max_width + 3; + +#ifdef G_OS_WIN32 + const gchar *keep_pre = g_path_skip_root(filename_canon); + if (keep_pre) { + waddnstr(win, filename_canon, + keep_pre - filename_canon); + keep_post += keep_pre - filename_canon; + } +#endif + wattron(win, A_UNDERLINE | A_BOLD); + waddstr(win, "..."); + wattroff(win, A_UNDERLINE | A_BOLD); + waddstr(win, keep_post); + } + + g_free(filename_canon); + return getcurx(win) - old_x; +} + +void +ViewCurses::initialize_impl(void) +{ + sci = scintilla_new(scintilla_notify); + setup(); +} + +InterfaceCurses::InterfaceCurses() : stdout_orig(-1), stderr_orig(-1), + screen(NULL), + screen_tty(NULL), + info_window(NULL), + info_type(INFO_TYPE_BUFFER), + info_current(NULL), + msg_window(NULL), + cmdline_window(NULL), cmdline_pad(NULL), + cmdline_len(0), cmdline_rubout_len(0) +{ + for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) + color_table[i] = -1; + for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) + orig_color_table[i].r = -1; +} + +void +InterfaceCurses::Popup::add(PopupEntryType type, + const gchar *name, bool highlight) +{ + size_t name_len = strlen(name); + Entry *entry = (Entry *)g_malloc(sizeof(Entry) + name_len + 1); + + entry->type = type; + entry->highlight = highlight; + strcpy(entry->name, name); + + longest = MAX(longest, (gint)name_len); + length++; + + /* + * Entries are added in reverse (constant time for GSList), + * so they will later have to be reversed. + */ + list = g_slist_prepend(list, entry); +} + +void +InterfaceCurses::Popup::init_pad(attr_t attr) +{ + int cols = getmaxx(stdscr); /* screen width */ + int pad_lines; /* pad height */ + gint pad_cols; /* entry columns */ + gint pad_colwidth; /* width per entry column */ + + gint cur_col; + + /* reserve 2 spaces between columns */ + pad_colwidth = MIN(longest + 2, cols - 2); + + /* pad_cols = floor((cols - 2) / pad_colwidth) */ + pad_cols = (cols - 2) / pad_colwidth; + /* pad_lines = ceil(length / pad_cols) */ + pad_lines = (length+pad_cols-1) / pad_cols; + + /* + * Render the entire autocompletion list into a pad + * which can be higher than the physical screen. + * The pad uses two columns less than the screen since + * it will be drawn into the popup window which has left + * and right borders. + */ + pad = newpad(pad_lines, cols - 2); + + wbkgd(pad, ' ' | attr); + + /* + * cur_col is the row currently written. + * It does not wrap but grows indefinitely. + * Therefore the real current row is (cur_col % popup_cols) + */ + cur_col = 0; + for (GSList *cur = list; cur != NULL; cur = g_slist_next(cur)) { + Entry *entry = (Entry *)cur->data; + gint cur_line = cur_col/pad_cols + 1; + + wmove(pad, cur_line-1, + (cur_col % pad_cols)*pad_colwidth); + + wattrset(pad, entry->highlight ? A_BOLD : A_NORMAL); + + switch (entry->type) { + case POPUP_FILE: + case POPUP_DIRECTORY: + format_filename(pad, entry->name); + break; + default: + format_str(pad, entry->name); + break; + } + + cur_col++; + } +} + +void +InterfaceCurses::Popup::show(attr_t attr) +{ + int lines, cols; /* screen dimensions */ + gint pad_lines; + gint popup_lines; + gint bar_height, bar_y; + + if (!length) + /* nothing to display */ + return; + + getmaxyx(stdscr, lines, cols); + + if (window) + delwin(window); + else + /* reverse list only once */ + list = g_slist_reverse(list); + + if (!pad) + init_pad(attr); + pad_lines = getmaxy(pad); + + /* + * Popup window can cover all but one screen row. + * Another row is reserved for the top border. + */ + popup_lines = MIN(pad_lines + 1, lines - 1); + + /* window covers message, scintilla and info windows */ + window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0); + + wbkgdset(window, ' ' | attr); + + wborder(window, + ACS_VLINE, + ACS_VLINE, /* may be overwritten with scrollbar */ + ACS_HLINE, + ' ', /* no bottom line */ + ACS_ULCORNER, ACS_URCORNER, + ACS_VLINE, ACS_VLINE); + + copywin(pad, window, + pad_first_line, 0, + 1, 1, popup_lines - 1, cols - 2, FALSE); + + if (pad_lines <= popup_lines - 1) + /* no need for scrollbar */ + return; + + /* bar_height = ceil((popup_lines-1)/pad_lines * (popup_lines-2)) */ + bar_height = ((popup_lines-1)*(popup_lines-2) + pad_lines-1) / + pad_lines; + /* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */ + bar_y = pad_first_line*(popup_lines-2) / pad_lines + 1; + + mvwvline(window, 1, cols-1, ACS_CKBOARD, popup_lines-2); + /* + * We do not use ACS_BLOCK here since it will not + * always be drawn as a solid block (e.g. xterm). + * Instead, simply draw reverse blanks. + */ + wmove(window, bar_y, cols-1); + wattron(window, A_REVERSE); + wvline(window, ' ', bar_height); + + /* progress scroll position */ + pad_first_line += popup_lines - 1; + /* wrap on last shown page */ + pad_first_line %= pad_lines; + if (pad_lines - pad_first_line < popup_lines - 1) + /* show last page */ + pad_first_line = pad_lines - (popup_lines - 1); +} + +void +InterfaceCurses::Popup::clear(void) +{ + g_slist_free_full(list, g_free); + list = NULL; + length = 0; + longest = 0; + + pad_first_line = 0; + + if (window) { + delwin(window); + window = NULL; + } + + if (pad) { + delwin(pad); + pad = NULL; + } +} + +InterfaceCurses::Popup::~Popup() +{ + if (window) + delwin(window); + if (pad) + delwin(pad); + if (list) + g_slist_free_full(list, g_free); +} + +void +InterfaceCurses::main_impl(int &argc, char **&argv) +{ + /* + * We must register this handler to handle + * asynchronous interruptions via CTRL+C + * reliably. The signal handler we already + * have won't do. + */ +#if defined(PDCURSES_WIN32) || defined(NCURSES_WIN32) + SetConsoleCtrlHandler(console_ctrl_handler, TRUE); +#endif + + /* + * Make sure we have a string for the info line + * even if info_update() is never called. + */ + info_current = g_strdup(PACKAGE_NAME); +} + +void +InterfaceCurses::init_color_safe(guint color, guint32 rgb) +{ + short r, g, b; + +#ifdef PDCURSES_WIN32 + if (orig_color_table[color].r < 0) { + color_content((short)color, + &orig_color_table[color].r, + &orig_color_table[color].g, + &orig_color_table[color].b); + } +#endif + + rgb2curses(rgb, r, g, b); + ::init_color((short)color, r, g, b); +} + +#ifdef PDCURSES_WIN32 + +/* + * On PDCurses/win32, color_content() will actually return + * the real console color palette - or at least the default + * palette when the console started. + */ +void +InterfaceCurses::restore_colors(void) +{ + if (!can_change_color()) + return; + + for (guint i = 0; i < G_N_ELEMENTS(orig_color_table); i++) { + if (orig_color_table[i].r < 0) + continue; + + ::init_color((short)i, + orig_color_table[i].r, + orig_color_table[i].g, + orig_color_table[i].b); + } +} + +#elif defined(NCURSES_UNIX) + +/* + * FIXME: On UNIX/ncurses init_color_safe() __may__ change the + * terminal's palette permanently and there does not appear to be + * any portable way of restoring the original one. + * Curses has color_content(), but there is actually no terminal + * that allows querying the current palette and so color_content() + * will return bogus "default" values and only for the first 8 colors. + * It would do more damage to restore the palette returned by + * color_content() than it helps. + * xterm has the escape sequence "\e]104\x07" which restores + * the palette from Xdefaults but not all terminal emulators + * claiming to be "xterm" via $TERM support this escape sequence. + * lxterminal for instance will print gibberish instead. + * So we try to look whether $XTERM_VERSION is set. + * There are hardly any other terminal emulators that support palette + * resets. + * The only emulator I'm aware of which can be identified reliably + * by $TERM supporting a palette reset is the Linux console + * (see console_codes(4)). The escape sequence "\e]R" is already + * part of its terminfo description (orig_colors capability) + * which is apparently sent by endwin(), so the palette is + * already properly restored on endwin(). + * Welcome in Curses hell. + */ +void +InterfaceCurses::restore_colors(void) +{ + if (g_str_has_prefix(g_getenv("TERM") ? : "", "xterm") && + g_getenv("XTERM_VERSION")) { + /* + * Looks like a real xterm. $TERM alone is not + * sufficient to tell. + */ + fputs("\e]104\x07", screen_tty); + fflush(screen_tty); + } +} + +#else /* !PDCURSES_WIN32 && !NCURSES_UNIX */ + +void +InterfaceCurses::restore_colors(void) +{ + /* + * No way to restore the palette, or it's + * unnecessary (e.g. XCurses) + */ +} + +#endif + +void +InterfaceCurses::init_color(guint color, guint32 rgb) +{ + if (color >= G_N_ELEMENTS(color_table)) + return; + +#if defined(__PDCURSES__) && !defined(PDC_RGB) + /* + * PDCurses will usually number color codes differently + * (least significant bit is the blue component) while + * SciTECO macros will assume a standard terminal color + * code numbering with red as the LSB. + * Therefore we have to swap the bit order of the least + * significant 3 bits here. + */ + color = (color & ~0x5) | + ((color & 0x1) << 2) | ((color & 0x4) >> 2); +#endif + + if (cmdline_window) { + /* interactive mode */ + if (!can_change_color()) + return; + + init_color_safe(color, rgb); + } else { + /* + * batch mode: store colors, + * they can only be initialized after start_color() + * which is called by Scinterm when interactive + * mode is initialized + */ + color_table[color] = (gint32)rgb; + } +} + +#ifdef NCURSES_UNIX + +void +InterfaceCurses::init_screen(void) +{ + screen_tty = g_fopen("/dev/tty", "r+"); + /* should never fail */ + g_assert(screen_tty != NULL); + + screen = newterm(NULL, screen_tty, screen_tty); + if (!screen) { + g_fprintf(stderr, "Error initializing interactive mode. " + "$TERM may be incorrect.\n"); + exit(EXIT_FAILURE); + } + + /* + * If stdout or stderr would go to the terminal, + * redirect it. Otherwise, they are already redirected + * (e.g. to a file) and writing to them does not + * interrupt terminal interaction. + */ + if (isatty(1)) { + FILE *stdout_new; + stdout_orig = dup(1); + g_assert(stdout_orig >= 0); + stdout_new = g_freopen("/dev/null", "a+", stdout); + g_assert(stdout_new != NULL); + } + if (isatty(2)) { + FILE *stderr_new; + stderr_orig = dup(2); + g_assert(stderr_orig >= 0); + stderr_new = g_freopen("/dev/null", "a+", stderr); + g_assert(stderr_new != NULL); + } +} + +#elif defined(XCURSES) + +void +InterfaceCurses::init_screen(void) +{ + const char *argv[] = {PACKAGE_NAME, NULL}; + + /* + * This sets the program name to "SciTECO" + * which may then also be used as the X11 class name + * for overwriting X11 resources in .Xdefaults + * FIXME: We could support passing in resource + * overrides via the SciTECO command line. + * But unfortunately, Xinitscr() is called too + * late to modify argc/argv for command-line parsing. + * Therefore this could only be supported by + * adding a special option like --resource. + */ + Xinitscr(1, (char **)argv); +} + +#else + +void +InterfaceCurses::init_screen(void) +{ + initscr(); +} + +#endif + +void +InterfaceCurses::init_interactive(void) +{ + /* + * Curses accesses many environment variables + * internally. In order to be able to modify them in + * the SciTECO profile, we must update the process + * environment before initscr()/newterm(). + * This is safe to do here since there are no threads. + */ + QRegisters::globals.update_environ(); + + /* + * On UNIX terminals, the escape key is usually + * delivered as the escape character even though function + * keys are delivered as escape sequences as well. + * That's why there has to be a timeout for detecting + * escape presses if function key handling is enabled. + * This timeout can be controlled using $ESCDELAY on + * ncurses but its default is much too long. + * We set it to 25ms as Vim does. In the very rare cases + * this won't suffice, $ESCDELAY can still be set explicitly. + * + * NOTE: The only terminal emulator I'm aware of that lets + * us send an escape sequence for the escape key is Mintty + * (see "\e[?7727h"). + */ +#ifdef NCURSES_UNIX + if (!g_getenv("ESCDELAY")) + set_escdelay(25); +#endif + + /* + * $TERM must be unset or "#win32con" for the win32 + * driver to load. + * So we always ignore any $TERM changes by the user. + */ +#ifdef NCURSES_WIN32 + g_setenv("TERM", "#win32con", TRUE); +#endif + +#ifdef PDCURSES_WIN32A + /* + * Necessary to enable window resizing in Win32a port + */ + PDC_set_resize_limits(25, 0xFFFF, 80, 0xFFFF); + + /* + * Disable all magic function keys. + * NOTE: This could also be used to assign + * a "shutdown" key when program termination is requested. + */ + for (int i = 0; i < N_FUNCTION_KEYS; i++) + PDC_set_function_key(i, 0); + + /* + * Register the special shutdown function with the + * CLOSE key, so closing the window behaves similar as on + * GTK+. + */ + PDC_set_function_key(FUNCTION_KEY_SHUT_DOWN, KEY_CLOSE); +#endif + + /* for displaying UTF-8 characters properly */ + setlocale(LC_CTYPE, ""); + + init_screen(); + + cbreak(); + noecho(); + /* Scintilla draws its own cursor */ + curs_set(0); + + info_window = newwin(1, 0, 0, 0); + + msg_window = newwin(1, 0, LINES - 2, 0); + + cmdline_window = newwin(0, 0, LINES - 1, 0); + keypad(cmdline_window, TRUE); + +#ifdef EMSCRIPTEN + nodelay(cmdline_window, TRUE); +#endif + + /* + * Will also initialize Scinterm, Curses color pairs + * and resizes the current view. + */ + if (current_view) + show_view(current_view); + + /* + * Only now it's safe to redefine the 16 default colors. + */ + if (can_change_color()) { + for (guint i = 0; i < G_N_ELEMENTS(color_table); i++) { + /* + * init_color() may still fail if COLORS < 16 + */ + if (color_table[i] >= 0) + init_color_safe(i, (guint32)color_table[i]); + } + } +} + +void +InterfaceCurses::restore_batch(void) +{ + /* + * Set window title to a reasonable default, + * in case it is not reset immediately by the + * shell. + * FIXME: See set_window_title() why this + * is necessary. + */ +#if defined(NCURSES_UNIX) && defined(HAVE_TIGETSTR) + set_window_title(g_getenv("TERM") ? : ""); +#endif + + /* + * Restore ordinary terminal behaviour + * (i.e. return to batch mode) + */ + endwin(); + restore_colors(); + + /* + * Restore stdout and stderr, so output goes to + * the terminal again in case we "muted" them. + */ +#ifdef NCURSES_UNIX + if (stdout_orig >= 0) { + int fd = dup2(stdout_orig, 1); + g_assert(fd == 1); + } + if (stderr_orig >= 0) { + int fd = dup2(stderr_orig, 2); + g_assert(fd == 2); + } +#endif + + /* + * See vmsg_impl(): It looks at msg_win to determine + * whether we're in batch mode. + */ + if (msg_window) { + delwin(msg_window); + msg_window = NULL; + } +} + +void +InterfaceCurses::resize_all_windows(void) +{ + int lines, cols; /* screen dimensions */ + + getmaxyx(stdscr, lines, cols); + + wresize(info_window, 1, cols); + wresize(current_view->get_window(), + lines - 3, cols); + wresize(msg_window, 1, cols); + mvwin(msg_window, lines - 2, 0); + wresize(cmdline_window, 1, cols); + mvwin(cmdline_window, lines - 1, 0); + + draw_info(); + msg_clear(); /* FIXME: use saved message */ + popup_clear(); + draw_cmdline(); +} + +void +InterfaceCurses::vmsg_impl(MessageType type, const gchar *fmt, va_list ap) +{ + short fg, bg; + + if (!msg_window) { /* batch mode */ + stdio_vmsg(type, fmt, ap); + return; + } + + /* + * On most platforms we can write to stdout/stderr + * even in interactive mode. + */ +#if defined(XCURSES) || defined(PDCURSES_WIN32A) || \ + defined(NCURSES_UNIX) || defined(NCURSES_WIN32) + va_list aq; + va_copy(aq, ap); + stdio_vmsg(type, fmt, aq); + va_end(aq); +#endif + + fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + + switch (type) { + default: + case MSG_USER: + bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + break; + case MSG_INFO: + bg = COLOR_GREEN; + break; + case MSG_WARNING: + bg = COLOR_YELLOW; + break; + case MSG_ERROR: + bg = COLOR_RED; + beep(); + break; + } + + wmove(msg_window, 0, 0); + wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + vw_printw(msg_window, fmt, ap); + wclrtoeol(msg_window); +} + +void +InterfaceCurses::msg_clear(void) +{ + short fg, bg; + + if (!msg_window) /* batch mode */ + return; + + fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + + wbkgdset(msg_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + werase(msg_window); +} + +void +InterfaceCurses::show_view_impl(ViewCurses *view) +{ + int lines, cols; /* screen dimensions */ + WINDOW *current_view_win; + + current_view = view; + + if (!cmdline_window) /* batch mode */ + return; + + current_view_win = current_view->get_window(); + + /* + * screen size might have changed since + * this view's WINDOW was last active + */ + getmaxyx(stdscr, lines, cols); + wresize(current_view_win, lines - 3, cols); + /* Set up window position: never changes */ + mvwin(current_view_win, 1, 0); +} + +#if PDCURSES + +void +InterfaceCurses::set_window_title(const gchar *title) +{ + static gchar *last_title = NULL; + + /* + * PDC_set_title() can result in flickering + * even when executed only once per pressed key, + * so we check whether it is really necessary to change + * the title. + * This is an issue at least with PDCurses/win32. + */ + if (!g_strcmp0(title, last_title)) + return; + + PDC_set_title(title); + + g_free(last_title); + last_title = g_strdup(title); +} + +#elif defined(NCURSES_UNIX) && defined(HAVE_TIGETSTR) + +void +InterfaceCurses::set_window_title(const gchar *title) +{ + if (!has_status_line || !to_status_line || !from_status_line) + return; + + /* + * Modern terminal emulators map the window title to + * the historic status line. + * This feature is not standardized in ncurses, + * so we query the terminfo database. + * This feature may make problems with terminal emulators + * that do support a status line but do not map them + * to the window title. Some emulators (like xterm) + * support setting the window title via custom escape + * sequences and via the status line but their + * terminfo entry does not say so. (xterm can also + * save and restore window titles but there is not + * even a terminfo capability defined for this.) + * Taken the different emulator incompatibilites + * it may be best to make this configurable. + * Once we support configurable status lines, + * there could be a special status line that's sent + * to the terminal that may be set up in the profile + * depending on $TERM. + * + * NOTE: The terminfo manpage advises us to use putp() + * but on ncurses/UNIX (where terminfo is available), + * we do not let curses write to stdout. + * NOTE: This leaves the title set after we quit. + */ + fputs(to_status_line, screen_tty); + fputs(title, screen_tty); + fputs(from_status_line, screen_tty); + fflush(screen_tty); +} + +#else + +void +InterfaceCurses::set_window_title(const gchar *title) +{ + /* no way to set window title */ +} + +#endif + +void +InterfaceCurses::draw_info(void) +{ + short fg, bg; + const gchar *info_type_str; + gchar *info_current_canon, *title; + + if (!info_window) /* batch mode */ + return; + + /* + * The info line is printed in reverse colors of + * the current buffer's STYLE_DEFAULT. + * The same style is used for MSG_USER messages. + */ + fg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + + wmove(info_window, 0, 0); + wbkgdset(info_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + + switch (info_type) { + case INFO_TYPE_QREGISTER: + info_type_str = PACKAGE_NAME " - "; + waddstr(info_window, info_type_str); + /* same formatting as in command lines */ + format_str(info_window, info_current); + break; + + case INFO_TYPE_BUFFER: + info_type_str = PACKAGE_NAME " - "; + waddstr(info_window, info_type_str); + format_filename(info_window, info_current); + break; + + default: + g_assert_not_reached(); + } + + wclrtoeol(info_window); + + /* + * Make sure the title will consist only of printable + * characters + */ + info_current_canon = String::canonicalize_ctl(info_current); + title = g_strconcat(info_type_str, info_current_canon, NIL); + g_free(info_current_canon); + set_window_title(title); + g_free(title); +} + +void +InterfaceCurses::info_update_impl(const QRegister *reg) +{ + g_free(info_current); + /* NOTE: will contain control characters */ + info_type = INFO_TYPE_QREGISTER; + info_current = g_strdup(reg->name); + /* NOTE: drawn in event_loop_iter() */ +} + +void +InterfaceCurses::info_update_impl(const Buffer *buffer) +{ + g_free(info_current); + info_type = INFO_TYPE_BUFFER; + info_current = g_strconcat(buffer->filename ? : UNNAMED_FILE, + buffer->dirty ? "*" : " ", NIL); + /* NOTE: drawn in event_loop_iter() */ +} + +void +InterfaceCurses::cmdline_update_impl(const Cmdline *cmdline) +{ + short fg, bg; + int max_cols = 1; + + /* + * Replace entire pre-formatted command-line. + * We don't know if it is similar to the last one, + * so resizing makes no sense. + * We approximate the size of the new formatted command-line, + * wasting a few bytes for control characters. + */ + if (cmdline_pad) + delwin(cmdline_pad); + for (guint i = 0; i < cmdline->len+cmdline->rubout_len; i++) + max_cols += IS_CTL((*cmdline)[i]) ? 3 : 1; + cmdline_pad = newpad(1, max_cols); + + fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + wcolor_set(cmdline_pad, SCI_COLOR_PAIR(fg, bg), NULL); + + /* format effective command line */ + cmdline_len = format_str(cmdline_pad, cmdline->str, cmdline->len); + + /* + * A_BOLD should result in either a bold font or a brighter + * color both on 8 and 16 color terminals. + * This is not quite color-scheme-agnostic, but works + * with both the `terminal` and `solarized` themes. + * This problem will be gone once we use a Scintilla view + * as command line, since we can then define a style + * for rubbed out parts of the command line which will + * be user-configurable. + */ + wattron(cmdline_pad, A_UNDERLINE | A_BOLD); + + /* + * Format rubbed-out command line. + * NOTE: This formatting will never be truncated since we're + * writing into the pad which is large enough. + */ + cmdline_rubout_len = format_str(cmdline_pad, cmdline->str + cmdline->len, + cmdline->rubout_len); + + /* highlight cursor after effective command line */ + if (cmdline_rubout_len) { + attr_t attr; + short pair; + + wmove(cmdline_pad, 0, cmdline_len); + wattr_get(cmdline_pad, &attr, &pair, NULL); + wchgat(cmdline_pad, 1, + (attr & A_UNDERLINE) | A_REVERSE, pair, NULL); + } else { + cmdline_len++; + wattroff(cmdline_pad, A_UNDERLINE | A_BOLD); + waddch(cmdline_pad, ' ' | A_REVERSE); + } + + draw_cmdline(); +} + +void +InterfaceCurses::draw_cmdline(void) +{ + short fg, bg; + /* total width available for command line */ + guint total_width = getmaxx(cmdline_window) - 1; + /* beginning of command line to show */ + guint disp_offset; + /* length of command line to show */ + guint disp_len; + + disp_offset = cmdline_len - + MIN(cmdline_len, + total_width/2 + cmdline_len % MAX(total_width/2, 1)); + /* + * NOTE: we do not use getmaxx(cmdline_pad) here since it may be + * larger than the text the pad contains. + */ + disp_len = MIN(total_width, cmdline_len+cmdline_rubout_len - disp_offset); + + fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_DEFAULT)); + bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_DEFAULT)); + + wbkgdset(cmdline_window, ' ' | SCI_COLOR_ATTR(fg, bg)); + werase(cmdline_window); + mvwaddch(cmdline_window, 0, 0, '*' | A_BOLD); + copywin(cmdline_pad, cmdline_window, + 0, disp_offset, 0, 1, 0, disp_len, FALSE); +} + +void +InterfaceCurses::popup_show_impl(void) +{ + short fg, bg; + + if (!cmdline_window) + /* batch mode */ + return; + + fg = rgb2curses(ssm(SCI_STYLEGETFORE, STYLE_CALLTIP)); + bg = rgb2curses(ssm(SCI_STYLEGETBACK, STYLE_CALLTIP)); + + popup.show(SCI_COLOR_ATTR(fg, bg)); +} + +void +InterfaceCurses::popup_clear_impl(void) +{ +#ifdef __PDCURSES__ + /* + * PDCurses will not redraw all windows that may be + * overlapped by the popup window correctly - at least + * not the info window. + * The Scintilla window is apparently always touched by + * scintilla_noutrefresh(). + * Actually we would expect this to be necessary on any curses, + * but ncurses doesn't require this. + */ + if (popup.is_shown()) { + touchwin(info_window); + touchwin(msg_window); + } +#endif + + popup.clear(); +} + +/** + * One iteration of the event loop. + * + * This is a global function, so it may + * be used as an Emscripten callback. + * + * @bug + * Can probably be defined as a static method, + * so we can avoid declaring it a fried function of + * InterfaceCurses. + */ +void +event_loop_iter() +{ + int key; + + /* + * On PDCurses/win32, raw() and cbreak() does + * not disable and enable CTRL+C handling properly. + * Since I don't want to patch PDCurses/win32, + * we do this manually here. + * NOTE: This exploits the fact that PDCurses uses + * STD_INPUT_HANDLE internally! + */ +#ifdef PDCURSES_WIN32 + HANDLE console_hnd = GetStdHandle(STD_INPUT_HANDLE); + DWORD console_mode; + GetConsoleMode(console_hnd, &console_mode); +#endif + + /* + * Setting function key processing is important + * on Unix Curses, as ESCAPE is handled as the beginning + * of a escape sequence when terminal emulators are + * involved. + * On some Curses variants (XCurses) however, keypad + * must always be TRUE so we receive KEY_RESIZE. + */ +#ifdef NCURSES_UNIX + keypad(interface.cmdline_window, Flags::ed & Flags::ED_FNKEYS); +#endif + + /* no special handling */ + raw(); +#ifdef PDCURSES_WIN32 + SetConsoleMode(console_hnd, console_mode & ~ENABLE_PROCESSED_INPUT); +#endif + key = wgetch(interface.cmdline_window); + /* allow asynchronous interruptions on */ + sigint_occurred = FALSE; + noraw(); /* FIXME: necessary because of NCURSES_WIN32 bug */ + cbreak(); +#ifdef PDCURSES_WIN32 + SetConsoleMode(console_hnd, console_mode | ENABLE_PROCESSED_INPUT); +#endif + if (key == ERR) + return; + + switch (key) { +#ifdef KEY_RESIZE + case KEY_RESIZE: +#if PDCURSES + resize_term(0, 0); +#endif + interface.resize_all_windows(); + break; +#endif + case CTL_KEY('H'): + case 0x7F: /* ^? */ + case KEY_BACKSPACE: + /* + * For historic reasons terminals can send + * ASCII 8 (^H) or 127 (^?) for backspace. + * Curses also defines KEY_BACKSPACE, probably + * for terminals that send an escape sequence for + * backspace. + * In SciTECO backspace is normalized to ^H. + */ + cmdline.keypress(CTL_KEY('H')); + break; + case KEY_ENTER: + case '\r': + case '\n': + cmdline.keypress('\n'); + break; + + /* + * Function key macros + */ +#define FN(KEY) case KEY_##KEY: cmdline.fnmacro(#KEY); break +#define FNS(KEY) FN(KEY); FN(S##KEY) + FN(DOWN); FN(UP); FNS(LEFT); FNS(RIGHT); + FNS(HOME); + case KEY_F(0)...KEY_F(63): { + gchar macro_name[3+1]; + + g_snprintf(macro_name, sizeof(macro_name), + "F%d", key - KEY_F0); + cmdline.fnmacro(macro_name); + break; + } + FNS(DC); + FNS(IC); + FN(NPAGE); FN(PPAGE); + FNS(PRINT); + FN(A1); FN(A3); FN(B2); FN(C1); FN(C3); + FNS(END); + FNS(HELP); + FN(CLOSE); +#undef FNS +#undef FN + + /* + * Control keys and keys with printable representation + */ + default: + if (key <= 0xFF) + cmdline.keypress((gchar)key); + } + + /* + * Info window is updated very often which is very + * costly, especially when using PDC_set_title(), + * so we redraw it here, where the overhead does + * not matter much. + */ + interface.draw_info(); + wnoutrefresh(interface.info_window); + interface.current_view->noutrefresh(); + wnoutrefresh(interface.msg_window); + wnoutrefresh(interface.cmdline_window); + interface.popup.noutrefresh(); + doupdate(); +} + +void +InterfaceCurses::event_loop_impl(void) +{ + static const Cmdline empty_cmdline; + + /* + * Initialize Curses for interactive mode + */ + init_interactive(); + + /* initial refresh */ + draw_info(); + wnoutrefresh(info_window); + current_view->noutrefresh(); + msg_clear(); + wnoutrefresh(msg_window); + cmdline_update(&empty_cmdline); + wnoutrefresh(cmdline_window); + doupdate(); + +#ifdef EMSCRIPTEN + PDC_emscripten_set_handler(event_loop_iter, TRUE); + /* + * We must not block emscripten's main loop, + * instead event_loop_iter() is called asynchronously. + * We also must not exit the event_loop() method, since + * SciTECO would assume ordinary program termination. + * We also must not call exit() since that would run + * the global destructors. + * The following exits the main() function immediately + * while keeping the "runtime" alive. + */ + emscripten_exit_with_live_runtime(); +#else + try { + for (;;) + event_loop_iter(); + } catch (Quit) { + /* SciTECO termination (e.g. EX$$) */ + } + + restore_batch(); +#endif +} + +InterfaceCurses::~InterfaceCurses() +{ + if (info_window) + delwin(info_window); + g_free(info_current); + if (cmdline_window) + delwin(cmdline_window); + if (cmdline_pad) + delwin(cmdline_pad); + if (msg_window) + delwin(msg_window); + + /* + * PDCurses (win32) crashes if initscr() wasn't called. + * Others (XCurses) crash if we try to use isendwin() here. + * Perhaps Curses cleanup should be in restore_batch() + * instead. + */ +#ifndef XCURSES + if (info_window && !isendwin()) + endwin(); +#endif + + if (screen) + delscreen(screen); + if (screen_tty) + fclose(screen_tty); + if (stderr_orig >= 0) + close(stderr_orig); + if (stdout_orig >= 0) + close(stdout_orig); +} + +/* + * Callbacks + */ + +static void +scintilla_notify(Scintilla *sci, int idFrom, void *notify, void *user_data) +{ + interface.process_notify((SCNotification *)notify); +} + +} /* namespace SciTECO */ diff --git a/src/interface-curses/interface-curses.h b/src/interface-curses/interface-curses.h new file mode 100644 index 0000000..a6b0e1c --- /dev/null +++ b/src/interface-curses/interface-curses.h @@ -0,0 +1,232 @@ +/* + * 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 . + */ + +#ifndef __INTERFACE_CURSES_H +#define __INTERFACE_CURSES_H + +#include + +#include + +#include + +#include +#include + +#include "interface.h" + +namespace SciTECO { + +typedef class ViewCurses : public View { + Scintilla *sci; + +public: + ViewCurses() : sci(NULL) {} + + /* implementation of View::initialize() */ + void initialize_impl(void); + + inline ~ViewCurses() + { + /* + * NOTE: This deletes/frees the view's + * curses WINDOW, despite of what old versions + * of the Scinterm documentation claim. + */ + if (sci) + scintilla_delete(sci); + } + + inline void + noutrefresh(void) + { + scintilla_noutrefresh(sci); + } + + inline void + refresh(void) + { + scintilla_refresh(sci); + } + + inline WINDOW * + get_window(void) + { + return scintilla_get_window(sci); + } + + /* implementation of View::ssm() */ + inline sptr_t + ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0) + { + return scintilla_send_message(sci, iMessage, wParam, lParam); + } +} ViewCurrent; + +typedef class InterfaceCurses : public Interface { + /** + * Mapping of the first 16 curses color codes (that may or may not + * correspond with the standard terminal color codes) to + * Scintilla-compatible RGB values (red is LSB) to initialize after + * Curses startup. + * Negative values mean no color redefinition (keep the original + * palette entry). + */ + gint32 color_table[16]; + + /** + * Mapping of the first 16 curses color codes to their + * original values for restoring them on shutdown. + * Unfortunately, this may not be supported on all + * curses ports, so this array may be unused. + */ + struct { + short r, g, b; + } orig_color_table[16]; + + int stdout_orig, stderr_orig; + SCREEN *screen; + FILE *screen_tty; + + WINDOW *info_window; + enum { + INFO_TYPE_BUFFER = 0, + INFO_TYPE_QREGISTER + } info_type; + gchar *info_current; + + WINDOW *msg_window; + + WINDOW *cmdline_window, *cmdline_pad; + gsize cmdline_len, cmdline_rubout_len; + + class Popup { + WINDOW *window; /**! window showing part of pad */ + WINDOW *pad; /**! full-height entry list */ + + struct Entry { + PopupEntryType type; + bool highlight; + gchar name[]; + }; + + GSList *list; /**! list of popup entries */ + gint longest; /**! size of longest entry */ + gint length; /**! total number of popup entries */ + + gint pad_first_line; /**! first line in pad to show */ + + public: + Popup() : window(NULL), pad(NULL), + list(NULL), longest(0), length(0), + pad_first_line(0) {} + + void add(PopupEntryType type, + const gchar *name, bool highlight = false); + + void show(attr_t attr); + inline bool + is_shown(void) + { + return window != NULL; + } + + void clear(void); + + inline void + noutrefresh(void) + { + if (window) + wnoutrefresh(window); + } + + ~Popup(); + + private: + void init_pad(attr_t attr); + } popup; + +public: + InterfaceCurses(); + ~InterfaceCurses(); + + /* implementation of Interface::main() */ + void main_impl(int &argc, char **&argv); + + /* override of Interface::init_color() */ + void init_color(guint color, guint32 rgb); + + /* implementation of Interface::vmsg() */ + void vmsg_impl(MessageType type, const gchar *fmt, va_list ap); + /* override of Interface::msg_clear() */ + void msg_clear(void); + + /* implementation of Interface::show_view() */ + void show_view_impl(ViewCurses *view); + + /* implementation of Interface::info_update() */ + void info_update_impl(const QRegister *reg); + void info_update_impl(const Buffer *buffer); + + /* implementation of Interface::cmdline_update() */ + void cmdline_update_impl(const Cmdline *cmdline); + + /* implementation of Interface::popup_add() */ + inline void + popup_add_impl(PopupEntryType type, + const gchar *name, bool highlight = false) + { + if (cmdline_window) + /* interactive mode */ + popup.add(type, name, highlight); + } + + /* implementation of Interface::popup_show() */ + void popup_show_impl(void); + /* implementation of Interface::popup_is_shown() */ + inline bool + popup_is_shown_impl(void) + { + return popup.is_shown(); + } + + /* implementation of Interface::popup_clear() */ + void popup_clear_impl(void); + + /* main entry point (implementation) */ + void event_loop_impl(void); + +private: + void init_color_safe(guint color, guint32 rgb); + void restore_colors(void); + + void init_screen(void); + void init_interactive(void); + void restore_batch(void); + + void resize_all_windows(void); + + void set_window_title(const gchar *title); + void draw_info(void); + void draw_cmdline(void); + + friend void event_loop_iter(); +} InterfaceCurrent; + +} /* namespace SciTECO */ + +#endif diff --git a/src/interface-gtk.cpp b/src/interface-gtk.cpp deleted file mode 100644 index 9b0b0aa..0000000 --- a/src/interface-gtk.cpp +++ /dev/null @@ -1,759 +0,0 @@ -/* - * 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 . - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include - -#include -#include - -/* - * FIXME: Because of gdk_threads_enter(). - * The only way to do it in Gtk3 style would be using - * idle callbacks into the main thread and sync barriers (inefficient!) - * or doing it single-threaded and ticking the Gtk main loop - * (may be inefficient since gtk_events_pending() is doing - * syscalls; however that may be ailed by doing it less frequently). - */ -#define GDK_DISABLE_DEPRECATION_WARNINGS -#include - -#include -#include "gtk-info-popup.h" - -#include -#include - -#include "sciteco.h" -#include "string-utils.h" -#include "cmdline.h" -#include "qregisters.h" -#include "ring.h" -#include "interface.h" -#include "interface-gtk.h" - -/* - * Signal handlers (e.g. for handling SIGTERM) are only - * available on Unix and beginning with v2.30, while - * we still support v2.28. - * Handlers using `signal()` cannot be used easily for - * this purpose. - */ -#if defined(G_OS_UNIX) && GLIB_CHECK_VERSION(2,30,0) -#include -#define SCITECO_HANDLE_SIGNALS -#endif - -namespace SciTECO { - -extern "C" { - -static void scintilla_notify(ScintillaObject *sci, uptr_t idFrom, - SCNotification *notify, gpointer user_data); - -static gpointer exec_thread_cb(gpointer data); -static gboolean cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, - gpointer user_data); -static gboolean window_delete_cb(GtkWidget *w, GdkEventAny *e, - gpointer user_data); - -static gboolean sigterm_handler(gpointer user_data) G_GNUC_UNUSED; - -} /* extern "C" */ - -#define UNNAMED_FILE "(Unnamed)" - -void -ViewGtk::initialize_impl(void) -{ - gint events; - - gdk_threads_enter(); - - sci = SCINTILLA(scintilla_new()); - /* - * We don't want the object to be destroyed - * when it is removed from the vbox. - */ - g_object_ref_sink(G_OBJECT(sci)); - - scintilla_set_id(sci, 0); - - gtk_widget_set_size_request(get_widget(), 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(get_widget(), FALSE); - events = gtk_widget_get_events(get_widget()); - events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); - events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); - gtk_widget_set_events(get_widget(), events); - - g_signal_connect(G_OBJECT(sci), SCINTILLA_NOTIFY, - G_CALLBACK(scintilla_notify), NULL); - - /* - * setup() calls Scintilla messages, so we must unlock - * here already to avoid deadlocks. - */ - gdk_threads_leave(); - - setup(); -} - -void -InterfaceGtk::main_impl(int &argc, char **&argv) -{ - static const Cmdline empty_cmdline; - GtkWidget *overlay_widget; - GtkWidget *info_content; - - /* - * g_thread_init() is required prior to v2.32 - * (we still support v2.28) but generates a warning - * on newer versions. - */ -#if !GLIB_CHECK_VERSION(2,32,0) - g_thread_init(NULL); -#endif - gdk_threads_init(); - gtk_init(&argc, &argv); - - /* - * The event queue is initialized now, so we can - * pass it as user data to C-linkage callbacks. - */ - event_queue = g_async_queue_new(); - - window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_title(GTK_WINDOW(window), PACKAGE_NAME); - g_signal_connect(G_OBJECT(window), "delete-event", - G_CALLBACK(window_delete_cb), event_queue); - - vbox = gtk_vbox_new(FALSE, 0); - - info_current = g_strdup(PACKAGE_NAME); - - /* - * 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. - */ - overlay_widget = gtk_overlay_new(); - event_box_widget = gtk_event_box_new(); - gtk_event_box_set_above_child(GTK_EVENT_BOX(event_box_widget), TRUE); - gtk_container_add(GTK_CONTAINER(overlay_widget), event_box_widget); - gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0); - - info_widget = gtk_info_bar_new(); - info_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_widget)); - message_widget = gtk_label_new(""); - gtk_misc_set_alignment(GTK_MISC(message_widget), 0., 0.); - gtk_container_add(GTK_CONTAINER(info_content), message_widget); - gtk_box_pack_start(GTK_BOX(vbox), info_widget, FALSE, FALSE, 0); - - cmdline_widget = gtk_entry_new(); - gtk_entry_set_has_frame(GTK_ENTRY(cmdline_widget), FALSE); - gtk_editable_set_editable(GTK_EDITABLE(cmdline_widget), FALSE); - widget_set_font(cmdline_widget, "Courier"); - g_signal_connect(G_OBJECT(cmdline_widget), "key-press-event", - G_CALLBACK(cmdline_key_pressed_cb), event_queue); - gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0); - - gtk_container_add(GTK_CONTAINER(window), vbox); - - /* - * Popup widget will be shown in the bottom - * of the overlay widget (i.e. the Scintilla views), - * filling the entire width. - */ - popup_widget = gtk_info_popup_new(); - gtk_widget_set_halign(popup_widget, GTK_ALIGN_FILL); - gtk_widget_set_valign(popup_widget, GTK_ALIGN_END); - gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), popup_widget); - - gtk_widget_grab_focus(cmdline_widget); - - cmdline_update(&empty_cmdline); -} - -void -InterfaceGtk::vmsg_impl(MessageType type, const gchar *fmt, va_list ap) -{ - static const GtkMessageType type2gtk[] = { - /* [MSG_USER] = */ GTK_MESSAGE_OTHER, - /* [MSG_INFO] = */ GTK_MESSAGE_INFO, - /* [MSG_WARNING] = */ GTK_MESSAGE_WARNING, - /* [MSG_ERROR] = */ GTK_MESSAGE_ERROR - }; - - va_list aq; - gchar buf[255]; - - /* - * stdio_vmsg() leaves `ap` undefined and we are expected - * to do the same and behave like vprintf(). - */ - va_copy(aq, ap); - stdio_vmsg(type, fmt, ap); - g_vsnprintf(buf, sizeof(buf), fmt, aq); - va_end(aq); - - gdk_threads_enter(); - - gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget), - type2gtk[type]); - gtk_label_set_text(GTK_LABEL(message_widget), buf); - - if (type == MSG_ERROR) - gtk_widget_error_bell(window); - - gdk_threads_leave(); -} - -void -InterfaceGtk::msg_clear(void) -{ - gdk_threads_enter(); - - gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget), - GTK_MESSAGE_OTHER); - gtk_label_set_text(GTK_LABEL(message_widget), ""); - - gdk_threads_leave(); -} - -void -InterfaceGtk::show_view_impl(ViewGtk *view) -{ - current_view = view; -} - -void -InterfaceGtk::info_update_impl(const QRegister *reg) -{ - gchar *name = String::canonicalize_ctl(reg->name); - - g_free(info_current); - info_current = g_strconcat(PACKAGE_NAME " - ", - name, NIL); - g_free(name); -} - -void -InterfaceGtk::info_update_impl(const Buffer *buffer) -{ - g_free(info_current); - info_current = g_strconcat(PACKAGE_NAME " - ", - buffer->filename ? : UNNAMED_FILE, - buffer->dirty ? "*" : "", NIL); -} - -void -InterfaceGtk::cmdline_insert_chr(gint &pos, gchar chr) -{ - gchar buffer[5+1]; - - /* - * NOTE: This mapping is similar to - * View::set_representations() - */ - switch (chr) { - case CTL_KEY_ESC: - strcpy(buffer, "$"); - break; - case '\r': - strcpy(buffer, ""); - break; - case '\n': - strcpy(buffer, ""); - break; - case '\t': - strcpy(buffer, ""); - break; - default: - if (IS_CTL(chr)) { - buffer[0] = '^'; - buffer[1] = CTL_ECHO(chr); - buffer[2] = '\0'; - } else { - buffer[0] = chr; - buffer[1] = '\0'; - } - } - - gtk_editable_insert_text(GTK_EDITABLE(cmdline_widget), - buffer, -1, &pos); -} - -void -InterfaceGtk::cmdline_update_impl(const Cmdline *cmdline) -{ - gint pos = 1; - gint cmdline_len; - - gdk_threads_enter(); - - /* - * We don't know if the new command line is similar to - * the old one, so we can just as well rebuild it. - */ - gtk_entry_set_text(GTK_ENTRY(cmdline_widget), "*"); - - /* format effective command line */ - for (guint i = 0; i < cmdline->len; i++) - cmdline_insert_chr(pos, (*cmdline)[i]); - /* save end of effective command line */ - cmdline_len = pos; - - /* format rubbed out command line */ - for (guint i = cmdline->len; i < cmdline->len+cmdline->rubout_len; i++) - cmdline_insert_chr(pos, (*cmdline)[i]); - - /* set cursor after effective command line */ - gtk_editable_set_position(GTK_EDITABLE(cmdline_widget), cmdline_len); - - gdk_threads_leave(); -} - -void -InterfaceGtk::popup_add_impl(PopupEntryType type, - const gchar *name, bool highlight) -{ - static const GtkInfoPopupEntryType type2gtk[] = { - /* [POPUP_PLAIN] = */ GTK_INFO_POPUP_PLAIN, - /* [POPUP_FILE] = */ GTK_INFO_POPUP_FILE, - /* [POPUP_DIRECTORY] = */ GTK_INFO_POPUP_DIRECTORY - }; - - gdk_threads_enter(); - - gtk_info_popup_add(GTK_INFO_POPUP(popup_widget), - type2gtk[type], name, highlight); - - gdk_threads_leave(); -} - -void -InterfaceGtk::popup_show_impl(void) -{ - gdk_threads_enter(); - - if (gtk_widget_get_visible(popup_widget)) - gtk_info_popup_scroll_page(GTK_INFO_POPUP(popup_widget)); - else - gtk_widget_show(popup_widget); - - gdk_threads_leave(); -} - -void -InterfaceGtk::popup_clear_impl(void) -{ - gdk_threads_enter(); - - if (gtk_widget_get_visible(popup_widget)) { - gtk_widget_hide(popup_widget); - gtk_info_popup_clear(GTK_INFO_POPUP(popup_widget)); - } - - gdk_threads_leave(); -} - -void -InterfaceGtk::widget_set_font(GtkWidget *widget, const gchar *font_name) -{ - PangoFontDescription *font_desc; - - font_desc = pango_font_description_from_string(font_name); - gtk_widget_modify_font(widget, font_desc); - pango_font_description_free(font_desc); -} - -void -InterfaceGtk::event_loop_impl(void) -{ - GThread *thread; - - /* - * Assign an icon to the window. - * If the file could not be found, we fail silently. - * FIXME: On Windows, it may be better to load the icon compiled - * as a resource into the binary. - * FIXME: Provide all the different icon sizes we have - * (gtk_window_set_default_icon_list()). - */ - gtk_window_set_default_icon_from_file(SCITECODATADIR G_DIR_SEPARATOR_S - "sciteco-48.png", - NULL); - - /* - * When changing views, the new widget is not - * added immediately to avoid flickering in the GUI. - * It is only updated once per key press and only - * if it really changed. - * Therefore we must add the current view to the - * window initially. - * For the same reason, window title updates are - * deferred to once after every key press, so we must - * set the window title initially. - */ - if (current_view) { - current_view_widget = current_view->get_widget(); - gtk_container_add(GTK_CONTAINER(event_box_widget), - current_view_widget); - } - gtk_window_set_title(GTK_WINDOW(window), info_current); - - gtk_widget_show_all(window); - /* don't show popup by default */ - gtk_widget_hide(popup_widget); - - /* - * SIGTERM emulates the "Close" key just like when - * closing the window if supported by this version of glib. - * Note that this replaces SciTECO's default SIGTERM handler - * so it will additionally raise(SIGINT). - */ -#ifdef SCITECO_HANDLE_SIGNALS - g_unix_signal_add(SIGTERM, sigterm_handler, event_queue); -#endif - - /* - * Start up SciTECO execution thread. - * Whenever it needs to send a Scintilla message - * it locks the GDK mutex. - */ - thread = g_thread_new("sciteco-exec", - exec_thread_cb, event_queue); - - /* - * NOTE: The watchers do not modify any GTK objects - * using one of the methods that lock the GDK mutex. - * This is from now on reserved to the execution - * thread. Therefore there can be no dead-locks. - */ - gdk_threads_enter(); - gtk_main(); - gdk_threads_leave(); - - /* - * This usually means that the user requested program - * termination and the execution thread called - * gtk_main_quit(). - * We still wait for the execution thread to shut down - * properly. This also frees `thread`. - */ - g_thread_join(thread); - - /* - * Make sure the window is hidden - * now already, as there may be code that has to be - * executed in batch mode. - */ - gtk_widget_hide(window); -} - -static gpointer -exec_thread_cb(gpointer data) -{ - GAsyncQueue *event_queue = (GAsyncQueue *)data; - - for (;;) { - GdkEventKey *event = (GdkEventKey *)g_async_queue_pop(event_queue); - - bool is_shift = event->state & GDK_SHIFT_MASK; - bool is_ctl = event->state & GDK_CONTROL_MASK; - - try { - sigint_occurred = FALSE; - interface.handle_key_press(is_shift, is_ctl, event->keyval); - sigint_occurred = FALSE; - } catch (Quit) { - /* - * SciTECO should terminate, so we exit - * this thread. - * The main loop will terminate and - * event_loop() will return. - */ - gdk_event_free((GdkEvent *)event); - - gdk_threads_enter(); - gtk_main_quit(); - gdk_threads_leave(); - break; - } - - gdk_event_free((GdkEvent *)event); - } - - return NULL; -} - -void -InterfaceGtk::handle_key_press(bool is_shift, bool is_ctl, guint keyval) -{ - GdkWindow *view_window; - ViewGtk *last_view = current_view; - - /* - * Avoid redraws of the current view by freezing updates - * on the view's GDK window (we're running in parallel - * to the main loop so there could be frequent redraws). - * By freezing updates, the behaviour is similar to - * the Curses UI. - */ - gdk_threads_enter(); - view_window = gtk_widget_get_parent_window(event_box_widget); - gdk_window_freeze_updates(view_window); - gdk_threads_leave(); - - switch (keyval) { - case GDK_KEY_Escape: - cmdline.keypress(CTL_KEY_ESC); - break; - case GDK_KEY_BackSpace: - cmdline.keypress(CTL_KEY('H')); - break; - case GDK_KEY_Tab: - cmdline.keypress('\t'); - break; - case GDK_KEY_Return: - cmdline.keypress('\n'); - break; - - /* - * Function key macros - */ -#define FN(KEY, MACRO) \ - case GDK_KEY_##KEY: cmdline.fnmacro(#MACRO); break -#define FNS(KEY, MACRO) \ - case GDK_KEY_##KEY: cmdline.fnmacro(is_shift ? "S" #MACRO : #MACRO); break - FN(Down, DOWN); FN(Up, UP); - FNS(Left, LEFT); FNS(Right, RIGHT); - FN(KP_Down, DOWN); FN(KP_Up, UP); - FNS(KP_Left, LEFT); FNS(KP_Right, RIGHT); - FNS(Home, HOME); - case GDK_KEY_F1...GDK_KEY_F35: { - gchar macro_name[3+1]; - - g_snprintf(macro_name, sizeof(macro_name), - "F%d", keyval - GDK_KEY_F1 + 1); - cmdline.fnmacro(macro_name); - break; - } - FNS(Delete, DC); - FNS(Insert, IC); - FN(Page_Down, NPAGE); FN(Page_Up, PPAGE); - FNS(Print, PRINT); - FN(KP_Home, A1); FN(KP_Prior, A3); - FN(KP_Begin, B2); - FN(KP_End, C1); FN(KP_Next, C3); - FNS(End, END); - FNS(Help, HELP); - FN(Close, CLOSE); -#undef FNS -#undef FN - - /* - * Control keys and keys with printable representation - */ - default: - gunichar u = gdk_keyval_to_unicode(keyval); - - if (u && g_unichar_to_utf8(u, NULL) == 1) { - gchar key; - - g_unichar_to_utf8(u, &key); - if (key > 0x7F) - break; - if (is_ctl) - key = CTL_KEY(g_ascii_toupper(key)); - - cmdline.keypress(key); - } - } - - /* - * The info area is updated very often and setting the - * window title each time it is updated is VERY costly. - * So we set it here once after every keypress even if the - * info line did not change. - * View changes are also only applied here to the GTK - * window even though GDK updates have been frozen since - * the size reallocations are very costly. - */ - gdk_threads_enter(); - - if (current_view != last_view) { - /* - * The last view's object is not guaranteed to - * still exist. - * However its widget is, due to reference counting. - */ - if (current_view_widget) - gtk_container_remove(GTK_CONTAINER(event_box_widget), - current_view_widget); - - current_view_widget = current_view->get_widget(); - - gtk_container_add(GTK_CONTAINER(event_box_widget), - current_view_widget); - gtk_widget_show(current_view_widget); - } - - gtk_window_set_title(GTK_WINDOW(window), info_current); - - gdk_window_thaw_updates(view_window); - - gdk_threads_leave(); -} - -InterfaceGtk::~InterfaceGtk() -{ - g_free(info_current); - if (popup_widget) - gtk_widget_destroy(popup_widget); - if (window) - gtk_widget_destroy(window); - - scintilla_release_resources(); - - if (event_queue) { - GdkEvent *e; - - while ((e = (GdkEvent *)g_async_queue_try_pop(event_queue))) - gdk_event_free(e); - - g_async_queue_unref(event_queue); - } -} - -/* - * GTK+ callbacks - */ - -static void -scintilla_notify(ScintillaObject *sci, uptr_t idFrom, - SCNotification *notify, gpointer user_data) -{ - interface.process_notify(notify); -} - -static gboolean -cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, - gpointer user_data) -{ - GAsyncQueue *event_queue = (GAsyncQueue *)user_data; - - bool is_ctl = event->state & GDK_CONTROL_MASK; - -#ifdef DEBUG - g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n", - event->string, *event->string, - event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK); -#endif - - g_async_queue_lock(event_queue); - - if (g_async_queue_length_unlocked(event_queue) >= 0 && - is_ctl && gdk_keyval_to_upper(event->keyval) == GDK_KEY_C) { - /* - * Handle asynchronous interruptions if CTRL+C is pressed. - * This will usually send SIGINT to the entire process - * group and set `sigint_occurred`. - * If the execution thread is currently blocking, - * the key is delivered like an ordinary key press. - */ - interrupt(); - } else { - /* - * Copies the key-press event, since it must be evaluated - * by the exec_thread_cb. This is costly, but since we're - * using the event queue as a kind of keyboard buffer, - * who cares? - */ - g_async_queue_push_unlocked(event_queue, - gdk_event_copy((GdkEvent *)event)); - } - - g_async_queue_unlock(event_queue); - - return TRUE; -} - -static gboolean -window_delete_cb(GtkWidget *w, GdkEventAny *e, gpointer user_data) -{ - GAsyncQueue *event_queue = (GAsyncQueue *)user_data; - GdkEventKey *close_event; - - /* - * Emulate that the "close" key was pressed - * which may then be handled by the execution thread - * which invokes the appropriate "function key macro" - * if it exists. Its default action will ensure that - * the execution thread shuts down and the main loop - * will eventually terminate. - */ - close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS); - close_event->window = gtk_widget_get_parent_window(w); - close_event->keyval = GDK_KEY_Close; - - g_async_queue_push(event_queue, close_event); - - return TRUE; -} - -static gboolean -sigterm_handler(gpointer user_data) -{ - GAsyncQueue *event_queue = (GAsyncQueue *)user_data; - GdkEventKey *close_event; - - /* - * Since this handler replaces the default one, we - * also have to make sure it interrupts. - */ - interrupt(); - - /* - * Similar to window deletion - emulate "close" key press. - */ - close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS); - close_event->keyval = GDK_KEY_Close; - - g_async_queue_push(event_queue, close_event); - - return G_SOURCE_CONTINUE; -} - -} /* namespace SciTECO */ diff --git a/src/interface-gtk.h b/src/interface-gtk.h deleted file mode 100644 index b5200ea..0000000 --- a/src/interface-gtk.h +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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 . - */ - -#ifndef __INTERFACE_GTK_H -#define __INTERFACE_GTK_H - -#include - -#include - -/* FIXME: see interface-gtk.cpp */ -#define GDK_DISABLE_DEPRECATION_WARNINGS -#include -#include - -#include -#include - -#include "interface.h" - -namespace SciTECO { - -typedef class ViewGtk : public View { - ScintillaObject *sci; - -public: - ViewGtk() : sci(NULL) {} - - /* implementation of View::initialize() */ - void initialize_impl(void); - - inline ~ViewGtk() - { - /* - * This does NOT destroy the Scintilla object - * and GTK widget, if it is the current view - * (and therefore added to the vbox). - */ - if (sci) - g_object_unref(G_OBJECT(sci)); - } - - inline GtkWidget * - get_widget(void) - { - return GTK_WIDGET(sci); - } - - /* implementation of View::ssm() */ - inline sptr_t - ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0) - { - sptr_t ret; - - gdk_threads_enter(); - ret = scintilla_send_message(sci, iMessage, wParam, lParam); - gdk_threads_leave(); - - return ret; - } -} ViewCurrent; - -typedef class InterfaceGtk : public Interface { - GtkWidget *window; - GtkWidget *vbox; - - GtkWidget *event_box_widget; - - gchar *info_current; - GtkWidget *info_widget; - - GtkWidget *message_widget; - GtkWidget *cmdline_widget; - - GtkWidget *popup_widget; - - GtkWidget *current_view_widget; - - GAsyncQueue *event_queue; - -public: - InterfaceGtk() : Interface(), - window(NULL), - vbox(NULL), - event_box_widget(NULL), - info_current(NULL), info_widget(NULL), - message_widget(NULL), - cmdline_widget(NULL), - popup_widget(NULL), - current_view_widget(NULL), - event_queue(NULL) {} - ~InterfaceGtk(); - - /* overrides Interface::get_options() */ - inline GOptionGroup * - get_options(void) - { - return gtk_get_option_group(TRUE); - } - - /* implementation of Interface::main() */ - void main_impl(int &argc, char **&argv); - - /* implementation of Interface::vmsg() */ - void vmsg_impl(MessageType type, const gchar *fmt, va_list ap); - /* overrides Interface::msg_clear() */ - void msg_clear(void); - - /* implementation of Interface::show_view() */ - void show_view_impl(ViewGtk *view); - - /* implementation of Interface::info_update() */ - void info_update_impl(const QRegister *reg); - void info_update_impl(const Buffer *buffer); - - /* implementation of Interface::cmdline_update() */ - void cmdline_update_impl(const Cmdline *cmdline); - - /* implementation of Interface::popup_add() */ - void popup_add_impl(PopupEntryType type, - const gchar *name, bool highlight = false); - /* implementation of Interface::popup_show() */ - void popup_show_impl(void); - - /* implementation of Interface::popup_is_shown() */ - inline bool - popup_is_shown_impl(void) - { - bool ret; - - gdk_threads_enter(); - ret = gtk_widget_get_visible(popup_widget); - gdk_threads_leave(); - - return ret; - } - /* implementation of Interface::popup_clear() */ - void popup_clear_impl(void); - - /* main entry point (implementation) */ - void event_loop_impl(void); - - /* - * FIXME: This is for internal use only and could be - * hidden in a nested forward-declared friend struct. - */ - void handle_key_press(bool is_shift, bool is_ctl, guint keyval); - -private: - static void widget_set_font(GtkWidget *widget, const gchar *font_name); - - void cmdline_insert_chr(gint &pos, gchar chr); -} InterfaceCurrent; - -} /* namespace SciTECO */ - -#endif diff --git a/src/interface-gtk/Makefile.am b/src/interface-gtk/Makefile.am new file mode 100644 index 0000000..64b4625 --- /dev/null +++ b/src/interface-gtk/Makefile.am @@ -0,0 +1,23 @@ +AM_CPPFLAGS += -I$(top_srcdir)/src + +AM_CFLAGS = -Wall -std=c99 +AM_CXXFLAGS = -Wall -Wno-char-subscripts +if CLANG +AM_CXXFLAGS += -Wno-mismatched-tags +endif + +EXTRA_DIST = gtk-info-popup.gob +BUILT_SOURCES = gtk-info-popup.c \ + gtk-info-popup.h gtk-info-popup-private.h + +noinst_LTLIBRARIES = libsciteco-interface.la +libsciteco_interface_la_SOURCES = interface-gtk.cpp interface-gtk.h +if GTK_FLOW_BOX_FALLBACK +libsciteco_interface_la_SOURCES += gtkflowbox.c gtkflowbox.h +endif +nodist_libsciteco_interface_la_SOURCES = gtk-info-popup.c + +CLEANFILES = $(BUILT_SOURCES) + +%.c %.h %-private.h : %.gob + @GOB2@ --gtk3 $< diff --git a/src/interface-gtk/gtk-info-popup.gob b/src/interface-gtk/gtk-info-popup.gob new file mode 100644 index 0000000..5082091 --- /dev/null +++ b/src/interface-gtk/gtk-info-popup.gob @@ -0,0 +1,269 @@ +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); + } +} diff --git a/src/interface-gtk/gtkflowbox.c b/src/interface-gtk/gtkflowbox.c new file mode 100644 index 0000000..1a5c2e9 --- /dev/null +++ b/src/interface-gtk/gtkflowbox.c @@ -0,0 +1,4795 @@ +/* + * Copyright (C) 2007-2010 Openismus GmbH + * Copyright (C) 2013 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, see . + * + * Authors: + * Tristan Van Berkom + * Matthias Clasen + * William Jon McCann + */ + +/* Preamble {{{1 */ + +/** + * SECTION:gtkflowbox + * @Short_Description: A container that allows reflowing its children + * @Title: GtkFlowBox + * + * A GtkFlowBox positions child widgets in sequence according to its + * orientation. + * + * For instance, with the horizontal orientation, the widgets will be + * arranged from left to right, starting a new row under the previous + * row when necessary. Reducing the width in this case will require more + * rows, so a larger height will be requested. + * + * Likewise, with the vertical orientation, the widgets will be arranged + * from top to bottom, starting a new column to the right when necessary. + * Reducing the height will require more columns, so a larger width will + * be requested. + * + * The children of a GtkFlowBox can be dynamically sorted and filtered. + * + * Although a GtkFlowBox must have only #GtkFlowBoxChild children, + * you can add any kind of widget to it via gtk_container_add(), and + * a GtkFlowBoxChild widget will automatically be inserted between + * the box and the widget. + * + * Also see #GtkListBox. + * + * GtkFlowBox was added in GTK+ 3.12. + */ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "gtkflowbox.h" + +/* Forward declarations and utilities {{{1 */ + +static void gtk_flow_box_update_cursor (GtkFlowBox *box, + GtkFlowBoxChild *child); +static void gtk_flow_box_select_and_activate (GtkFlowBox *box, + GtkFlowBoxChild *child); +static void gtk_flow_box_update_selection (GtkFlowBox *box, + GtkFlowBoxChild *child, + gboolean modify, + gboolean extend); +static void gtk_flow_box_apply_filter (GtkFlowBox *box, + GtkFlowBoxChild *child); +static void gtk_flow_box_apply_sort (GtkFlowBox *box, + GtkFlowBoxChild *child); +static gint gtk_flow_box_sort (GtkFlowBoxChild *a, + GtkFlowBoxChild *b, + GtkFlowBox *box); + +static void +get_current_selection_modifiers (GtkWidget *widget, + gboolean *modify, + gboolean *extend) +{ + GdkModifierType state = 0; + GdkModifierType mask; + + *modify = FALSE; + *extend = FALSE; + + if (gtk_get_current_event_state (&state)) + { + mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_MODIFY_SELECTION); + if ((state & mask) == mask) + *modify = TRUE; + mask = gtk_widget_get_modifier_mask (widget, GDK_MODIFIER_INTENT_EXTEND_SELECTION); + if ((state & mask) == mask) + *extend = TRUE; + } +} + +static void +path_from_horizontal_line_rects (cairo_t *cr, + GdkRectangle *lines, + gint n_lines) +{ + gint start_line, end_line; + GdkRectangle *r; + gint i; + + /* Join rows vertically by extending to the middle */ + for (i = 0; i < n_lines - 1; i++) + { + GdkRectangle *r1 = &lines[i]; + GdkRectangle *r2 = &lines[i+1]; + gint gap, old; + + gap = r2->y - (r1->y + r1->height); + r1->height += gap / 2; + old = r2->y; + r2->y = r1->y + r1->height; + r2->height += old - r2->y; + } + + cairo_new_path (cr); + start_line = 0; + + do + { + for (i = start_line; i < n_lines; i++) + { + r = &lines[i]; + if (i == start_line) + cairo_move_to (cr, r->x + r->width, r->y); + else + cairo_line_to (cr, r->x + r->width, r->y); + cairo_line_to (cr, r->x + r->width, r->y + r->height); + + if (i < n_lines - 1 && + (r->x + r->width < lines[i+1].x || + r->x > lines[i+1].x + lines[i+1].width)) + { + i++; + break; + } + } + end_line = i; + for (i = end_line - 1; i >= start_line; i--) + { + r = &lines[i]; + cairo_line_to (cr, r->x, r->y + r->height); + cairo_line_to (cr, r->x, r->y); + } + cairo_close_path (cr); + start_line = end_line; + } + while (end_line < n_lines); +} + +static void +path_from_vertical_line_rects (cairo_t *cr, + GdkRectangle *lines, + gint n_lines) +{ + gint start_line, end_line; + GdkRectangle *r; + gint i; + + /* Join rows horizontally by extending to the middle */ + for (i = 0; i < n_lines - 1; i++) + { + GdkRectangle *r1 = &lines[i]; + GdkRectangle *r2 = &lines[i+1]; + gint gap, old; + + gap = r2->x - (r1->x + r1->width); + r1->width += gap / 2; + old = r2->x; + r2->x = r1->x + r1->width; + r2->width += old - r2->x; + } + + cairo_new_path (cr); + start_line = 0; + + do + { + for (i = start_line; i < n_lines; i++) + { + r = &lines[i]; + if (i == start_line) + cairo_move_to (cr, r->x, r->y + r->height); + else + cairo_line_to (cr, r->x, r->y + r->height); + cairo_line_to (cr, r->x + r->width, r->y + r->height); + + if (i < n_lines - 1 && + (r->y + r->height < lines[i+1].y || + r->y > lines[i+1].y + lines[i+1].height)) + { + i++; + break; + } + } + end_line = i; + for (i = end_line - 1; i >= start_line; i--) + { + r = &lines[i]; + cairo_line_to (cr, r->x + r->width, r->y); + cairo_line_to (cr, r->x, r->y); + } + cairo_close_path (cr); + start_line = end_line; + } + while (end_line < n_lines); +} + +/* GtkFlowBoxChild {{{1 */ + +/* GObject boilerplate {{{2 */ + +enum { + CHILD_ACTIVATE, + CHILD_LAST_SIGNAL +}; + +static guint child_signals[CHILD_LAST_SIGNAL] = { 0 }; + +typedef struct _GtkFlowBoxChildPrivate GtkFlowBoxChildPrivate; +struct _GtkFlowBoxChildPrivate +{ + GSequenceIter *iter; + gboolean selected; +}; + +#define CHILD_PRIV(child) ((GtkFlowBoxChildPrivate*)gtk_flow_box_child_get_instance_private ((GtkFlowBoxChild*)(child))) + +G_DEFINE_TYPE_WITH_PRIVATE (GtkFlowBoxChild, gtk_flow_box_child, GTK_TYPE_BIN) + +/* Internal API {{{2 */ + +static GtkFlowBox * +gtk_flow_box_child_get_box (GtkFlowBoxChild *child) +{ + GtkWidget *parent; + + parent = gtk_widget_get_parent (GTK_WIDGET (child)); + if (parent && GTK_IS_FLOW_BOX (parent)) + return GTK_FLOW_BOX (parent); + + return NULL; +} + +static void +gtk_flow_box_child_set_focus (GtkFlowBoxChild *child) +{ + GtkFlowBox *box = gtk_flow_box_child_get_box (child); + gboolean modify; + gboolean extend; + + get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend); + + if (modify) + gtk_flow_box_update_cursor (box, child); + else + gtk_flow_box_update_selection (box, child, FALSE, FALSE); +} + +/* GtkWidget implementation {{{2 */ + +static gboolean +gtk_flow_box_child_focus (GtkWidget *widget, + GtkDirectionType direction) +{ + gboolean had_focus = FALSE; + GtkWidget *child; + + child = gtk_bin_get_child (GTK_BIN (widget)); + + g_object_get (widget, "has-focus", &had_focus, NULL); + if (had_focus) + { + /* If on row, going right, enter into possible container */ + if (child && + (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD)) + { + if (gtk_widget_child_focus (GTK_WIDGET (child), direction)) + return TRUE; + } + + return FALSE; + } + else if (gtk_container_get_focus_child (GTK_CONTAINER (widget)) != NULL) + { + /* Child has focus, always navigate inside it first */ + if (gtk_widget_child_focus (child, direction)) + return TRUE; + + /* If exiting child container to the left, select child */ + if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD) + { + gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget)); + return TRUE; + } + + return FALSE; + } + else + { + /* If coming from the left, enter into possible container */ + if (child && + (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD)) + { + if (gtk_widget_child_focus (child, direction)) + return TRUE; + } + + gtk_flow_box_child_set_focus (GTK_FLOW_BOX_CHILD (widget)); + return TRUE; + } +} + +static void +gtk_flow_box_child_activate (GtkFlowBoxChild *child) +{ + GtkFlowBox *box; + + box = gtk_flow_box_child_get_box (child); + if (box) + gtk_flow_box_select_and_activate (box, child); +} + +static gboolean +gtk_flow_box_child_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkAllocation allocation = {0}; + GtkStyleContext* context; + GtkStateFlags state; + GtkBorder border; + gint focus_pad; + + gtk_widget_get_allocation (widget, &allocation); + context = gtk_widget_get_style_context (widget); + state = gtk_widget_get_state_flags (widget); + + gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height); + gtk_render_frame (context, cr, 0, 0, allocation.width, allocation.height); + + if (gtk_widget_has_visible_focus (widget)) + { + gtk_style_context_get_border (context, state, &border); + + gtk_style_context_get_style (context, + "focus-padding", &focus_pad, + NULL); + gtk_render_focus (context, cr, border.left + focus_pad, border.top + focus_pad, + allocation.width - 2 * focus_pad - border.left - border.right, + allocation.height - 2 * focus_pad - border.top - border.bottom); + } + + GTK_WIDGET_CLASS (gtk_flow_box_child_parent_class)->draw (widget, cr); + + return TRUE; +} + +/* Size allocation {{{3 */ + +static void +gtk_flow_box_child_get_full_border (GtkFlowBoxChild *child, + GtkBorder *full_border) +{ + GtkWidget *widget = GTK_WIDGET (child); + GtkStyleContext *context; + GtkStateFlags state; + GtkBorder padding, border; + int focus_width, focus_pad; + + context = gtk_widget_get_style_context (widget); + state = gtk_style_context_get_state (context); + + gtk_style_context_get_padding (context, state, &padding); + gtk_style_context_get_border (context, state, &border); + gtk_style_context_get_style (context, + "focus-line-width", &focus_width, + "focus-padding", &focus_pad, + NULL); + + full_border->left = padding.left + border.left + focus_width + focus_pad; + full_border->right = padding.right + border.right + focus_width + focus_pad; + full_border->top = padding.top + border.top + focus_width + focus_pad; + full_border->bottom = padding.bottom + border.bottom + focus_width + focus_pad; +} + +static GtkSizeRequestMode +gtk_flow_box_child_get_request_mode (GtkWidget *widget) +{ + GtkFlowBox *box; + + box = gtk_flow_box_child_get_box (GTK_FLOW_BOX_CHILD (widget)); + if (box) + return gtk_widget_get_request_mode (GTK_WIDGET (box)); + else + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width); +static void gtk_flow_box_child_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height); + +static void +gtk_flow_box_child_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum_height, + gint *natural_height) +{ + if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) + { + GtkWidget *child; + gint child_min = 0, child_natural = 0; + GtkBorder full_border = { 0, }; + + gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); + child = gtk_bin_get_child (GTK_BIN (widget)); + if (child && gtk_widget_get_visible (child)) + gtk_widget_get_preferred_height_for_width (child, width - full_border.left - full_border.right, + &child_min, &child_natural); + + if (minimum_height) + *minimum_height = full_border.top + child_min + full_border.bottom; + if (natural_height) + *natural_height = full_border.top + child_natural + full_border.bottom; + } + else + { + gtk_flow_box_child_get_preferred_height (widget, minimum_height, natural_height); + } +} + +static void +gtk_flow_box_child_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) + { + GtkWidget *child; + gint child_min = 0, child_natural = 0; + GtkBorder full_border = { 0, }; + + gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); + child = gtk_bin_get_child (GTK_BIN (widget)); + if (child && gtk_widget_get_visible (child)) + gtk_widget_get_preferred_width (child, &child_min, &child_natural); + + if (minimum_width) + *minimum_width = full_border.left + child_min + full_border.right; + if (natural_width) + *natural_width = full_border.left + child_natural + full_border.right; + } + else + { + gint natural_height; + + gtk_flow_box_child_get_preferred_height (widget, NULL, &natural_height); + gtk_flow_box_child_get_preferred_width_for_height (widget, natural_height, + minimum_width, natural_width); + } +} + +static void +gtk_flow_box_child_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width) +{ + if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) + { + gtk_flow_box_child_get_preferred_width (widget, minimum_width, natural_width); + } + else + { + GtkWidget *child; + gint child_min = 0, child_natural = 0; + GtkBorder full_border = { 0, }; + + gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); + child = gtk_bin_get_child (GTK_BIN (widget)); + if (child && gtk_widget_get_visible (child)) + gtk_widget_get_preferred_width_for_height (child, height - full_border.top - full_border.bottom, + &child_min, &child_natural); + + if (minimum_width) + *minimum_width = full_border.left + child_min + full_border.right; + if (natural_width) + *natural_width = full_border.left + child_natural + full_border.right; + } +} + +static void +gtk_flow_box_child_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + if (gtk_flow_box_child_get_request_mode (widget) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) + { + gint natural_width; + + gtk_flow_box_child_get_preferred_width (widget, NULL, &natural_width); + gtk_flow_box_child_get_preferred_height_for_width (widget, natural_width, + minimum_height, natural_height); + } + else + { + GtkWidget *child; + gint child_min = 0, child_natural = 0; + GtkBorder full_border = { 0, }; + + gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &full_border); + child = gtk_bin_get_child (GTK_BIN (widget)); + if (child && gtk_widget_get_visible (child)) + gtk_widget_get_preferred_height (child, &child_min, &child_natural); + + if (minimum_height) + *minimum_height = full_border.top + child_min + full_border.bottom; + if (natural_height) + *natural_height = full_border.top + child_natural + full_border.bottom; + } +} + +static void +gtk_flow_box_child_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GtkWidget *child; + + gtk_widget_set_allocation (widget, allocation); + + child = gtk_bin_get_child (GTK_BIN (widget)); + if (child && gtk_widget_get_visible (child)) + { + GtkAllocation child_allocation; + GtkBorder border = { 0, }; + + gtk_flow_box_child_get_full_border (GTK_FLOW_BOX_CHILD (widget), &border); + + child_allocation.x = allocation->x + border.left; + child_allocation.y = allocation->y + border.top; + child_allocation.width = allocation->width - border.left - border.right; + child_allocation.height = allocation->height - border.top - border.bottom; + + child_allocation.width = MAX (1, child_allocation.width); + child_allocation.height = MAX (1, child_allocation.height); + + gtk_widget_size_allocate (child, &child_allocation); + } +} + +/* GObject implementation {{{2 */ + +static void +gtk_flow_box_child_class_init (GtkFlowBoxChildClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + widget_class->draw = gtk_flow_box_child_draw; + widget_class->get_request_mode = gtk_flow_box_child_get_request_mode; + widget_class->get_preferred_height = gtk_flow_box_child_get_preferred_height; + widget_class->get_preferred_height_for_width = gtk_flow_box_child_get_preferred_height_for_width; + widget_class->get_preferred_width = gtk_flow_box_child_get_preferred_width; + widget_class->get_preferred_width_for_height = gtk_flow_box_child_get_preferred_width_for_height; + widget_class->size_allocate = gtk_flow_box_child_size_allocate; + widget_class->focus = gtk_flow_box_child_focus; + + class->activate = gtk_flow_box_child_activate; + + /** + * GtkFlowBoxChild::activate: + * @child: The child on which the signal is emitted + * + * The ::activate signal is emitted when the user activates + * a child widget in a #GtkFlowBox, either by clicking or + * double-clicking, or by using the Space or Enter key. + * + * While this signal is used as a + * [keybinding signal][GtkBindingSignal], + * it can be used by applications for their own purposes. + */ + child_signals[CHILD_ACTIVATE] = + g_signal_new ("activate", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (GtkFlowBoxChildClass, activate), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + widget_class->activate_signal = child_signals[CHILD_ACTIVATE]; + + gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_LIST_ITEM); +} + +static void +gtk_flow_box_child_init (GtkFlowBoxChild *child) +{ + GtkStyleContext *context; + + gtk_widget_set_can_focus (GTK_WIDGET (child), TRUE); + gtk_widget_set_redraw_on_allocate (GTK_WIDGET (child), TRUE); + + context = gtk_widget_get_style_context (GTK_WIDGET (child)); + gtk_style_context_add_class (context, "grid-child"); +} + +/* Public API {{{2 */ + +/** + * gtk_flow_box_child_new: + * + * Creates a new #GtkFlowBoxChild, to be used as a child + * of a #GtkFlowBox. + * + * Returns: a new #GtkFlowBoxChild + * + * Since: 3.12 + */ +GtkWidget * +gtk_flow_box_child_new (void) +{ + return g_object_new (GTK_TYPE_FLOW_BOX_CHILD, NULL); +} + +/** + * gtk_flow_box_child_get_index: + * @child: a #GtkFlowBoxChild + * + * Gets the current index of the @child in its #GtkFlowBox container. + * + * Returns: the index of the @child, or -1 if the @child is not + * in a flow box. + * + * Since: 3.12 + */ +gint +gtk_flow_box_child_get_index (GtkFlowBoxChild *child) +{ + GtkFlowBoxChildPrivate *priv; + + g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), -1); + + priv = CHILD_PRIV (child); + + if (priv->iter != NULL) + return g_sequence_iter_get_position (priv->iter); + + return -1; +} + +/** + * gtk_flow_box_child_is_selected: + * @child: a #GtkFlowBoxChild + * + * Returns whether the @child is currently selected in its + * #GtkFlowBox container. + * + * Returns: %TRUE if @child is selected + * + * Since: 3.12 + */ +gboolean +gtk_flow_box_child_is_selected (GtkFlowBoxChild *child) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX_CHILD (child), FALSE); + + return CHILD_PRIV (child)->selected; +} + +/** + * gtk_flow_box_child_changed: + * @child: a #GtkFlowBoxChild + * + * Marks @child as changed, causing any state that depends on this + * to be updated. This affects sorting and filtering. + * + * Note that calls to this method must be in sync with the data + * used for the sorting and filtering functions. For instance, if + * the list is mirroring some external data set, and *two* children + * changed in the external data set when you call + * gtk_flow_box_child_changed() on the first child, the sort function + * must only read the new data for the first of the two changed + * children, otherwise the resorting of the children will be wrong. + * + * This generally means that if you don’t fully control the data + * model, you have to duplicate the data that affects the sorting + * and filtering functions into the widgets themselves. Another + * alternative is to call gtk_flow_box_invalidate_sort() on any + * model change, but that is more expensive. + * + * Since: 3.12 + */ +void +gtk_flow_box_child_changed (GtkFlowBoxChild *child) +{ + GtkFlowBox *box; + + g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); + + box = gtk_flow_box_child_get_box (child); + + if (box == NULL) + return; + + gtk_flow_box_apply_sort (box, child); + gtk_flow_box_apply_filter (box, child); +} + +/* GtkFlowBox {{{1 */ + +/* Constants {{{2 */ + +#define DEFAULT_MAX_CHILDREN_PER_LINE 7 +#define RUBBERBAND_START_DISTANCE 32 +#define AUTOSCROLL_FAST_DISTANCE 32 +#define AUTOSCROLL_FACTOR 20 +#define AUTOSCROLL_FACTOR_FAST 10 + +/* GObject boilerplate {{{2 */ + +enum { + CHILD_ACTIVATED, + SELECTED_CHILDREN_CHANGED, + ACTIVATE_CURSOR_CHILD, + TOGGLE_CURSOR_CHILD, + MOVE_CURSOR, + SELECT_ALL, + UNSELECT_ALL, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +enum { + PROP_0, + PROP_ORIENTATION, + PROP_HOMOGENEOUS, + PROP_HALIGN_POLICY, + PROP_VALIGN_POLICY, + PROP_COLUMN_SPACING, + PROP_ROW_SPACING, + PROP_MIN_CHILDREN_PER_LINE, + PROP_MAX_CHILDREN_PER_LINE, + PROP_SELECTION_MODE, + PROP_ACTIVATE_ON_SINGLE_CLICK +}; + +typedef struct _GtkFlowBoxPrivate GtkFlowBoxPrivate; +struct _GtkFlowBoxPrivate { + GtkOrientation orientation; + gboolean homogeneous; + + guint row_spacing; + guint column_spacing; + + GtkFlowBoxChild *prelight_child; + GtkFlowBoxChild *cursor_child; + GtkFlowBoxChild *selected_child; + + gboolean active_child_active; + GtkFlowBoxChild *active_child; + + GtkSelectionMode selection_mode; + + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; + gboolean activate_on_single_click; + + guint16 min_children_per_line; + guint16 max_children_per_line; + guint16 cur_children_per_line; + + GSequence *children; + + GtkFlowBoxFilterFunc filter_func; + gpointer filter_data; + GDestroyNotify filter_destroy; + + GtkFlowBoxSortFunc sort_func; + gpointer sort_data; + GDestroyNotify sort_destroy; + + gboolean track_motion; + gboolean rubberband_select; + GtkFlowBoxChild *rubberband_first; + GtkFlowBoxChild *rubberband_last; + gint button_down_x; + gint button_down_y; + gboolean rubberband_modify; + gboolean rubberband_extend; + GdkDevice *rubberband_device; + + GtkScrollType autoscroll_mode; + guint autoscroll_id; +}; + +#define BOX_PRIV(box) ((GtkFlowBoxPrivate*)gtk_flow_box_get_instance_private ((GtkFlowBox*)(box))) + +G_DEFINE_TYPE_WITH_CODE (GtkFlowBox, gtk_flow_box, GTK_TYPE_CONTAINER, + G_ADD_PRIVATE (GtkFlowBox) + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +/* Internal API, utilities {{{2 */ + +#define ORIENTATION_ALIGN(box) \ + (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \ + ? gtk_widget_get_halign (GTK_WIDGET (box)) \ + : gtk_widget_get_valign (GTK_WIDGET (box))) + +#define OPPOSING_ORIENTATION_ALIGN(box) \ + (BOX_PRIV(box)->orientation == GTK_ORIENTATION_HORIZONTAL \ + ? gtk_widget_get_valign (GTK_WIDGET (box)) \ + : gtk_widget_get_halign (GTK_WIDGET (box))) + +/* Children are visible if they are shown by the app (visible) + * and not filtered out (child_visible) by the box + */ +static inline gboolean +child_is_visible (GtkWidget *child) +{ + return gtk_widget_get_visible (child) && + gtk_widget_get_child_visible (child); +} + +static gint +get_visible_children (GtkFlowBox *box) +{ + GSequenceIter *iter; + gint i = 0; + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + + child = g_sequence_get (iter); + if (child_is_visible (child)) + i++; + } + + return i; +} + +static GtkFlowBoxChild * +gtk_flow_box_find_child_at_pos (GtkFlowBox *box, + gint x, + gint y) +{ + GtkWidget *child; + GSequenceIter *iter; + GtkAllocation allocation; + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + child = g_sequence_get (iter); + if (!child_is_visible (child)) + continue; + gtk_widget_get_allocation (child, &allocation); + if (x >= allocation.x && x < (allocation.x + allocation.width) && + y >= allocation.y && y < (allocation.y + allocation.height)) + return GTK_FLOW_BOX_CHILD (child); + } + + return NULL; +} + +static void +gtk_flow_box_update_prelight (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + if (child != priv->prelight_child) + { + priv->prelight_child = child; + gtk_widget_queue_draw (GTK_WIDGET (box)); + } +} + +static void +gtk_flow_box_update_active (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gboolean val; + + val = priv->active_child == child; + if (priv->active_child != NULL && + val != priv->active_child_active) + { + priv->active_child_active = val; + gtk_widget_queue_draw (GTK_WIDGET (box)); + } +} + +static void +gtk_flow_box_apply_filter (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gboolean do_show; + + do_show = TRUE; + if (priv->filter_func != NULL) + do_show = priv->filter_func (child, priv->filter_data); + + gtk_widget_set_child_visible (GTK_WIDGET (child), do_show); +} + +static void +gtk_flow_box_apply_filter_all (GtkFlowBox *box) +{ + GSequenceIter *iter; + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkFlowBoxChild *child; + + child = g_sequence_get (iter); + gtk_flow_box_apply_filter (box, child); + } + gtk_widget_queue_resize (GTK_WIDGET (box)); +} + +static void +gtk_flow_box_apply_sort (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + if (BOX_PRIV (box)->sort_func != NULL) + { + g_sequence_sort_changed (CHILD_PRIV (child)->iter, + (GCompareDataFunc)gtk_flow_box_sort, box); + gtk_widget_queue_resize (GTK_WIDGET (box)); + } +} + +/* Selection utilities {{{3 */ + +static gboolean +gtk_flow_box_child_set_selected (GtkFlowBoxChild *child, + gboolean selected) +{ + if (CHILD_PRIV (child)->selected != selected) + { + CHILD_PRIV (child)->selected = selected; + if (selected) + gtk_widget_set_state_flags (GTK_WIDGET (child), + GTK_STATE_FLAG_SELECTED, FALSE); + else + gtk_widget_unset_state_flags (GTK_WIDGET (child), + GTK_STATE_FLAG_SELECTED); + + gtk_widget_queue_draw (GTK_WIDGET (child)); + + return TRUE; + } + + return FALSE; +} + +static gboolean +gtk_flow_box_unselect_all_internal (GtkFlowBox *box) +{ + GtkFlowBoxChild *child; + GSequenceIter *iter; + gboolean dirty = FALSE; + + if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) + return FALSE; + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + child = g_sequence_get (iter); + dirty |= gtk_flow_box_child_set_selected (child, FALSE); + } + + return dirty; +} + +static void +gtk_flow_box_unselect_child_internal (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + if (!CHILD_PRIV (child)->selected) + return; + + if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) + return; + else if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) + gtk_flow_box_unselect_all_internal (box); + else + gtk_flow_box_child_set_selected (child, FALSE); + + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); +} + +static void +gtk_flow_box_update_cursor (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + BOX_PRIV (box)->cursor_child = child; + gtk_widget_grab_focus (GTK_WIDGET (child)); + gtk_widget_queue_draw (GTK_WIDGET (child)); +} + +static void +gtk_flow_box_select_child_internal (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + if (CHILD_PRIV (child)->selected) + return; + + if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_NONE) + return; + if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) + gtk_flow_box_unselect_all_internal (box); + + gtk_flow_box_child_set_selected (child, TRUE); + BOX_PRIV (box)->selected_child = child; + + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); +} + +static void +gtk_flow_box_select_all_between (GtkFlowBox *box, + GtkFlowBoxChild *child1, + GtkFlowBoxChild *child2, + gboolean modify) +{ + GSequenceIter *iter, *iter1, *iter2; + + if (child1) + iter1 = CHILD_PRIV (child1)->iter; + else + iter1 = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + + if (child2) + iter2 = CHILD_PRIV (child2)->iter; + else + iter2 = g_sequence_get_end_iter (BOX_PRIV (box)->children); + + if (g_sequence_iter_compare (iter2, iter1) < 0) + { + iter = iter1; + iter1 = iter2; + iter2 = iter; + } + + for (iter = iter1; + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + + child = g_sequence_get (iter); + if (child_is_visible (child)) + { + if (modify) + gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), !CHILD_PRIV (child)->selected); + else + gtk_flow_box_child_set_selected (GTK_FLOW_BOX_CHILD (child), TRUE); + } + + if (g_sequence_iter_compare (iter, iter2) == 0) + break; + } +} + +static void +gtk_flow_box_update_selection (GtkFlowBox *box, + GtkFlowBoxChild *child, + gboolean modify, + gboolean extend) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + gtk_flow_box_update_cursor (box, child); + + if (priv->selection_mode == GTK_SELECTION_NONE) + return; + + if (priv->selection_mode == GTK_SELECTION_BROWSE) + { + gtk_flow_box_unselect_all_internal (box); + gtk_flow_box_child_set_selected (child, TRUE); + priv->selected_child = child; + } + else if (priv->selection_mode == GTK_SELECTION_SINGLE) + { + gboolean was_selected; + + was_selected = CHILD_PRIV (child)->selected; + gtk_flow_box_unselect_all_internal (box); + gtk_flow_box_child_set_selected (child, modify ? !was_selected : TRUE); + priv->selected_child = CHILD_PRIV (child)->selected ? child : NULL; + } + else /* GTK_SELECTION_MULTIPLE */ + { + if (extend) + { + gtk_flow_box_unselect_all_internal (box); + if (priv->selected_child == NULL) + { + gtk_flow_box_child_set_selected (child, TRUE); + priv->selected_child = child; + } + else + gtk_flow_box_select_all_between (box, priv->selected_child, child, FALSE); + } + else + { + if (modify) + { + gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected); + } + else + { + gtk_flow_box_unselect_all_internal (box); + gtk_flow_box_child_set_selected (child, !CHILD_PRIV (child)->selected); + priv->selected_child = child; + } + } + } + + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); +} + +static void +gtk_flow_box_select_and_activate (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + if (child != NULL) + { + gtk_flow_box_select_child_internal (box, child); + gtk_flow_box_update_cursor (box, child); + g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child); + } +} + +/* Focus utilities {{{3 */ + +static GSequenceIter * +gtk_flow_box_get_previous_focusable (GtkFlowBox *box, + GSequenceIter *iter) +{ + GtkFlowBoxChild *child; + + while (!g_sequence_iter_is_begin (iter)) + { + iter = g_sequence_iter_prev (iter); + child = g_sequence_get (iter); + if (child_is_visible (GTK_WIDGET (child)) && + gtk_widget_is_sensitive (GTK_WIDGET (child))) + return iter; + } + + return NULL; +} + +static GSequenceIter * +gtk_flow_box_get_next_focusable (GtkFlowBox *box, + GSequenceIter *iter) +{ + GtkFlowBoxChild *child; + + while (TRUE) + { + iter = g_sequence_iter_next (iter); + if (g_sequence_iter_is_end (iter)) + return NULL; + child = g_sequence_get (iter); + if (child_is_visible (GTK_WIDGET (child)) && + gtk_widget_is_sensitive (GTK_WIDGET (child))) + return iter; + } + + return NULL; +} + +static GSequenceIter * +gtk_flow_box_get_first_focusable (GtkFlowBox *box) +{ + GSequenceIter *iter; + GtkFlowBoxChild *child; + + iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + if (g_sequence_iter_is_end (iter)) + return NULL; + + child = g_sequence_get (iter); + if (child_is_visible (GTK_WIDGET (child)) && + gtk_widget_is_sensitive (GTK_WIDGET (child))) + return iter; + + return gtk_flow_box_get_next_focusable (box, iter); +} + +static GSequenceIter * +gtk_flow_box_get_last_focusable (GtkFlowBox *box) +{ + GSequenceIter *iter; + + iter = g_sequence_get_end_iter (BOX_PRIV (box)->children); + return gtk_flow_box_get_previous_focusable (box, iter); +} + + +static GSequenceIter * +gtk_flow_box_get_above_focusable (GtkFlowBox *box, + GSequenceIter *iter) +{ + GtkFlowBoxChild *child = NULL; + gint i; + + while (TRUE) + { + i = 0; + while (i < BOX_PRIV (box)->cur_children_per_line) + { + if (g_sequence_iter_is_begin (iter)) + return NULL; + iter = g_sequence_iter_prev (iter); + child = g_sequence_get (iter); + if (child_is_visible (GTK_WIDGET (child))) + i++; + } + if (child && gtk_widget_get_sensitive (GTK_WIDGET (child))) + return iter; + } + + return NULL; +} + +static GSequenceIter * +gtk_flow_box_get_below_focusable (GtkFlowBox *box, + GSequenceIter *iter) +{ + GtkFlowBoxChild *child = NULL; + gint i; + + while (TRUE) + { + i = 0; + while (i < BOX_PRIV (box)->cur_children_per_line) + { + iter = g_sequence_iter_next (iter); + if (g_sequence_iter_is_end (iter)) + return NULL; + child = g_sequence_get (iter); + if (child_is_visible (GTK_WIDGET (child))) + i++; + } + if (child && gtk_widget_get_sensitive (GTK_WIDGET (child))) + return iter; + } + + return NULL; +} + +/* GtkWidget implementation {{{2 */ + +/* Size allocation {{{3 */ + +/* Used in columned modes where all items share at least their + * equal widths or heights + */ +static void +get_max_item_size (GtkFlowBox *box, + GtkOrientation orientation, + gint *min_size, + gint *nat_size) +{ + GSequenceIter *iter; + gint max_min_size = 0; + gint max_nat_size = 0; + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + gint child_min, child_nat; + + child = g_sequence_get (iter); + + if (!child_is_visible (child)) + continue; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_width (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_height (child, &child_min, &child_nat); + + max_min_size = MAX (max_min_size, child_min); + max_nat_size = MAX (max_nat_size, child_nat); + } + + if (min_size) + *min_size = max_min_size; + + if (nat_size) + *nat_size = max_nat_size; +} + + +/* Gets the largest minimum/natural size for a given size (used to get + * the largest item heights for a fixed item width and the opposite) + */ +static void +get_largest_size_for_opposing_orientation (GtkFlowBox *box, + GtkOrientation orientation, + gint item_size, + gint *min_item_size, + gint *nat_item_size) +{ + GSequenceIter *iter; + gint max_min_size = 0; + gint max_nat_size = 0; + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + gint child_min, child_nat; + + child = g_sequence_get (iter); + + if (!child_is_visible (child)) + continue; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_height_for_width (child, + item_size, + &child_min, &child_nat); + else + gtk_widget_get_preferred_width_for_height (child, + item_size, + &child_min, &child_nat); + + max_min_size = MAX (max_min_size, child_min); + max_nat_size = MAX (max_nat_size, child_nat); + } + + if (min_item_size) + *min_item_size = max_min_size; + + if (nat_item_size) + *nat_item_size = max_nat_size; +} + +/* Gets the largest minimum/natural size on a single line for a given size + * (used to get the largest line heights for a fixed item width and the opposite + * while iterating over a list of children, note the new index is returned) + */ +static GSequenceIter * +get_largest_size_for_line_in_opposing_orientation (GtkFlowBox *box, + GtkOrientation orientation, + GSequenceIter *cursor, + gint line_length, + GtkRequestedSize *item_sizes, + gint extra_pixels, + gint *min_item_size, + gint *nat_item_size) +{ + GSequenceIter *iter; + gint max_min_size = 0; + gint max_nat_size = 0; + gint i; + + i = 0; + for (iter = cursor; + !g_sequence_iter_is_end (iter) && i < line_length; + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + gint child_min, child_nat, this_item_size; + + child = g_sequence_get (iter); + + if (!child_is_visible (child)) + continue; + + /* Distribute the extra pixels to the first children in the line + * (could be fancier and spread them out more evenly) */ + this_item_size = item_sizes[i].minimum_size; + if (extra_pixels > 0 && ORIENTATION_ALIGN (box) == GTK_ALIGN_FILL) + { + this_item_size++; + extra_pixels--; + } + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_height_for_width (child, + this_item_size, + &child_min, &child_nat); + else + gtk_widget_get_preferred_width_for_height (child, + this_item_size, + &child_min, &child_nat); + + max_min_size = MAX (max_min_size, child_min); + max_nat_size = MAX (max_nat_size, child_nat); + + i++; + } + + if (min_item_size) + *min_item_size = max_min_size; + + if (nat_item_size) + *nat_item_size = max_nat_size; + + /* Return next item in the list */ + return iter; +} + +/* fit_aligned_item_requests() helper */ +static gint +gather_aligned_item_requests (GtkFlowBox *box, + GtkOrientation orientation, + gint line_length, + gint item_spacing, + gint n_children, + GtkRequestedSize *item_sizes) +{ + GSequenceIter *iter; + gint i; + gint extra_items, natural_line_size = 0; + + extra_items = n_children % line_length; + + i = 0; + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + GtkAlign item_align; + gint child_min, child_nat; + gint position; + + child = g_sequence_get (iter); + + if (!child_is_visible (child)) + continue; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_width (child, + &child_min, &child_nat); + else + gtk_widget_get_preferred_height (child, + &child_min, &child_nat); + + /* Get the index and push it over for the last line when spreading to the end */ + position = i % line_length; + + item_align = ORIENTATION_ALIGN (box); + if (item_align == GTK_ALIGN_END && i >= n_children - extra_items) + position += line_length - extra_items; + + /* Round up the size of every column/row */ + item_sizes[position].minimum_size = MAX (item_sizes[position].minimum_size, child_min); + item_sizes[position].natural_size = MAX (item_sizes[position].natural_size, child_nat); + + i++; + } + + for (i = 0; i < line_length; i++) + natural_line_size += item_sizes[i].natural_size; + + natural_line_size += (line_length - 1) * item_spacing; + + return natural_line_size; +} + +static GtkRequestedSize * +fit_aligned_item_requests (GtkFlowBox *box, + GtkOrientation orientation, + gint avail_size, + gint item_spacing, + gint *line_length, /* in-out */ + gint items_per_line, + gint n_children) +{ + GtkRequestedSize *sizes, *try_sizes; + gint try_line_size, try_length; + + sizes = g_new0 (GtkRequestedSize, *line_length); + + /* get the sizes for the initial guess */ + try_line_size = gather_aligned_item_requests (box, + orientation, + *line_length, + item_spacing, + n_children, + sizes); + + /* Try columnizing the whole thing and adding an item to the end of + * the line; try to fit as many columns into the available size as + * possible + */ + for (try_length = *line_length + 1; try_line_size < avail_size; try_length++) + { + try_sizes = g_new0 (GtkRequestedSize, try_length); + try_line_size = gather_aligned_item_requests (box, + orientation, + try_length, + item_spacing, + n_children, + try_sizes); + + if (try_line_size <= avail_size && + items_per_line >= try_length) + { + *line_length = try_length; + + g_free (sizes); + sizes = try_sizes; + } + else + { + /* oops, this one failed; stick to the last size that fit and then return */ + g_free (try_sizes); + break; + } + } + + return sizes; +} + +typedef struct { + GArray *requested; + gint extra_pixels; +} AllocatedLine; + +static gint +get_offset_pixels (GtkAlign align, + gint pixels) +{ + gint offset; + + switch (align) { + case GTK_ALIGN_START: + case GTK_ALIGN_FILL: + offset = 0; + break; + case GTK_ALIGN_CENTER: + offset = pixels / 2; + break; + case GTK_ALIGN_END: + offset = pixels; + break; + default: + g_assert_not_reached (); + break; + } + + return offset; +} + +static void +gtk_flow_box_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + GtkAllocation child_allocation; + gint avail_size, avail_other_size, min_items, item_spacing, line_spacing; + GtkAlign item_align; + GtkAlign line_align; + GdkWindow *window; + GtkRequestedSize *line_sizes = NULL; + GtkRequestedSize *item_sizes = NULL; + gint min_item_size, nat_item_size; + gint line_length; + gint item_size = 0; + gint line_size = 0, min_fixed_line_size = 0, nat_fixed_line_size = 0; + gint line_offset, item_offset, n_children, n_lines, line_count; + gint extra_pixels = 0, extra_per_item = 0, extra_extra = 0; + gint extra_line_pixels = 0, extra_per_line = 0, extra_line_extra = 0; + gint i, this_line_size; + GSequenceIter *iter; + + child_allocation.x = 0; + child_allocation.y = 0; + child_allocation.width = 0; + child_allocation.height = 0; + + gtk_widget_set_allocation (widget, allocation); + window = gtk_widget_get_window (widget); + if (window != NULL) + gdk_window_move_resize (window, + allocation->x, allocation->y, + allocation->width, allocation->height); + + child_allocation.x = 0; + child_allocation.y = 0; + child_allocation.width = allocation->width; + + min_items = MAX (1, priv->min_children_per_line); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + avail_size = allocation->width; + avail_other_size = allocation->height; + item_spacing = priv->column_spacing; line_spacing = priv->row_spacing; + } + else /* GTK_ORIENTATION_VERTICAL */ + { + avail_size = allocation->height; + avail_other_size = allocation->width; + item_spacing = priv->row_spacing; + line_spacing = priv->column_spacing; + } + + item_align = ORIENTATION_ALIGN (box); + line_align = OPPOSING_ORIENTATION_ALIGN (box); + + /* Get how many lines we'll be needing to flow */ + n_children = get_visible_children (box); + if (n_children <= 0) + return; + + /* + * Deal with ALIGNED/HOMOGENEOUS modes first, start with + * initial guesses at item/line sizes + */ + get_max_item_size (box, priv->orientation, &min_item_size, &nat_item_size); + if (nat_item_size <= 0) + return; + + /* By default flow at the natural item width */ + line_length = avail_size / (nat_item_size + item_spacing); + + /* After the above aproximation, check if we cant fit one more on the line */ + if (line_length * item_spacing + (line_length + 1) * nat_item_size <= avail_size) + line_length++; + + /* Its possible we were allocated just less than the natural width of the + * minimum item flow length */ + line_length = MAX (min_items, line_length); + line_length = MIN (line_length, priv->max_children_per_line); + + /* Here we just use the largest height-for-width and use that for the height + * of all lines */ + if (priv->homogeneous) + { + n_lines = n_children / line_length; + if ((n_children % line_length) > 0) + n_lines++; + + n_lines = MAX (n_lines, 1); + + /* Now we need the real item allocation size */ + item_size = (avail_size - (line_length - 1) * item_spacing) / line_length; + + /* Cut out the expand space if we're not distributing any */ + if (item_align != GTK_ALIGN_FILL) + item_size = MIN (item_size, nat_item_size); + + get_largest_size_for_opposing_orientation (box, + priv->orientation, + item_size, + &min_fixed_line_size, + &nat_fixed_line_size); + + /* resolve a fixed 'line_size' */ + line_size = (avail_other_size - (n_lines - 1) * line_spacing) / n_lines; + + if (line_align != GTK_ALIGN_FILL) + line_size = MIN (line_size, nat_fixed_line_size); + + /* Get the real extra pixels incase of GTK_ALIGN_START lines */ + extra_pixels = avail_size - (line_length - 1) * item_spacing - item_size * line_length; + extra_line_pixels = avail_other_size - (n_lines - 1) * line_spacing - line_size * n_lines; + } + else + { + gboolean first_line = TRUE; + + /* Find the amount of columns that can fit aligned into the available space + * and collect their requests. + */ + item_sizes = fit_aligned_item_requests (box, + priv->orientation, + avail_size, + item_spacing, + &line_length, + priv->max_children_per_line, + n_children); + + /* Calculate the number of lines after determining the final line_length */ + n_lines = n_children / line_length; + if ((n_children % line_length) > 0) + n_lines++; + + n_lines = MAX (n_lines, 1); + line_sizes = g_new0 (GtkRequestedSize, n_lines); + + /* Get the available remaining size */ + avail_size -= (line_length - 1) * item_spacing; + for (i = 0; i < line_length; i++) + avail_size -= item_sizes[i].minimum_size; + + /* Perform a natural allocation on the columnized items and get the remaining pixels */ + if (avail_size > 0) + extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); + + /* Now that we have the size of each column of items find the size of each individual + * line based on the aligned item sizes. + */ + + for (i = 0, iter = g_sequence_get_begin_iter (priv->children); + !g_sequence_iter_is_end (iter) && i < n_lines; + i++) + { + iter = get_largest_size_for_line_in_opposing_orientation (box, + priv->orientation, + iter, + line_length, + item_sizes, + extra_pixels, + &line_sizes[i].minimum_size, + &line_sizes[i].natural_size); + + + /* Its possible a line is made of completely invisible children */ + if (line_sizes[i].natural_size > 0) + { + if (first_line) + first_line = FALSE; + else + avail_other_size -= line_spacing; + + avail_other_size -= line_sizes[i].minimum_size; + + line_sizes[i].data = GINT_TO_POINTER (i); + } + } + + /* Distribute space among lines naturally */ + if (avail_other_size > 0) + extra_line_pixels = gtk_distribute_natural_allocation (avail_other_size, n_lines, line_sizes); + } + + /* + * Initial sizes of items/lines guessed at this point, + * go on to distribute expand space if needed. + */ + + priv->cur_children_per_line = line_length; + + /* FIXME: This portion needs to consider which columns + * and rows asked for expand space and distribute those + * accordingly for the case of ALIGNED allocation. + * + * If at least one child in a column/row asked for expand; + * we should make that row/column expand entirely. + */ + + /* Calculate expand space per item */ + if (item_align == GTK_ALIGN_FILL) + { + extra_per_item = extra_pixels / line_length; + extra_extra = extra_pixels % line_length; + } + + /* Calculate expand space per line */ + if (line_align == GTK_ALIGN_FILL) + { + extra_per_line = extra_line_pixels / n_lines; + extra_line_extra = extra_line_pixels % n_lines; + } + + /* + * Prepare item/line initial offsets and jump into the + * real allocation loop. + */ + line_offset = item_offset = 0; + + /* prepend extra space to item_offset/line_offset for SPREAD_END */ + item_offset += get_offset_pixels (item_align, extra_pixels); + line_offset += get_offset_pixels (line_align, extra_line_pixels); + + /* Get the allocation size for the first line */ + if (priv->homogeneous) + this_line_size = line_size; + else + { + this_line_size = line_sizes[0].minimum_size; + + if (line_align == GTK_ALIGN_FILL) + { + this_line_size += extra_per_line; + + if (extra_line_extra > 0) + this_line_size++; + } + } + + i = 0; + line_count = 0; + for (iter = g_sequence_get_begin_iter (priv->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + gint position; + gint this_item_size; + + child = g_sequence_get (iter); + + if (!child_is_visible (child)) + continue; + + /* Get item position */ + position = i % line_length; + + /* adjust the line_offset/count at the beginning of each new line */ + if (i > 0 && position == 0) + { + /* Push the line_offset */ + line_offset += this_line_size + line_spacing; + + line_count++; + + /* Get the new line size */ + if (priv->homogeneous) + this_line_size = line_size; + else + { + this_line_size = line_sizes[line_count].minimum_size; + + if (line_align == GTK_ALIGN_FILL) + { + this_line_size += extra_per_line; + + if (line_count < extra_line_extra) + this_line_size++; + } + } + + item_offset = 0; + + if (item_align == GTK_ALIGN_CENTER) + { + item_offset += get_offset_pixels (item_align, extra_pixels); + } + else if (item_align == GTK_ALIGN_END) + { + item_offset += get_offset_pixels (item_align, extra_pixels); + + /* If we're on the last line, prepend the space for + * any leading items */ + if (line_count == n_lines -1) + { + gint extra_items = n_children % line_length; + + if (priv->homogeneous) + { + item_offset += item_size * (line_length - extra_items); + item_offset += item_spacing * (line_length - extra_items); + } + else + { + gint j; + + for (j = 0; j < (line_length - extra_items); j++) + { + item_offset += item_sizes[j].minimum_size; + item_offset += item_spacing; + } + } + } + } + } + + /* Push the index along for the last line when spreading to the end */ + if (item_align == GTK_ALIGN_END && line_count == n_lines -1) + { + gint extra_items = n_children % line_length; + + position += line_length - extra_items; + } + + if (priv->homogeneous) + this_item_size = item_size; + else + this_item_size = item_sizes[position].minimum_size; + + if (item_align == GTK_ALIGN_FILL) + { + this_item_size += extra_per_item; + + if (position < extra_extra) + this_item_size++; + } + + /* Do the actual allocation */ + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + child_allocation.x = item_offset; + child_allocation.y = line_offset; + child_allocation.width = this_item_size; + child_allocation.height = this_line_size; + } + else /* GTK_ORIENTATION_VERTICAL */ + { + child_allocation.x = line_offset; + child_allocation.y = item_offset; + child_allocation.width = this_line_size; + child_allocation.height = this_item_size; + } + + if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width; + gtk_widget_size_allocate (child, &child_allocation); + + item_offset += this_item_size; + item_offset += item_spacing; + + i++; + } + + g_free (item_sizes); + g_free (line_sizes); +} + +static GtkSizeRequestMode +gtk_flow_box_get_request_mode (GtkWidget *widget) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + + return (BOX_PRIV (box)->orientation == GTK_ORIENTATION_HORIZONTAL) ? + GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH : GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT; +} + +/* Gets the largest minimum and natural length of + * 'line_length' consecutive items when aligned into rows/columns */ +static void +get_largest_aligned_line_length (GtkFlowBox *box, + GtkOrientation orientation, + gint line_length, + gint *min_size, + gint *nat_size) +{ + GSequenceIter *iter; + gint max_min_size = 0; + gint max_nat_size = 0; + gint spacing, i; + GtkRequestedSize *aligned_item_sizes; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + spacing = BOX_PRIV (box)->column_spacing; + else + spacing = BOX_PRIV (box)->row_spacing; + + aligned_item_sizes = g_new0 (GtkRequestedSize, line_length); + + /* Get the largest sizes of each index in the line. + */ + i = 0; + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + gint child_min, child_nat; + + child = g_sequence_get (iter); + if (!child_is_visible (child)) + continue; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_width (child, + &child_min, &child_nat); + else /* GTK_ORIENTATION_VERTICAL */ + gtk_widget_get_preferred_height (child, + &child_min, &child_nat); + + aligned_item_sizes[i % line_length].minimum_size = + MAX (aligned_item_sizes[i % line_length].minimum_size, child_min); + + aligned_item_sizes[i % line_length].natural_size = + MAX (aligned_item_sizes[i % line_length].natural_size, child_nat); + + i++; + } + + /* Add up the largest indexes */ + for (i = 0; i < line_length; i++) + { + max_min_size += aligned_item_sizes[i].minimum_size; + max_nat_size += aligned_item_sizes[i].natural_size; + } + + g_free (aligned_item_sizes); + + max_min_size += (line_length - 1) * spacing; + max_nat_size += (line_length - 1) * spacing; + + if (min_size) + *min_size = max_min_size; + + if (nat_size) + *nat_size = max_nat_size; +} + + +static void +gtk_flow_box_get_preferred_width (GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gint min_item_width, nat_item_width; + gint min_items, nat_items; + gint min_width, nat_width; + + min_items = MAX (1, priv->min_children_per_line); + nat_items = MAX (min_items, priv->max_children_per_line); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + min_width = nat_width = 0; + + if (!priv->homogeneous) + { + /* When not homogeneous; horizontally oriented boxes + * need enough width for the widest row */ + if (min_items == 1) + { + get_max_item_size (box, + GTK_ORIENTATION_HORIZONTAL, + &min_item_width, + &nat_item_width); + + min_width += min_item_width; + nat_width += nat_item_width; + } + else + { + gint min_line_length, nat_line_length; + + get_largest_aligned_line_length (box, + GTK_ORIENTATION_HORIZONTAL, + min_items, + &min_line_length, + &nat_line_length); + + if (nat_items > min_items) + get_largest_aligned_line_length (box, + GTK_ORIENTATION_HORIZONTAL, + nat_items, + NULL, + &nat_line_length); + + min_width += min_line_length; + nat_width += nat_line_length; + } + } + else /* In homogeneous mode; horizontally oriented boxs + * give the same width to all children */ + { + get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, + &min_item_width, &nat_item_width); + + min_width += min_item_width * min_items; + min_width += (min_items -1) * priv->column_spacing; + + nat_width += nat_item_width * nat_items; + nat_width += (nat_items -1) * priv->column_spacing; + } + } + else /* GTK_ORIENTATION_VERTICAL */ + { + /* Return the width for the minimum height */ + gint min_height; + + GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL); + GTK_WIDGET_GET_CLASS (widget)->get_preferred_width_for_height (widget, + min_height, + &min_width, + &nat_width); + + } + + if (minimum_size) + *minimum_size = min_width; + + if (natural_size) + *natural_size = nat_width; +} + +static void +gtk_flow_box_get_preferred_height (GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gint min_item_height, nat_item_height; + gint min_items, nat_items; + gint min_height, nat_height; + + min_items = MAX (1, priv->min_children_per_line); + nat_items = MAX (min_items, priv->max_children_per_line); + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + /* Return the height for the minimum width */ + gint min_width; + + GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL); + GTK_WIDGET_GET_CLASS (widget)->get_preferred_height_for_width (widget, + min_width, + &min_height, + &nat_height); + } + else /* GTK_ORIENTATION_VERTICAL */ + { + min_height = nat_height = 0; + + if (! priv->homogeneous) + { + /* When not homogeneous; vertically oriented boxes + * need enough height for the tallest column */ + if (min_items == 1) + { + get_max_item_size (box, GTK_ORIENTATION_VERTICAL, + &min_item_height, &nat_item_height); + + min_height += min_item_height; + nat_height += nat_item_height; + } + else + { + gint min_line_length, nat_line_length; + + get_largest_aligned_line_length (box, + GTK_ORIENTATION_VERTICAL, + min_items, + &min_line_length, + &nat_line_length); + + if (nat_items > min_items) + get_largest_aligned_line_length (box, + GTK_ORIENTATION_VERTICAL, + nat_items, + NULL, + &nat_line_length); + + min_height += min_line_length; + nat_height += nat_line_length; + } + + } + else + { + /* In homogeneous mode; vertically oriented boxes + * give the same height to all children + */ + get_max_item_size (box, + GTK_ORIENTATION_VERTICAL, + &min_item_height, + &nat_item_height); + + min_height += min_item_height * min_items; + min_height += (min_items -1) * priv->row_spacing; + + nat_height += nat_item_height * nat_items; + nat_height += (nat_items -1) * priv->row_spacing; + } + } + + if (minimum_size) + *minimum_size = min_height; + + if (natural_size) + *natural_size = nat_height; +} + +static void +gtk_flow_box_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum_height, + gint *natural_height) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gint min_item_width, nat_item_width; + gint min_items; + gint min_height, nat_height; + gint avail_size, n_children; + + min_items = MAX (1, priv->min_children_per_line); + + min_height = 0; + nat_height = 0; + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + gint min_width; + gint line_length; + gint item_size, extra_pixels; + + n_children = get_visible_children (box); + if (n_children <= 0) + goto out; + + /* Make sure its no smaller than the minimum */ + GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, NULL); + + avail_size = MAX (width, min_width); + if (avail_size <= 0) + goto out; + + get_max_item_size (box, GTK_ORIENTATION_HORIZONTAL, &min_item_width, &nat_item_width); + if (nat_item_width <= 0) + goto out; + + /* By default flow at the natural item width */ + line_length = avail_size / (nat_item_width + priv->column_spacing); + + /* After the above aproximation, check if we cant fit one more on the line */ + if (line_length * priv->column_spacing + (line_length + 1) * nat_item_width <= avail_size) + line_length++; + + /* Its possible we were allocated just less than the natural width of the + * minimum item flow length + */ + line_length = MAX (min_items, line_length); + line_length = MIN (line_length, priv->max_children_per_line); + + /* Now we need the real item allocation size */ + item_size = (avail_size - (line_length - 1) * priv->column_spacing) / line_length; + + /* Cut out the expand space if we're not distributing any */ + if (gtk_widget_get_halign (widget) != GTK_ALIGN_FILL) + { + item_size = MIN (item_size, nat_item_width); + extra_pixels = 0; + } + else + /* Collect the extra pixels for expand children */ + extra_pixels = (avail_size - (line_length - 1) * priv->column_spacing) % line_length; + + if (priv->homogeneous) + { + gint min_item_height, nat_item_height; + gint lines; + + /* Here we just use the largest height-for-width and + * add up the size accordingly + */ + get_largest_size_for_opposing_orientation (box, + GTK_ORIENTATION_HORIZONTAL, + item_size, + &min_item_height, + &nat_item_height); + + /* Round up how many lines we need to allocate for */ + lines = n_children / line_length; + if ((n_children % line_length) > 0) + lines++; + + min_height = min_item_height * lines; + nat_height = nat_item_height * lines; + + min_height += (lines - 1) * priv->row_spacing; + nat_height += (lines - 1) * priv->row_spacing; + } + else + { + gint min_line_height, nat_line_height, i; + gboolean first_line = TRUE; + GtkRequestedSize *item_sizes; + GSequenceIter *iter; + + /* First get the size each set of items take to span the line + * when aligning the items above and below after flowping. + */ + item_sizes = fit_aligned_item_requests (box, + priv->orientation, + avail_size, + priv->column_spacing, + &line_length, + priv->max_children_per_line, + n_children); + + /* Get the available remaining size */ + avail_size -= (line_length - 1) * priv->column_spacing; + for (i = 0; i < line_length; i++) + avail_size -= item_sizes[i].minimum_size; + + if (avail_size > 0) + extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); + + for (iter = g_sequence_get_begin_iter (priv->children); + !g_sequence_iter_is_end (iter);) + { + iter = get_largest_size_for_line_in_opposing_orientation (box, + GTK_ORIENTATION_HORIZONTAL, + iter, + line_length, + item_sizes, + extra_pixels, + &min_line_height, + &nat_line_height); + /* Its possible the line only had invisible widgets */ + if (nat_line_height > 0) + { + if (first_line) + first_line = FALSE; + else + { + min_height += priv->row_spacing; + nat_height += priv->row_spacing; + } + + min_height += min_line_height; + nat_height += nat_line_height; + } + } + + g_free (item_sizes); + } + } + else /* GTK_ORIENTATION_VERTICAL */ + { + /* Return the minimum height */ + GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, &nat_height); + } + + out: + + if (minimum_height) + *minimum_height = min_height; + + if (natural_height) + *natural_height = nat_height; +} + +static void +gtk_flow_box_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gint min_item_height, nat_item_height; + gint min_items; + gint min_width, nat_width; + gint avail_size, n_children; + + min_items = MAX (1, priv->min_children_per_line); + + min_width = 0; + nat_width = 0; + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + { + /* Return the minimum width */ + GTK_WIDGET_GET_CLASS (widget)->get_preferred_width (widget, &min_width, &nat_width); + } + else /* GTK_ORIENTATION_VERTICAL */ + { + gint min_height; + gint line_length; + gint item_size, extra_pixels; + + n_children = get_visible_children (box); + if (n_children <= 0) + goto out; + + /* Make sure its no smaller than the minimum */ + GTK_WIDGET_GET_CLASS (widget)->get_preferred_height (widget, &min_height, NULL); + + avail_size = MAX (height, min_height); + if (avail_size <= 0) + goto out; + + get_max_item_size (box, GTK_ORIENTATION_VERTICAL, &min_item_height, &nat_item_height); + + /* By default flow at the natural item width */ + line_length = avail_size / (nat_item_height + priv->row_spacing); + + /* After the above aproximation, check if we cant fit one more on the line */ + if (line_length * priv->row_spacing + (line_length + 1) * nat_item_height <= avail_size) + line_length++; + + /* Its possible we were allocated just less than the natural width of the + * minimum item flow length + */ + line_length = MAX (min_items, line_length); + line_length = MIN (line_length, priv->max_children_per_line); + + /* Now we need the real item allocation size */ + item_size = (avail_size - (line_length - 1) * priv->row_spacing) / line_length; + + /* Cut out the expand space if we're not distributing any */ + if (gtk_widget_get_valign (widget) != GTK_ALIGN_FILL) + { + item_size = MIN (item_size, nat_item_height); + extra_pixels = 0; + } + else + /* Collect the extra pixels for expand children */ + extra_pixels = (avail_size - (line_length - 1) * priv->row_spacing) % line_length; + + if (priv->homogeneous) + { + gint min_item_width, nat_item_width; + gint lines; + + /* Here we just use the largest height-for-width and + * add up the size accordingly + */ + get_largest_size_for_opposing_orientation (box, + GTK_ORIENTATION_VERTICAL, + item_size, + &min_item_width, + &nat_item_width); + + /* Round up how many lines we need to allocate for */ + n_children = get_visible_children (box); + lines = n_children / line_length; + if ((n_children % line_length) > 0) + lines++; + + min_width = min_item_width * lines; + nat_width = nat_item_width * lines; + + min_width += (lines - 1) * priv->column_spacing; + nat_width += (lines - 1) * priv->column_spacing; + } + else + { + gint min_line_width, nat_line_width, i; + gboolean first_line = TRUE; + GtkRequestedSize *item_sizes; + GSequenceIter *iter; + + /* First get the size each set of items take to span the line + * when aligning the items above and below after flowping. + */ + item_sizes = fit_aligned_item_requests (box, + priv->orientation, + avail_size, + priv->row_spacing, + &line_length, + priv->max_children_per_line, + n_children); + + /* Get the available remaining size */ + avail_size -= (line_length - 1) * priv->column_spacing; + for (i = 0; i < line_length; i++) + avail_size -= item_sizes[i].minimum_size; + + if (avail_size > 0) + extra_pixels = gtk_distribute_natural_allocation (avail_size, line_length, item_sizes); + + for (iter = g_sequence_get_begin_iter (priv->children); + !g_sequence_iter_is_end (iter);) + { + iter = get_largest_size_for_line_in_opposing_orientation (box, + GTK_ORIENTATION_VERTICAL, + iter, + line_length, + item_sizes, + extra_pixels, + &min_line_width, + &nat_line_width); + + /* Its possible the last line only had invisible widgets */ + if (nat_line_width > 0) + { + if (first_line) + first_line = FALSE; + else + { + min_width += priv->column_spacing; + nat_width += priv->column_spacing; + } + + min_width += min_line_width; + nat_width += nat_line_width; + } + } + g_free (item_sizes); + } + } + + out: + if (minimum_width) + *minimum_width = min_width; + + if (natural_width) + *natural_width = nat_width; +} + +/* Drawing {{{3 */ + +static gboolean +gtk_flow_box_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + GtkAllocation allocation = { 0, }; + GtkStyleContext* context; + + gtk_widget_get_allocation (GTK_WIDGET (box), &allocation); + context = gtk_widget_get_style_context (GTK_WIDGET (box)); + gtk_render_background (context, cr, 0, 0, allocation.width, allocation.height); + + GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->draw (widget, cr); + + if (priv->rubberband_first && priv->rubberband_last) + { + GSequenceIter *iter, *iter1, *iter2; + GdkRectangle line_rect, rect; + GArray *lines; + gboolean vertical; + + vertical = priv->orientation == GTK_ORIENTATION_VERTICAL; + + cairo_save (cr); + + context = gtk_widget_get_style_context (widget); + gtk_style_context_save (context); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_RUBBERBAND); + + iter1 = CHILD_PRIV (priv->rubberband_first)->iter; + iter2 = CHILD_PRIV (priv->rubberband_last)->iter; + + if (g_sequence_iter_compare (iter2, iter1) < 0) + { + iter = iter1; + iter1 = iter2; + iter2 = iter; + } + + line_rect.width = 0; + lines = g_array_new (FALSE, FALSE, sizeof (GdkRectangle)); + + for (iter = iter1; + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkWidget *child; + + child = g_sequence_get (iter); + gtk_widget_get_allocation (GTK_WIDGET (child), &rect); + if (line_rect.width == 0) + line_rect = rect; + else + { + if ((vertical && rect.x == line_rect.x) || + (!vertical && rect.y == line_rect.y)) + gdk_rectangle_union (&rect, &line_rect, &line_rect); + else + { + g_array_append_val (lines, line_rect); + line_rect = rect; + } + } + + if (g_sequence_iter_compare (iter, iter2) == 0) + break; + } + + if (line_rect.width != 0) + g_array_append_val (lines, line_rect); + + if (lines->len > 0) + { + GtkStateFlags state; + cairo_path_t *path; + GtkBorder border; + GdkRGBA border_color; + + if (vertical) + path_from_vertical_line_rects (cr, (GdkRectangle *)lines->data, lines->len); + else + path_from_horizontal_line_rects (cr, (GdkRectangle *)lines->data, lines->len); + + /* For some reason we need to copy and reapply the path, + * or it gets eaten by gtk_render_background() + */ + path = cairo_copy_path (cr); + + cairo_save (cr); + cairo_clip (cr); + gtk_widget_get_allocation (widget, &allocation); + gtk_render_background (context, cr, + 0, 0, + allocation.width, allocation.height); + cairo_restore (cr); + + cairo_append_path (cr, path); + cairo_path_destroy (path); + + state = gtk_widget_get_state_flags (widget); + gtk_style_context_get_border_color (context, state, &border_color); + gtk_style_context_get_border (context, state, &border); + + cairo_set_line_width (cr, border.left); + gdk_cairo_set_source_rgba (cr, &border_color); + cairo_stroke (cr); + } + g_array_free (lines, TRUE); + + gtk_style_context_restore (context); + cairo_restore (cr); + } + + return TRUE; +} + +/* Autoscrolling {{{3 */ + +static void +remove_autoscroll (GtkFlowBox *box) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + if (priv->autoscroll_id) + { + gtk_widget_remove_tick_callback (GTK_WIDGET (box), priv->autoscroll_id); + priv->autoscroll_id = 0; + } + + priv->autoscroll_mode = GTK_SCROLL_NONE; +} + +static gboolean +autoscroll_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer data) +{ + GtkFlowBox *box = GTK_FLOW_BOX (data); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + GtkAdjustment *adjustment; + gdouble factor; + gdouble increment; + gdouble value; + + if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) + adjustment = priv->vadjustment; + else + adjustment = priv->hadjustment; + + switch (priv->autoscroll_mode) + { + case GTK_SCROLL_STEP_FORWARD: + factor = AUTOSCROLL_FACTOR; + break; + case GTK_SCROLL_STEP_BACKWARD: + factor = - AUTOSCROLL_FACTOR; + break; + case GTK_SCROLL_PAGE_FORWARD: + factor = AUTOSCROLL_FACTOR_FAST; + break; + case GTK_SCROLL_PAGE_BACKWARD: + factor = - AUTOSCROLL_FACTOR_FAST; + break; + default: + g_assert_not_reached (); + } + + increment = gtk_adjustment_get_step_increment (adjustment) / factor; + + value = gtk_adjustment_get_value (adjustment); + value += increment; + gtk_adjustment_set_value (adjustment, value); + + if (priv->rubberband_select) + { + gint x, y; + GtkFlowBoxChild *child; + + gdk_window_get_device_position (gtk_widget_get_window (widget), + priv->rubberband_device, + &x, &y, NULL); + + child = gtk_flow_box_find_child_at_pos (box, x, y); + + gtk_flow_box_update_prelight (box, child); + gtk_flow_box_update_active (box, child); + + if (child != NULL) + priv->rubberband_last = child; + } + + return G_SOURCE_CONTINUE; +} + +static void +add_autoscroll (GtkFlowBox *box) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + if (priv->autoscroll_id != 0 || + priv->autoscroll_mode == GTK_SCROLL_NONE) + return; + + priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (box), + (GtkTickCallback)autoscroll_cb, + box, + NULL); +} + +static gboolean +get_view_rect (GtkFlowBox *box, + GdkRectangle *rect) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + GtkWidget *parent; + GdkWindow *view; + + parent = gtk_widget_get_parent (GTK_WIDGET (box)); + if (GTK_IS_VIEWPORT (parent)) + { + view = gtk_viewport_get_view_window (GTK_VIEWPORT (parent)); + rect->x = gtk_adjustment_get_value (priv->hadjustment); + rect->y = gtk_adjustment_get_value (priv->vadjustment); + rect->width = gdk_window_get_width (view); + rect->height = gdk_window_get_height (view); + return TRUE; + } + + return FALSE; +} + +static void +update_autoscroll_mode (GtkFlowBox *box, + gint x, + gint y) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + GtkScrollType mode = GTK_SCROLL_NONE; + GdkRectangle rect; + gint size, pos; + + if (priv->rubberband_select && get_view_rect (box, &rect)) + { + if (priv->orientation == GTK_ORIENTATION_VERTICAL) + { + size = rect.width; + pos = x - rect.x; + } + else + { + size = rect.height; + pos = y - rect.y; + } + + if (pos < 0 - AUTOSCROLL_FAST_DISTANCE) + mode = GTK_SCROLL_PAGE_BACKWARD; + else if (pos > size + AUTOSCROLL_FAST_DISTANCE) + mode = GTK_SCROLL_PAGE_FORWARD; + else if (pos < 0) + mode = GTK_SCROLL_STEP_BACKWARD; + else if (pos > size) + mode = GTK_SCROLL_STEP_FORWARD; + } + + if (mode != priv->autoscroll_mode) + { + remove_autoscroll (box); + priv->autoscroll_mode = mode; + add_autoscroll (box); + } +} + +/* Event handling {{{3 */ + +static gboolean +gtk_flow_box_enter_notify_event (GtkWidget *widget, + GdkEventCrossing *event) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxChild *child; + + if (event->window != gtk_widget_get_window (GTK_WIDGET (box))) + return FALSE; + + child = gtk_flow_box_find_child_at_pos (box, event->x, event->y); + gtk_flow_box_update_prelight (box, child); + gtk_flow_box_update_active (box, child); + + return FALSE; +} + +static gboolean +gtk_flow_box_leave_notify_event (GtkWidget *widget, + GdkEventCrossing *event) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxChild *child = NULL; + + if (event->window != gtk_widget_get_window (GTK_WIDGET (box))) + return FALSE; + + if (event->detail != GDK_NOTIFY_INFERIOR) + child = NULL; + else + child = gtk_flow_box_find_child_at_pos (box, event->x, event->y); + + gtk_flow_box_update_prelight (box, child); + gtk_flow_box_update_active (box, child); + + return FALSE; +} + +static gboolean +gtk_flow_box_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + GtkFlowBoxChild *child; + GdkWindow *window; + GdkWindow *event_window; + gint relative_x; + gint relative_y; + gdouble parent_x; + gdouble parent_y; + + window = gtk_widget_get_window (GTK_WIDGET (box)); + event_window = event->window; + relative_x = event->x; + relative_y = event->y; + + while ((event_window != NULL) && (event_window != window)) + { + gdk_window_coords_to_parent (event_window, + relative_x, relative_y, + &parent_x, &parent_y); + relative_x = parent_x; + relative_y = parent_y; + event_window = gdk_window_get_effective_parent (event_window); + } + + child = gtk_flow_box_find_child_at_pos (box, relative_x, relative_y); + gtk_flow_box_update_prelight (box, child); + gtk_flow_box_update_active (box, child); + + if (priv->track_motion) + { + if (!priv->rubberband_select && + (event->x - priv->button_down_x) * (event->x - priv->button_down_x) + + (event->y - priv->button_down_y) * (event->y - priv->button_down_y) > RUBBERBAND_START_DISTANCE * RUBBERBAND_START_DISTANCE) + { + priv->rubberband_select = TRUE; + priv->rubberband_first = gtk_flow_box_find_child_at_pos (box, priv->button_down_x, priv->button_down_y); + + /* Grab focus here, so Escape-to-stop-rubberband works */ + gtk_flow_box_update_cursor (box, priv->rubberband_first); + } + + if (priv->rubberband_select) + { + if (priv->rubberband_first == NULL) + priv->rubberband_first = child; + if (child != NULL) + priv->rubberband_last = child; + + update_autoscroll_mode (box, event->x, event->y); + } + } + + return FALSE; +} + +static gboolean +gtk_flow_box_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + GtkFlowBoxChild *child; + + if (event->button == GDK_BUTTON_PRIMARY) + { + child = gtk_flow_box_find_child_at_pos (box, event->x, event->y); + if (child != NULL) + { + priv->active_child = child; + priv->active_child_active = TRUE; + gtk_widget_queue_draw (GTK_WIDGET (box)); + if (event->type == GDK_2BUTTON_PRESS && + !priv->activate_on_single_click) + { + g_signal_emit (box, signals[CHILD_ACTIVATED], 0, child); + return TRUE; + } + } + + if (priv->selection_mode == GTK_SELECTION_MULTIPLE) + { + priv->track_motion = TRUE; + priv->rubberband_select = FALSE; + priv->rubberband_first = NULL; + priv->rubberband_last = NULL; + priv->button_down_x = event->x; + priv->button_down_y = event->y; + priv->rubberband_device = gdk_event_get_device ((GdkEvent*)event); + get_current_selection_modifiers (widget, &priv->rubberband_modify, &priv->rubberband_extend); + } + } + + return FALSE; +} + +static void +gtk_flow_box_stop_rubberband (GtkFlowBox *box) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + priv->rubberband_select = FALSE; + priv->rubberband_first = NULL; + priv->rubberband_last = NULL; + priv->rubberband_device = NULL; + + remove_autoscroll (box); + + gtk_widget_queue_draw (GTK_WIDGET (box)); +} + +static gboolean +gtk_flow_box_button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + if (event->button == GDK_BUTTON_PRIMARY) + { + if (priv->active_child != NULL && priv->active_child_active) + { + if (priv->activate_on_single_click) + gtk_flow_box_select_and_activate (box, priv->active_child); + else + { + gboolean modify; + gboolean extend; + GdkDevice *device; + + get_current_selection_modifiers (widget, &modify, &extend); + + /* With touch, we default to modifying the selection. + * You can still clear the selection and start over + * by holding Ctrl. + */ + device = gdk_event_get_source_device ((GdkEvent *)event); + if (gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN) + modify = !modify; + + gtk_flow_box_update_selection (box, priv->active_child, modify, extend); + } + } + + priv->active_child = NULL; + priv->active_child_active = FALSE; + gtk_widget_queue_draw (GTK_WIDGET (box)); + } + + priv->track_motion = FALSE; + if (priv->rubberband_select) + { + if (!priv->rubberband_extend && !priv->rubberband_modify) + gtk_flow_box_unselect_all_internal (box); + gtk_flow_box_select_all_between (box, priv->rubberband_first, priv->rubberband_last, priv->rubberband_modify); + + gtk_flow_box_stop_rubberband (box); + + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); + + } + + return FALSE; +} + +static gboolean +gtk_flow_box_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + if (priv->rubberband_select) + { + if (event->keyval == GDK_KEY_Escape) + { + gtk_flow_box_stop_rubberband (box); + return TRUE; + } + } + + return GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->key_press_event (widget, event); +} + +static void +gtk_flow_box_grab_notify (GtkWidget *widget, + gboolean was_grabbed) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + if (!was_grabbed) + { + if (priv->rubberband_select) + gtk_flow_box_stop_rubberband (box); + } +} + +/* Realize and map {{{3 */ + +static void +gtk_flow_box_realize (GtkWidget *widget) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkAllocation allocation; + GdkWindowAttr attributes = {0}; + GdkWindow *window; + + gtk_widget_get_allocation (GTK_WIDGET (box), &allocation); + gtk_widget_set_realized (GTK_WIDGET (box), TRUE); + + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.event_mask = gtk_widget_get_events (GTK_WIDGET (box)) + | GDK_ENTER_NOTIFY_MASK + | GDK_LEAVE_NOTIFY_MASK + | GDK_POINTER_MOTION_MASK + | GDK_EXPOSURE_MASK + | GDK_KEY_PRESS_MASK + | GDK_BUTTON_PRESS_MASK + | GDK_BUTTON_RELEASE_MASK; + attributes.wclass = GDK_INPUT_OUTPUT; + + window = gdk_window_new (gtk_widget_get_parent_window (GTK_WIDGET (box)), + &attributes, GDK_WA_X | GDK_WA_Y); + gtk_widget_register_window (GTK_WIDGET (box), window); + gtk_widget_set_window (GTK_WIDGET (box), window); + gtk_style_context_set_background (gtk_widget_get_style_context (GTK_WIDGET (box)), window); +} + +static void +gtk_flow_box_unmap (GtkWidget *widget) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + + remove_autoscroll (box); + + GTK_WIDGET_CLASS (gtk_flow_box_parent_class)->unmap (widget); +} + +/* GtkContainer implementation {{{2 */ + +static void +gtk_flow_box_add (GtkContainer *container, + GtkWidget *child) +{ + gtk_flow_box_insert (GTK_FLOW_BOX (container), child, -1); +} + +static void +gtk_flow_box_remove (GtkContainer *container, + GtkWidget *widget) +{ + GtkFlowBox *box = GTK_FLOW_BOX (container); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gboolean was_visible; + gboolean was_selected; + GtkFlowBoxChild *child; + + if (GTK_IS_FLOW_BOX_CHILD (widget)) + child = GTK_FLOW_BOX_CHILD (widget); + else + { + child = (GtkFlowBoxChild*)gtk_widget_get_parent (widget); + if (!GTK_IS_FLOW_BOX_CHILD (child)) + { + g_warning ("Tried to remove non-child %p\n", widget); + return; + } + } + + was_visible = child_is_visible (GTK_WIDGET (child)); + was_selected = CHILD_PRIV (child)->selected; + + if (child == priv->prelight_child) + priv->prelight_child = NULL; + if (child == priv->active_child) + priv->active_child = NULL; + if (child == priv->selected_child) + priv->selected_child = NULL; + + gtk_widget_unparent (GTK_WIDGET (child)); + g_sequence_remove (CHILD_PRIV (child)->iter); + + if (was_visible && gtk_widget_get_visible (GTK_WIDGET (box))) + gtk_widget_queue_resize (GTK_WIDGET (box)); + + if (was_selected) + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); +} + +static void +gtk_flow_box_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_target) +{ + GSequenceIter *iter; + GtkWidget *child; + + iter = g_sequence_get_begin_iter (BOX_PRIV (container)->children); + while (!g_sequence_iter_is_end (iter)) + { + child = g_sequence_get (iter); + iter = g_sequence_iter_next (iter); + callback (child, callback_target); + } +} + +static GType +gtk_flow_box_child_type (GtkContainer *container) +{ + return GTK_TYPE_FLOW_BOX_CHILD; +} + +/* Keynav {{{2 */ + +static gboolean +gtk_flow_box_focus (GtkWidget *widget, + GtkDirectionType direction) +{ + GtkFlowBox *box = GTK_FLOW_BOX (widget); + GtkWidget *focus_child; + GSequenceIter *iter; + GtkFlowBoxChild *next_focus_child; + + focus_child = gtk_container_get_focus_child (GTK_CONTAINER (box)); + next_focus_child = NULL; + + if (focus_child != NULL) + { + if (gtk_widget_child_focus (focus_child, direction)) + return TRUE; + + iter = CHILD_PRIV (focus_child)->iter; + + if (direction == GTK_DIR_LEFT || direction == GTK_DIR_TAB_BACKWARD) + iter = gtk_flow_box_get_previous_focusable (box, iter); + else if (direction == GTK_DIR_RIGHT || direction == GTK_DIR_TAB_FORWARD) + iter = gtk_flow_box_get_next_focusable (box, iter); + else if (direction == GTK_DIR_UP) + iter = gtk_flow_box_get_above_focusable (box, iter); + else if (direction == GTK_DIR_DOWN) + iter = gtk_flow_box_get_below_focusable (box, iter); + + if (iter != NULL) + next_focus_child = g_sequence_get (iter); + } + else + { + if (BOX_PRIV (box)->selected_child) + next_focus_child = BOX_PRIV (box)->selected_child; + else + { + if (direction == GTK_DIR_UP || direction == GTK_DIR_TAB_BACKWARD) + iter = gtk_flow_box_get_last_focusable (box); + else + iter = gtk_flow_box_get_first_focusable (box); + + if (iter != NULL) + next_focus_child = g_sequence_get (iter); + } + } + + if (next_focus_child == NULL) + { + if (direction == GTK_DIR_UP || direction == GTK_DIR_DOWN || + direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT) + { + if (gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) + return TRUE; + } + + return FALSE; + } + + if (gtk_widget_child_focus (GTK_WIDGET (next_focus_child), direction)) + return TRUE; + + return TRUE; +} + +static void +gtk_flow_box_add_move_binding (GtkBindingSet *binding_set, + guint keyval, + GdkModifierType modmask, + GtkMovementStep step, + gint count) +{ + GdkDisplay *display; + GdkModifierType extend_mod_mask = GDK_SHIFT_MASK; + GdkModifierType modify_mod_mask = GDK_CONTROL_MASK; + + display = gdk_display_get_default (); + if (display) + { + extend_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), + GDK_MODIFIER_INTENT_EXTEND_SELECTION); + modify_mod_mask = gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), + GDK_MODIFIER_INTENT_MODIFY_SELECTION); + } + + gtk_binding_entry_add_signal (binding_set, keyval, modmask, + "move-cursor", 2, + GTK_TYPE_MOVEMENT_STEP, step, + G_TYPE_INT, count, + NULL); + gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask, + "move-cursor", 2, + GTK_TYPE_MOVEMENT_STEP, step, + G_TYPE_INT, count, + NULL); + gtk_binding_entry_add_signal (binding_set, keyval, modmask | modify_mod_mask, + "move-cursor", 2, + GTK_TYPE_MOVEMENT_STEP, step, + G_TYPE_INT, count, + NULL); + gtk_binding_entry_add_signal (binding_set, keyval, modmask | extend_mod_mask | modify_mod_mask, + "move-cursor", 2, + GTK_TYPE_MOVEMENT_STEP, step, + G_TYPE_INT, count, + NULL); +} + +static void +gtk_flow_box_activate_cursor_child (GtkFlowBox *box) +{ + gtk_flow_box_select_and_activate (box, BOX_PRIV (box)->cursor_child); +} + +static void +gtk_flow_box_toggle_cursor_child (GtkFlowBox *box) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + if (priv->cursor_child == NULL) + return; + + if ((priv->selection_mode == GTK_SELECTION_SINGLE || + priv->selection_mode == GTK_SELECTION_MULTIPLE) && + CHILD_PRIV (priv->cursor_child)->selected) + gtk_flow_box_unselect_child_internal (box, priv->cursor_child); + else + gtk_flow_box_select_and_activate (box, priv->cursor_child); +} + +static void +gtk_flow_box_move_cursor (GtkFlowBox *box, + GtkMovementStep step, + gint count) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + gboolean modify; + gboolean extend; + GtkFlowBoxChild *child; + GtkFlowBoxChild *prev; + GtkFlowBoxChild *next; + GtkAllocation allocation; + gint page_size; + GSequenceIter *iter; + gint start; + GtkAdjustment *adjustment; + gboolean vertical; + + vertical = priv->orientation == GTK_ORIENTATION_VERTICAL; + + if (vertical) + { + switch (step) + { + case GTK_MOVEMENT_VISUAL_POSITIONS: + step = GTK_MOVEMENT_DISPLAY_LINES; + break; + case GTK_MOVEMENT_DISPLAY_LINES: + step = GTK_MOVEMENT_VISUAL_POSITIONS; + break; + default: ; + } + } + + child = NULL; + switch (step) + { + case GTK_MOVEMENT_VISUAL_POSITIONS: + if (priv->cursor_child != NULL) + { + iter = CHILD_PRIV (priv->cursor_child)->iter; + if (gtk_widget_get_direction (GTK_WIDGET (box)) == GTK_TEXT_DIR_RTL) + count = - count; + + while (count < 0 && iter != NULL) + { + iter = gtk_flow_box_get_previous_focusable (box, iter); + count = count + 1; + } + while (count > 0 && iter != NULL) + { + iter = gtk_flow_box_get_next_focusable (box, iter); + count = count - 1; + } + + if (iter != NULL && !g_sequence_iter_is_end (iter)) + child = g_sequence_get (iter); + } + break; + + case GTK_MOVEMENT_BUFFER_ENDS: + if (count < 0) + iter = gtk_flow_box_get_first_focusable (box); + else + iter = gtk_flow_box_get_last_focusable (box); + if (iter != NULL) + child = g_sequence_get (iter); + break; + + case GTK_MOVEMENT_DISPLAY_LINES: + if (priv->cursor_child != NULL) + { + iter = CHILD_PRIV (priv->cursor_child)->iter; + + while (count < 0 && iter != NULL) + { + iter = gtk_flow_box_get_above_focusable (box, iter); + count = count + 1; + } + while (count > 0 && iter != NULL) + { + iter = gtk_flow_box_get_below_focusable (box, iter); + count = count - 1; + } + + if (iter != NULL) + child = g_sequence_get (iter); + } + break; + + case GTK_MOVEMENT_PAGES: + page_size = 100; + adjustment = vertical ? priv->hadjustment : priv->vadjustment; + if (adjustment) + page_size = gtk_adjustment_get_page_increment (adjustment); + + if (priv->cursor_child != NULL) + { + child = priv->cursor_child; + iter = CHILD_PRIV (child)->iter; + gtk_widget_get_allocation (GTK_WIDGET (child), &allocation); + start = vertical ? allocation.x : allocation.y; + + if (count < 0) + { + gint i = 0; + + /* Up */ + while (iter != NULL) + { + iter = gtk_flow_box_get_previous_focusable (box, iter); + if (iter == NULL) + break; + + prev = g_sequence_get (iter); + + /* go up an even number of rows */ + if (i % priv->cur_children_per_line == 0) + { + gtk_widget_get_allocation (GTK_WIDGET (prev), &allocation); + if ((vertical ? allocation.x : allocation.y) < start - page_size) + break; + } + + child = prev; + i++; + } + } + else + { + gint i = 0; + + /* Down */ + while (!g_sequence_iter_is_end (iter)) + { + iter = gtk_flow_box_get_next_focusable (box, iter); + if (g_sequence_iter_is_end (iter)) + break; + + next = g_sequence_get (iter); + + if (i % priv->cur_children_per_line == 0) + { + gtk_widget_get_allocation (GTK_WIDGET (next), &allocation); + if ((vertical ? allocation.x : allocation.y) > start + page_size) + break; + } + + child = next; + i++; + } + } + gtk_widget_get_allocation (GTK_WIDGET (child), &allocation); + } + break; + + default: + g_assert_not_reached (); + } + + if (child == NULL || child == priv->cursor_child) + { + GtkDirectionType direction = count < 0 ? GTK_DIR_UP : GTK_DIR_DOWN; + + if (!gtk_widget_keynav_failed (GTK_WIDGET (box), direction)) + { + GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (box)); + + if (toplevel) + gtk_widget_child_focus (toplevel, + direction == GTK_DIR_UP ? + GTK_DIR_TAB_BACKWARD : + GTK_DIR_TAB_FORWARD); + + } + + return; + } + + get_current_selection_modifiers (GTK_WIDGET (box), &modify, &extend); + + gtk_flow_box_update_cursor (box, child); + if (!modify) + gtk_flow_box_update_selection (box, child, FALSE, extend); +} + +/* Selection {{{2 */ + +static void +gtk_flow_box_selected_children_changed (GtkFlowBox *box) +{ +} + +/* GObject implementation {{{2 */ + +static void +gtk_flow_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkFlowBox *box = GTK_FLOW_BOX (object); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + switch (prop_id) + { + case PROP_ORIENTATION: + g_value_set_enum (value, priv->orientation); + break; + case PROP_HOMOGENEOUS: + g_value_set_boolean (value, priv->homogeneous); + break; + case PROP_COLUMN_SPACING: + g_value_set_uint (value, priv->column_spacing); + break; + case PROP_ROW_SPACING: + g_value_set_uint (value, priv->row_spacing); + break; + case PROP_MIN_CHILDREN_PER_LINE: + g_value_set_uint (value, priv->min_children_per_line); + break; + case PROP_MAX_CHILDREN_PER_LINE: + g_value_set_uint (value, priv->max_children_per_line); + break; + case PROP_SELECTION_MODE: + g_value_set_enum (value, priv->selection_mode); + break; + case PROP_ACTIVATE_ON_SINGLE_CLICK: + g_value_set_boolean (value, priv->activate_on_single_click); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_flow_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkFlowBox *box = GTK_FLOW_BOX (object); + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + switch (prop_id) + { + case PROP_ORIENTATION: + priv->orientation = g_value_get_enum (value); + /* Re-box the children in the new orientation */ + gtk_widget_queue_resize (GTK_WIDGET (box)); + break; + case PROP_HOMOGENEOUS: + gtk_flow_box_set_homogeneous (box, g_value_get_boolean (value)); + break; + case PROP_COLUMN_SPACING: + gtk_flow_box_set_column_spacing (box, g_value_get_uint (value)); + break; + case PROP_ROW_SPACING: + gtk_flow_box_set_row_spacing (box, g_value_get_uint (value)); + break; + case PROP_MIN_CHILDREN_PER_LINE: + gtk_flow_box_set_min_children_per_line (box, g_value_get_uint (value)); + break; + case PROP_MAX_CHILDREN_PER_LINE: + gtk_flow_box_set_max_children_per_line (box, g_value_get_uint (value)); + break; + case PROP_SELECTION_MODE: + gtk_flow_box_set_selection_mode (box, g_value_get_enum (value)); + break; + case PROP_ACTIVATE_ON_SINGLE_CLICK: + gtk_flow_box_set_activate_on_single_click (box, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_flow_box_finalize (GObject *obj) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (obj); + + if (priv->filter_destroy != NULL) + priv->filter_destroy (priv->filter_data); + if (priv->sort_destroy != NULL) + priv->sort_destroy (priv->sort_data); + + g_sequence_free (priv->children); + g_clear_object (&priv->hadjustment); + g_clear_object (&priv->vadjustment); + + G_OBJECT_CLASS (gtk_flow_box_parent_class)->finalize (obj); +} + +static void +gtk_flow_box_class_init (GtkFlowBoxClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class); + GtkBindingSet *binding_set; + + object_class->finalize = gtk_flow_box_finalize; + object_class->get_property = gtk_flow_box_get_property; + object_class->set_property = gtk_flow_box_set_property; + + widget_class->enter_notify_event = gtk_flow_box_enter_notify_event; + widget_class->leave_notify_event = gtk_flow_box_leave_notify_event; + widget_class->motion_notify_event = gtk_flow_box_motion_notify_event; + widget_class->size_allocate = gtk_flow_box_size_allocate; + widget_class->realize = gtk_flow_box_realize; + widget_class->unmap = gtk_flow_box_unmap; + widget_class->focus = gtk_flow_box_focus; + widget_class->draw = gtk_flow_box_draw; + widget_class->button_press_event = gtk_flow_box_button_press_event; + widget_class->button_release_event = gtk_flow_box_button_release_event; + widget_class->key_press_event = gtk_flow_box_key_press_event; + widget_class->grab_notify = gtk_flow_box_grab_notify; + widget_class->get_request_mode = gtk_flow_box_get_request_mode; + widget_class->get_preferred_width = gtk_flow_box_get_preferred_width; + widget_class->get_preferred_height = gtk_flow_box_get_preferred_height; + widget_class->get_preferred_height_for_width = gtk_flow_box_get_preferred_height_for_width; + widget_class->get_preferred_width_for_height = gtk_flow_box_get_preferred_width_for_height; + + container_class->add = gtk_flow_box_add; + container_class->remove = gtk_flow_box_remove; + container_class->forall = gtk_flow_box_forall; + container_class->child_type = gtk_flow_box_child_type; + gtk_container_class_handle_border_width (container_class); + + class->activate_cursor_child = gtk_flow_box_activate_cursor_child; + class->toggle_cursor_child = gtk_flow_box_toggle_cursor_child; + class->move_cursor = gtk_flow_box_move_cursor; + class->select_all = gtk_flow_box_select_all; + class->unselect_all = gtk_flow_box_unselect_all; + class->selected_children_changed = gtk_flow_box_selected_children_changed; + + g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation"); + + /** + * GtkFlowBox:selection-mode: + * + * The selection mode used by the flow box. + */ + g_object_class_install_property (object_class, + PROP_SELECTION_MODE, + g_param_spec_enum ("selection-mode", + "Selection mode", + "The selection mode", + GTK_TYPE_SELECTION_MODE, + GTK_SELECTION_SINGLE, + G_PARAM_READWRITE)); + + /** + * GtkFlowBox:activate-on-single-click: + * + * Determines whether children can be activated with a single + * click, or require a double-click. + */ + g_object_class_install_property (object_class, + PROP_ACTIVATE_ON_SINGLE_CLICK, + g_param_spec_boolean ("activate-on-single-click", + "Activate on Single Click", + "Activate row on a single click", + TRUE, + G_PARAM_READWRITE)); + + /** + * GtkFlowBox:homogeneous: + * + * Determines whether all children should be allocated the + * same size. + */ + g_object_class_install_property (object_class, + PROP_HOMOGENEOUS, + g_param_spec_boolean ("homogeneous", + "Homogeneous", + "Whether the children should all be the same size", + FALSE, + G_PARAM_READWRITE)); + + /** + * GtkFlowBox:min-children-per-line: + * + * The minimum number of children to allocate consecutively + * in the given orientation. + * + * Setting the minimum children per line ensures + * that a reasonably small height will be requested + * for the overall minimum width of the box. + */ + g_object_class_install_property (object_class, + PROP_MIN_CHILDREN_PER_LINE, + g_param_spec_uint ("min-children-per-line", + "Minimum Children Per Line", + "The minimum number of children to allocate " + "consecutively in the given orientation.", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE)); + + /** + * GtkFlowBox:max-children-per-line: + * + * The maximum amount of children to request space for consecutively + * in the given orientation. + */ + g_object_class_install_property (object_class, + PROP_MAX_CHILDREN_PER_LINE, + g_param_spec_uint ("max-children-per-line", + "Maximum Children Per Line", + "The maximum amount of children to request space for " + "consecutively in the given orientation.", + 0, + G_MAXUINT, + DEFAULT_MAX_CHILDREN_PER_LINE, + G_PARAM_READWRITE)); + + /** + * GtkFlowBox:row-spacing: + * + * The amount of vertical space between two children. + */ + g_object_class_install_property (object_class, + PROP_ROW_SPACING, + g_param_spec_uint ("row-spacing", + "Vertical spacing", + "The amount of vertical space between two children", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE)); + + /** + * GtkFlowBox:column-spacing: + * + * The amount of horizontal space between two children. + */ + g_object_class_install_property (object_class, + PROP_COLUMN_SPACING, + g_param_spec_uint ("column-spacing", + "Horizontal spacing", + "The amount of horizontal space between two children", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE)); + + /** + * GtkFlowBox::child-activated: + * @box: the #GtkFlowBox on which the signal is emitted + * @child: the child that is activated + * + * The ::child-activated signal is emitted when a child has been + * activated by the user. + */ + signals[CHILD_ACTIVATED] = g_signal_new ("child-activated", + GTK_TYPE_FLOW_BOX, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtkFlowBoxClass, child_activated), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + GTK_TYPE_FLOW_BOX_CHILD); + + /** + * GtkFlowBox::selected-children-changed: + * @box: the #GtkFlowBox on wich the signal is emitted + * + * The ::selected-children-changed signal is emitted when the + * set of selected children changes. + * + * Use gtk_flow_box_selected_foreach() or + * gtk_flow_box_get_selected_children() to obtain the + * selected children. + */ + signals[SELECTED_CHILDREN_CHANGED] = g_signal_new ("selected-children-changed", + GTK_TYPE_FLOW_BOX, + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (GtkFlowBoxClass, selected_children_changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /** + * GtkFlowBox::activate-cursor-child: + * @box: the #GtkFlowBox on which the signal is emitted + * + * The ::activate-cursor-child signal is a + * [keybinding signal][GtkBindingSignal] + * which gets emitted when the user activates the @box. + */ + signals[ACTIVATE_CURSOR_CHILD] = g_signal_new ("activate-cursor-child", + GTK_TYPE_FLOW_BOX, + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (GtkFlowBoxClass, activate_cursor_child), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /** + * GtkFlowBox::toggle-cursor-child: + * @box: the #GtkFlowBox on which the signal is emitted + * + * The ::toggle-cursor-child signal is a + * [keybinding signal][GtkBindingSignal] + * which toggles the selection of the child that has the focus. + * + * The default binding for this signal is Ctrl-Space. + */ + signals[TOGGLE_CURSOR_CHILD] = g_signal_new ("toggle-cursor-child", + GTK_TYPE_FLOW_BOX, + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (GtkFlowBoxClass, toggle_cursor_child), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /** + * GtkFlowBox::move-cursor: + * @box: the #GtkFlowBox on which the signal is emitted + * @step: the granularity fo the move, as a #GtkMovementStep + * @count: the number of @step units to move + * + * The ::move-cursor signal is a + * [keybinding signal][GtkBindingSignal] + * which gets emitted when the user initiates a cursor movement. + * If the cursor is not visible in @text_view, this signal causes + * the viewport to be moved instead. + * + * Applications should not connect to it, but may emit it with + * g_signal_emit_by_name() if they need to control the cursor + * programmatically. + * + * The default bindings for this signal come in two variants, + * the variant with the Shift modifier extends the selection, + * the variant without the Shift modifer does not. + * There are too many key combinations to list them all here. + * - Arrow keys move by individual children + * - Home/End keys move to the ends of the box + * - PageUp/PageDown keys move vertically by pages + */ + signals[MOVE_CURSOR] = g_signal_new ("move-cursor", + GTK_TYPE_FLOW_BOX, + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (GtkFlowBoxClass, move_cursor), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT); + /** + * GtkFlowBox::select-all: + * @box: the #GtkFlowBox on which the signal is emitted + * + * The ::select-all signal is a + * [keybinding signal][GtkBindingSignal] + * which gets emitted to select all children of the box, if + * the selection mode permits it. + * + * The default bindings for this signal is Ctrl-a. + */ + signals[SELECT_ALL] = g_signal_new ("select-all", + GTK_TYPE_FLOW_BOX, + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (GtkFlowBoxClass, select_all), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /** + * GtkFlowBox::unselect-all: + * @box: the #GtkFlowBox on which the signal is emitted + * + * The ::unselect-all signal is a + * [keybinding signal][GtkBindingSignal] + * which gets emitted to unselect all children of the box, if + * the selection mode permits it. + * + * The default bindings for this signal is Ctrl-Shift-a. + */ + signals[UNSELECT_ALL] = g_signal_new ("unselect-all", + GTK_TYPE_FLOW_BOX, + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (GtkFlowBoxClass, unselect_all), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + widget_class->activate_signal = signals[ACTIVATE_CURSOR_CHILD]; + + binding_set = gtk_binding_set_by_class (class); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Home, 0, + GTK_MOVEMENT_BUFFER_ENDS, -1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Home, 0, + GTK_MOVEMENT_BUFFER_ENDS, -1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_End, 0, + GTK_MOVEMENT_BUFFER_ENDS, 1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_End, 0, + GTK_MOVEMENT_BUFFER_ENDS, 1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Up, 0, + GTK_MOVEMENT_DISPLAY_LINES, -1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Up, 0, + GTK_MOVEMENT_DISPLAY_LINES, -1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Down, 0, + GTK_MOVEMENT_DISPLAY_LINES, 1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Down, 0, + GTK_MOVEMENT_DISPLAY_LINES, 1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Up, 0, + GTK_MOVEMENT_PAGES, -1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Up, 0, + GTK_MOVEMENT_PAGES, -1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Page_Down, 0, + GTK_MOVEMENT_PAGES, 1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Page_Down, 0, + GTK_MOVEMENT_PAGES, 1); + + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Right, 0, + GTK_MOVEMENT_VISUAL_POSITIONS, 1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Right, 0, + GTK_MOVEMENT_VISUAL_POSITIONS, 1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_Left, 0, + GTK_MOVEMENT_VISUAL_POSITIONS, -1); + gtk_flow_box_add_move_binding (binding_set, GDK_KEY_KP_Left, 0, + GTK_MOVEMENT_VISUAL_POSITIONS, -1); + + gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, GDK_CONTROL_MASK, + "toggle-cursor-child", 0, NULL); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space, GDK_CONTROL_MASK, + "toggle-cursor-child", 0, NULL); + + gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK, + "select-all", 0); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK, + "unselect-all", 0); +} + +static void +gtk_flow_box_init (GtkFlowBox *box) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + gtk_widget_set_has_window (GTK_WIDGET (box), TRUE); + gtk_widget_set_redraw_on_allocate (GTK_WIDGET (box), TRUE); + + priv->orientation = GTK_ORIENTATION_HORIZONTAL; + priv->selection_mode = GTK_SELECTION_SINGLE; + priv->max_children_per_line = DEFAULT_MAX_CHILDREN_PER_LINE; + priv->column_spacing = 0; + priv->row_spacing = 0; + priv->activate_on_single_click = TRUE; + + priv->children = g_sequence_new (NULL); +} + + /* Public API {{{2 */ + +/** + * gtk_flow_box_new: + * + * Creates a GtkFlowBox. + * + * Returns: a new #GtkFlowBox container + * + * Since: 3.12 + */ +GtkWidget * +gtk_flow_box_new (void) +{ + return (GtkWidget *)g_object_new (GTK_TYPE_FLOW_BOX, NULL); +} + +/** + * gtk_flow_box_insert: + * @box: a #GtkFlowBox + * @widget: the #GtkWidget to add + * @position: the position to insert @child in + * + * Inserts the @widget into @box at @position. + * + * If a sort function is set, the widget will actually be inserted + * at the calculated position and this function has the same effect + * as gtk_container_add(). + * + * If @position is -1, or larger than the total number of children + * in the @box, then the @widget will be appended to the end. + * + * Since: 3.12 + */ +void +gtk_flow_box_insert (GtkFlowBox *box, + GtkWidget *widget, + gint position) +{ + GtkFlowBoxPrivate *priv; + GtkFlowBoxChild *child; + GSequenceIter *iter; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + priv = BOX_PRIV (box); + + if (GTK_IS_FLOW_BOX_CHILD (widget)) + child = GTK_FLOW_BOX_CHILD (widget); + else + { + child = GTK_FLOW_BOX_CHILD (gtk_flow_box_child_new ()); + gtk_widget_show (GTK_WIDGET (child)); + gtk_container_add (GTK_CONTAINER (child), widget); + } + + if (priv->sort_func != NULL) + iter = g_sequence_insert_sorted (priv->children, child, + (GCompareDataFunc)gtk_flow_box_sort, box); + else if (position == 0) + iter = g_sequence_prepend (priv->children, child); + else if (position == -1) + iter = g_sequence_append (priv->children, child); + else + { + GSequenceIter *pos; + pos = g_sequence_get_iter_at_pos (priv->children, position); + iter = g_sequence_insert_before (pos, child); + } + + CHILD_PRIV (child)->iter = iter; + gtk_widget_set_parent (GTK_WIDGET (child), GTK_WIDGET (box)); + gtk_flow_box_apply_filter (box, child); +} + +/** + * gtk_flow_box_get_child_at_index: + * @box: a #GtkFlowBox + * @idx: the position of the child + * + * Gets the nth child in the @box. + * + * Returns: (transfer none): the child widget, which will + * always be a #GtkFlowBoxChild + * + * Since: 3.12 + */ +GtkFlowBoxChild * +gtk_flow_box_get_child_at_index (GtkFlowBox *box, + gint idx) +{ + GSequenceIter *iter; + + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL); + + iter = g_sequence_get_iter_at_pos (BOX_PRIV (box)->children, idx); + if (iter) + return g_sequence_get (iter); + + return NULL; +} + +/** + * gtk_flow_box_set_hadjustment: + * @box: a #GtkFlowBox + * @adjustment: an adjustment which should be adjusted + * when the focus is moved among the descendents of @container + * + * Hooks up an adjustment to focus handling in @box. + * The adjustment is also used for autoscrolling during + * rubberband selection. See gtk_scrolled_window_get_hadjustment() + * for a typical way of obtaining the adjustment, and + * gtk_flow_box_set_vadjustment()for setting the vertical + * adjustment. + * + * The adjustments have to be in pixel units and in the same + * coordinate system as the allocation for immediate children + * of the box. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_hadjustment (GtkFlowBox *box, + GtkAdjustment *adjustment) +{ + GtkFlowBoxPrivate *priv; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); + + priv = BOX_PRIV (box); + + g_object_ref (adjustment); + if (priv->hadjustment) + g_object_unref (priv->hadjustment); + priv->hadjustment = adjustment; + gtk_container_set_focus_hadjustment (GTK_CONTAINER (box), adjustment); +} + +/** + * gtk_flow_box_set_vadjustment: + * @box: a #GtkFlowBox + * @adjustment: an adjustment which should be adjusted + * when the focus is moved among the descendents of @container + * + * Hooks up an adjustment to focus handling in @box. + * The adjustment is also used for autoscrolling during + * rubberband selection. See gtk_scrolled_window_get_vadjustment() + * for a typical way of obtaining the adjustment, and + * gtk_flow_box_set_hadjustment()for setting the horizontal + * adjustment. + * + * The adjustments have to be in pixel units and in the same + * coordinate system as the allocation for immediate children + * of the box. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_vadjustment (GtkFlowBox *box, + GtkAdjustment *adjustment) +{ + GtkFlowBoxPrivate *priv; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); + + priv = BOX_PRIV (box); + + g_object_ref (adjustment); + if (priv->vadjustment) + g_object_unref (priv->vadjustment); + priv->vadjustment = adjustment; + gtk_container_set_focus_vadjustment (GTK_CONTAINER (box), adjustment); +} + +/* Setters and getters {{{2 */ + +/** + * gtk_flow_box_get_homogeneous: + * @box: a #GtkFlowBox + * + * Returns whether the box is homogeneous (all children are the + * same size). See gtk_box_set_homogeneous(). + * + * Returns: %TRUE if the box is homogeneous. + * + * Since: 3.12 + */ +gboolean +gtk_flow_box_get_homogeneous (GtkFlowBox *box) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); + + return BOX_PRIV (box)->homogeneous; +} + +/** + * gtk_flow_box_set_homogeneous: + * @box: a #GtkFlowBox + * @homogeneous: %TRUE to create equal allotments, + * %FALSE for variable allotments + * + * Sets the #GtkFlowBox:homogeneous property of @box, controlling + * whether or not all children of @box are given equal space + * in the box. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_homogeneous (GtkFlowBox *box, + gboolean homogeneous) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + homogeneous = homogeneous != FALSE; + + if (BOX_PRIV (box)->homogeneous != homogeneous) + { + BOX_PRIV (box)->homogeneous = homogeneous; + + g_object_notify (G_OBJECT (box), "homogeneous"); + gtk_widget_queue_resize (GTK_WIDGET (box)); + } +} + +/** + * gtk_flow_box_set_row_spacing: + * @box: a #GtkFlowBox + * @spacing: the spacing to use + * + * Sets the vertical space to add between children. + * See the #GtkFlowBox:row-spacing property. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_row_spacing (GtkFlowBox *box, + guint spacing) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (BOX_PRIV (box)->row_spacing != spacing) + { + BOX_PRIV (box)->row_spacing = spacing; + + gtk_widget_queue_resize (GTK_WIDGET (box)); + g_object_notify (G_OBJECT (box), "row-spacing"); + } +} + +/** + * gtk_flow_box_get_row_spacing: + * @box: a #GtkFlowBox + * + * Gets the vertical spacing. + * + * Returns: the vertical spacing + * + * Since: 3.12 + */ +guint +gtk_flow_box_get_row_spacing (GtkFlowBox *box) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); + + return BOX_PRIV (box)->row_spacing; +} + +/** + * gtk_flow_box_set_column_spacing: + * @box: a #GtkFlowBox + * @spacing: the spacing to use + * + * Sets the horizontal space to add between children. + * See the #GtkFlowBox:column-spacing property. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_column_spacing (GtkFlowBox *box, + guint spacing) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (BOX_PRIV (box)->column_spacing != spacing) + { + BOX_PRIV (box)->column_spacing = spacing; + + gtk_widget_queue_resize (GTK_WIDGET (box)); + g_object_notify (G_OBJECT (box), "column-spacing"); + } +} + +/** + * gtk_flow_box_get_column_spacing: + * @box: a #GtkFlowBox + * + * Gets the horizontal spacing. + * + * Returns: the horizontal spacing + * + * Since: 3.12 + */ +guint +gtk_flow_box_get_column_spacing (GtkFlowBox *box) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); + + return BOX_PRIV (box)->column_spacing; +} + +/** + * gtk_flow_box_set_min_children_per_line: + * @box: a #GtkFlowBox + * @n_children: the minimum number of children per line + * + * Sets the minimum number of children to line up + * in @box’s orientation before flowing. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_min_children_per_line (GtkFlowBox *box, + guint n_children) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (BOX_PRIV (box)->min_children_per_line != n_children) + { + BOX_PRIV (box)->min_children_per_line = n_children; + + gtk_widget_queue_resize (GTK_WIDGET (box)); + g_object_notify (G_OBJECT (box), "min-children-per-line"); + } +} + +/** + * gtk_flow_box_get_min_children_per_line: + * @box: a #GtkFlowBox + * + * Gets the minimum number of children per line. + * + * Returns: the minimum number of children per line + * + * Since: 3.12 + */ +guint +gtk_flow_box_get_min_children_per_line (GtkFlowBox *box) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); + + return BOX_PRIV (box)->min_children_per_line; +} + +/** + * gtk_flow_box_set_max_children_per_line: + * @box: a #GtkFlowBox + * @n_children: the maximum number of children per line + * + * Sets the maximum number of children to request and + * allocate space for in @box’s orientation. + * + * Setting the maximum number of children per line + * limits the overall natural size request to be no more + * than @n_children children long in the given orientation. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_max_children_per_line (GtkFlowBox *box, + guint n_children) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (BOX_PRIV (box)->max_children_per_line != n_children) + { + BOX_PRIV (box)->max_children_per_line = n_children; + + gtk_widget_queue_resize (GTK_WIDGET (box)); + g_object_notify (G_OBJECT (box), "max-children-per-line"); + } +} + +/** + * gtk_flow_box_get_max_children_per_line: + * @box: a #GtkFlowBox + * + * Gets the maximum number of children per line. + * + * Returns: the maximum number of children per line + * + * Since: 3.12 + */ +guint +gtk_flow_box_get_max_children_per_line (GtkFlowBox *box) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); + + return BOX_PRIV (box)->max_children_per_line; +} + +/** + * gtk_flow_box_set_activate_on_single_click: + * @box: a #GtkFlowBox + * @single: %TRUE to emit child-activated on a single click + * + * If @single is %TRUE, children will be activated when you click + * on them, otherwise you need to double-click. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box, + gboolean single) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + single = single != FALSE; + + if (BOX_PRIV (box)->activate_on_single_click != single) + { + BOX_PRIV (box)->activate_on_single_click = single; + g_object_notify (G_OBJECT (box), "activate-on-single-click"); + } +} + +/** + * gtk_flow_box_get_activate_on_single_click: + * @box: a #GtkFlowBox + * + * Returns whether children activate on single clicks. + * + * Returns: %TRUE if children are activated on single click, + * %FALSE otherwise + * + * Since: 3.12 + */ +gboolean +gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), FALSE); + + return BOX_PRIV (box)->activate_on_single_click; +} + + /* Selection handling {{{2 */ + +/** + * gtk_flow_box_get_selected_children: + * @box: a #GtkFlowBox + * + * Creates a list of all selected children. + * + * Returns: (element-type GtkFlowBoxChild) (transfer container): + * A #GList containing the #GtkWidget for each selected child. + * Free with g_list_free() when done. + * + * Since: 3.12 + */ +GList * +gtk_flow_box_get_selected_children (GtkFlowBox *box) +{ + GtkFlowBoxChild *child; + GSequenceIter *iter; + GList *selected = NULL; + + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), NULL); + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + child = g_sequence_get (iter); + if (CHILD_PRIV (child)->selected) + selected = g_list_prepend (selected, child); + } + + return g_list_reverse (selected); +} + +/** + * gtk_flow_box_select_child: + * @box: a #GtkFlowBox + * @child: a child of @box + * + * Selects a single child of @box, if the selection + * mode allows it. + * + * Since: 3.12 + */ +void +gtk_flow_box_select_child (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); + + gtk_flow_box_select_child_internal (box, child); +} + +/** + * gtk_flow_box_unselect_child: + * @box: a #GtkFlowBox + * @child: a child of @box + * + * Unselects a single child of @box, if the selection + * mode allows it. + * + * Since: 3.12 + */ +void +gtk_flow_box_unselect_child (GtkFlowBox *box, + GtkFlowBoxChild *child) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + g_return_if_fail (GTK_IS_FLOW_BOX_CHILD (child)); + + gtk_flow_box_unselect_child_internal (box, child); +} + +/** + * gtk_flow_box_select_all: + * @box: a #GtkFlowBox + * + * Select all children of @box, if the selection + * mode allows it. + * + * Since: 3.12 + */ +void +gtk_flow_box_select_all (GtkFlowBox *box) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (BOX_PRIV (box)->selection_mode != GTK_SELECTION_MULTIPLE) + return; + + if (g_sequence_get_length (BOX_PRIV (box)->children) > 0) + { + gtk_flow_box_select_all_between (box, NULL, NULL, FALSE); + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); + } +} + +/** + * gtk_flow_box_unselect_all: + * @box: a #GtkFlowBox + * + * Unselect all children of @box, if the selection + * mode allows it. + * + * Since: 3.12 + */ +void +gtk_flow_box_unselect_all (GtkFlowBox *box) +{ + gboolean dirty = FALSE; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (BOX_PRIV (box)->selection_mode == GTK_SELECTION_BROWSE) + return; + + dirty = gtk_flow_box_unselect_all_internal (box); + + if (dirty) + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); +} + +/** + * GtkFlowBoxForeachFunc: + * @box: a #GtkFlowBox + * @child: a #GtkFlowBoxChild + * @user_data: (closure): user data + * + * A function used by gtk_flow_box_selected_foreach(). + * It will be called on every selected child of the @box. + * + * Since: 3.12 + */ + +/** + * gtk_flow_box_selected_foreach: + * @box: a #GtkFlowBox + * @func: (scope call): the function to call for each selected child + * @data: user data to pass to the function + * + * Calls a function for each selected child. + * + * Note that the selection cannot be modified from within + * this function. + * + * Since: 3.12 + */ +void +gtk_flow_box_selected_foreach (GtkFlowBox *box, + GtkFlowBoxForeachFunc func, + gpointer data) +{ + GtkFlowBoxChild *child; + GSequenceIter *iter; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + for (iter = g_sequence_get_begin_iter (BOX_PRIV (box)->children); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + child = g_sequence_get (iter); + if (CHILD_PRIV (child)->selected) + (*func) (box, child, data); + } +} + +/** + * gtk_flow_box_set_selection_mode: + * @box: a #GtkFlowBox + * @mode: the new selection mode + * + * Sets how selection works in @box. + * See #GtkSelectionMode for details. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_selection_mode (GtkFlowBox *box, + GtkSelectionMode mode) +{ + gboolean dirty = FALSE; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (mode == BOX_PRIV (box)->selection_mode) + return; + + if (mode == GTK_SELECTION_NONE || + BOX_PRIV (box)->selection_mode == GTK_SELECTION_MULTIPLE) + { + dirty = gtk_flow_box_unselect_all_internal (box); + BOX_PRIV (box)->selected_child = NULL; + } + + BOX_PRIV (box)->selection_mode = mode; + + g_object_notify (G_OBJECT (box), "selection-mode"); + + if (dirty) + g_signal_emit (box, signals[SELECTED_CHILDREN_CHANGED], 0); +} + +/** + * gtk_flow_box_get_selection_mode: + * @box: a #GtkFlowBox + * + * Gets the selection mode of @box. + * + * Returns: the #GtkSelectionMode + * + * Since: 3.12 + */ +GtkSelectionMode +gtk_flow_box_get_selection_mode (GtkFlowBox *box) +{ + g_return_val_if_fail (GTK_IS_FLOW_BOX (box), GTK_SELECTION_SINGLE); + + return BOX_PRIV (box)->selection_mode; +} + +/* Filtering {{{2 */ + +/** + * GtkFlowBoxFilterFunc: + * @child: a #GtkFlowBoxChild that may be filtered + * @user_data: (closure): user data + * + * A function that will be called whenrever a child changes + * or is added. It lets you control if the child should be + * visible or not. + * + * Returns: %TRUE if the row should be visible, %FALSE otherwise + * + * Since: 3.12 + */ + +/** + * gtk_flow_box_set_filter_func: + * @box: a #GtkFlowBox + * @filter_func: (closure user_data) (allow-none): callback that + * lets you filter which children to show + * @user_data: user data passed to @filter_func + * @destroy: destroy notifier for @user_data + * + * By setting a filter function on the @box one can decide dynamically + * which of the children to show. For instance, to implement a search + * function that only shows the children matching the search terms. + * + * The @filter_func will be called for each child after the call, and + * it will continue to be called each time a child changes (via + * gtk_flow_box_child_changed()) or when gtk_flow_box_invalidate_filter() + * is called. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_filter_func (GtkFlowBox *box, + GtkFlowBoxFilterFunc filter_func, + gpointer user_data, + GDestroyNotify destroy) +{ + GtkFlowBoxPrivate *priv; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + priv = BOX_PRIV (box); + + if (priv->filter_destroy != NULL) + priv->filter_destroy (priv->filter_data); + + priv->filter_func = filter_func; + priv->filter_data = user_data; + priv->filter_destroy = destroy; + + gtk_flow_box_apply_filter_all (box); +} + +/** + * gtk_flow_box_invalidate_filter: + * @box: a #GtkFlowBox + * + * Updates the filtering for all children. + * + * Call this function when the result of the filter + * function on the @box is changed due ot an external + * factor. For instance, this would be used if the + * filter function just looked for a specific search + * term, and the entry with the string has changed. + * + * Since: 3.12 + */ +void +gtk_flow_box_invalidate_filter (GtkFlowBox *box) +{ + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + if (BOX_PRIV (box)->filter_func != NULL) + gtk_flow_box_apply_filter_all (box); +} + +/* Sorting {{{2 */ + +/** + * GtkFlowBoxSortFunc: + * @child1: the first child + * @child2: the second child + * @user_data: (closure): user data + * + * A function to compare two children to determine which + * should come first. + * + * Returns: < 0 if @child1 should be before @child2, 0 if + * the are equal, and > 0 otherwise + * + * Since: 3.12 + */ + +/** + * gtk_flow_box_set_sort_func: + * @box: a #GtkFlowBox + * @sort_func: (closure user_data) (allow-none): the sort function + * @user_data: user data passed to @sort_func + * @destroy: destroy notifier for @user_data + * + * By setting a sort function on the @box, one can dynamically + * reorder the children of the box, based on the contents of + * the children. + * + * The @sort_func will be called for each child after the call, + * and will continue to be called each time a child changes (via + * gtk_flow_box_child_changed()) and when gtk_flow_box_invalidate_sort() + * is called. + * + * Since: 3.12 + */ +void +gtk_flow_box_set_sort_func (GtkFlowBox *box, + GtkFlowBoxSortFunc sort_func, + gpointer user_data, + GDestroyNotify destroy) +{ + GtkFlowBoxPrivate *priv; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + priv = BOX_PRIV (box); + + if (priv->sort_destroy != NULL) + priv->sort_destroy (priv->sort_data); + + priv->sort_func = sort_func; + priv->sort_data = user_data; + priv->sort_destroy = destroy; + + gtk_flow_box_invalidate_sort (box); +} + +static gint +gtk_flow_box_sort (GtkFlowBoxChild *a, + GtkFlowBoxChild *b, + GtkFlowBox *box) +{ + GtkFlowBoxPrivate *priv = BOX_PRIV (box); + + return priv->sort_func (a, b, priv->sort_data); +} + +/** + * gtk_flow_box_invalidate_sort: + * @box: a #GtkFlowBox + * + * Updates the sorting for all children. + * + * Call this when the result of the sort function on + * @box is changed due to an external factor. + * + * Since: 3.12 + */ +void +gtk_flow_box_invalidate_sort (GtkFlowBox *box) +{ + GtkFlowBoxPrivate *priv; + + g_return_if_fail (GTK_IS_FLOW_BOX (box)); + + priv = BOX_PRIV (box); + + if (priv->sort_func != NULL) + { + g_sequence_sort (priv->children, + (GCompareDataFunc)gtk_flow_box_sort, box); + gtk_widget_queue_resize (GTK_WIDGET (box)); + } +} + +/* vim:set foldmethod=marker expandtab: */ diff --git a/src/interface-gtk/gtkflowbox.h b/src/interface-gtk/gtkflowbox.h new file mode 100644 index 0000000..6f0549f --- /dev/null +++ b/src/interface-gtk/gtkflowbox.h @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2010 Openismus GmbH + * Copyright (C) 2013 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, see . + + * + * Authors: + * Tristan Van Berkom + * Matthias Clasen + * William Jon McCann + */ + +#ifndef __GTK_FLOW_BOX_H__ +#define __GTK_FLOW_BOX_H__ + +#include + +G_BEGIN_DECLS + + +#define GTK_TYPE_FLOW_BOX (gtk_flow_box_get_type ()) +#define GTK_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBox)) +#define GTK_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass)) +#define GTK_IS_FLOW_BOX(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX)) +#define GTK_IS_FLOW_BOX_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX)) +#define GTK_FLOW_BOX_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_FLOW_BOX, GtkFlowBoxClass)) + +typedef struct _GtkFlowBox GtkFlowBox; +typedef struct _GtkFlowBoxClass GtkFlowBoxClass; + +typedef struct _GtkFlowBoxChild GtkFlowBoxChild; +typedef struct _GtkFlowBoxChildClass GtkFlowBoxChildClass; + +struct _GtkFlowBox +{ + GtkContainer container; +}; + +struct _GtkFlowBoxClass +{ + GtkContainerClass parent_class; + + void (*child_activated) (GtkFlowBox *box, + GtkFlowBoxChild *child); + void (*selected_children_changed) (GtkFlowBox *box); + void (*activate_cursor_child) (GtkFlowBox *box); + void (*toggle_cursor_child) (GtkFlowBox *box); + void (*move_cursor) (GtkFlowBox *box, + GtkMovementStep step, + gint count); + void (*select_all) (GtkFlowBox *box); + void (*unselect_all) (GtkFlowBox *box); + + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); + void (*_gtk_reserved3) (void); + void (*_gtk_reserved4) (void); + void (*_gtk_reserved5) (void); + void (*_gtk_reserved6) (void); +}; + +#define GTK_TYPE_FLOW_BOX_CHILD (gtk_flow_box_child_get_type ()) +#define GTK_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChild)) +#define GTK_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass)) +#define GTK_IS_FLOW_BOX_CHILD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_FLOW_BOX_CHILD)) +#define GTK_IS_FLOW_BOX_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FLOW_BOX_CHILD)) +#define GTK_FLOW_BOX_CHILD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EG_TYPE_FLOW_BOX_CHILD, GtkFlowBoxChildClass)) + +struct _GtkFlowBoxChild +{ + GtkBin parent_instance; +}; + +struct _GtkFlowBoxChildClass +{ + GtkBinClass parent_class; + + void (* activate) (GtkFlowBoxChild *child); + + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); +}; + +GType gtk_flow_box_child_get_type (void) G_GNUC_CONST; +GtkWidget* gtk_flow_box_child_new (void); +gint gtk_flow_box_child_get_index (GtkFlowBoxChild *child); +gboolean gtk_flow_box_child_is_selected (GtkFlowBoxChild *child); +void gtk_flow_box_child_changed (GtkFlowBoxChild *child); + + +GType gtk_flow_box_get_type (void) G_GNUC_CONST; + +GtkWidget *gtk_flow_box_new (void); +void gtk_flow_box_set_homogeneous (GtkFlowBox *box, + gboolean homogeneous); +gboolean gtk_flow_box_get_homogeneous (GtkFlowBox *box); +void gtk_flow_box_set_row_spacing (GtkFlowBox *box, + guint spacing); +guint gtk_flow_box_get_row_spacing (GtkFlowBox *box); + +void gtk_flow_box_set_column_spacing (GtkFlowBox *box, + guint spacing); +guint gtk_flow_box_get_column_spacing (GtkFlowBox *box); + +void gtk_flow_box_set_min_children_per_line (GtkFlowBox *box, + guint n_children); +guint gtk_flow_box_get_min_children_per_line (GtkFlowBox *box); + +void gtk_flow_box_set_max_children_per_line (GtkFlowBox *box, + guint n_children); +guint gtk_flow_box_get_max_children_per_line (GtkFlowBox *box); +void gtk_flow_box_set_activate_on_single_click (GtkFlowBox *box, + gboolean single); +gboolean gtk_flow_box_get_activate_on_single_click (GtkFlowBox *box); + +void gtk_flow_box_insert (GtkFlowBox *box, + GtkWidget *widget, + gint position); +GtkFlowBoxChild *gtk_flow_box_get_child_at_index (GtkFlowBox *box, + gint idx); + +typedef void (* GtkFlowBoxForeachFunc) (GtkFlowBox *box, + GtkFlowBoxChild *child, + gpointer user_data); + +void gtk_flow_box_selected_foreach (GtkFlowBox *box, + GtkFlowBoxForeachFunc func, + gpointer data); +GList *gtk_flow_box_get_selected_children (GtkFlowBox *box); +void gtk_flow_box_select_child (GtkFlowBox *box, + GtkFlowBoxChild *child); +void gtk_flow_box_unselect_child (GtkFlowBox *box, + GtkFlowBoxChild *child); +void gtk_flow_box_select_all (GtkFlowBox *box); +void gtk_flow_box_unselect_all (GtkFlowBox *box); +void gtk_flow_box_set_selection_mode (GtkFlowBox *box, + GtkSelectionMode mode); +GtkSelectionMode gtk_flow_box_get_selection_mode (GtkFlowBox *box); +void gtk_flow_box_set_hadjustment (GtkFlowBox *box, + GtkAdjustment *adjustment); +void gtk_flow_box_set_vadjustment (GtkFlowBox *box, + GtkAdjustment *adjustment); + +typedef gboolean (*GtkFlowBoxFilterFunc) (GtkFlowBoxChild *child, + gpointer user_data); + +void gtk_flow_box_set_filter_func (GtkFlowBox *box, + GtkFlowBoxFilterFunc filter_func, + gpointer user_data, + GDestroyNotify destroy); +void gtk_flow_box_invalidate_filter (GtkFlowBox *box); + +typedef gint (*GtkFlowBoxSortFunc) (GtkFlowBoxChild *child1, + GtkFlowBoxChild *child2, + gpointer user_data); + +void gtk_flow_box_set_sort_func (GtkFlowBox *box, + GtkFlowBoxSortFunc sort_func, + gpointer user_data, + GDestroyNotify destroy); +void gtk_flow_box_invalidate_sort (GtkFlowBox *box); + +G_END_DECLS + + +#endif /* __GTK_FLOW_BOX_H__ */ diff --git a/src/interface-gtk/interface-gtk.cpp b/src/interface-gtk/interface-gtk.cpp new file mode 100644 index 0000000..9b0b0aa --- /dev/null +++ b/src/interface-gtk/interface-gtk.cpp @@ -0,0 +1,759 @@ +/* + * 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 . + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include +#include + +/* + * FIXME: Because of gdk_threads_enter(). + * The only way to do it in Gtk3 style would be using + * idle callbacks into the main thread and sync barriers (inefficient!) + * or doing it single-threaded and ticking the Gtk main loop + * (may be inefficient since gtk_events_pending() is doing + * syscalls; however that may be ailed by doing it less frequently). + */ +#define GDK_DISABLE_DEPRECATION_WARNINGS +#include + +#include +#include "gtk-info-popup.h" + +#include +#include + +#include "sciteco.h" +#include "string-utils.h" +#include "cmdline.h" +#include "qregisters.h" +#include "ring.h" +#include "interface.h" +#include "interface-gtk.h" + +/* + * Signal handlers (e.g. for handling SIGTERM) are only + * available on Unix and beginning with v2.30, while + * we still support v2.28. + * Handlers using `signal()` cannot be used easily for + * this purpose. + */ +#if defined(G_OS_UNIX) && GLIB_CHECK_VERSION(2,30,0) +#include +#define SCITECO_HANDLE_SIGNALS +#endif + +namespace SciTECO { + +extern "C" { + +static void scintilla_notify(ScintillaObject *sci, uptr_t idFrom, + SCNotification *notify, gpointer user_data); + +static gpointer exec_thread_cb(gpointer data); +static gboolean cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, + gpointer user_data); +static gboolean window_delete_cb(GtkWidget *w, GdkEventAny *e, + gpointer user_data); + +static gboolean sigterm_handler(gpointer user_data) G_GNUC_UNUSED; + +} /* extern "C" */ + +#define UNNAMED_FILE "(Unnamed)" + +void +ViewGtk::initialize_impl(void) +{ + gint events; + + gdk_threads_enter(); + + sci = SCINTILLA(scintilla_new()); + /* + * We don't want the object to be destroyed + * when it is removed from the vbox. + */ + g_object_ref_sink(G_OBJECT(sci)); + + scintilla_set_id(sci, 0); + + gtk_widget_set_size_request(get_widget(), 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(get_widget(), FALSE); + events = gtk_widget_get_events(get_widget()); + events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); + events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); + gtk_widget_set_events(get_widget(), events); + + g_signal_connect(G_OBJECT(sci), SCINTILLA_NOTIFY, + G_CALLBACK(scintilla_notify), NULL); + + /* + * setup() calls Scintilla messages, so we must unlock + * here already to avoid deadlocks. + */ + gdk_threads_leave(); + + setup(); +} + +void +InterfaceGtk::main_impl(int &argc, char **&argv) +{ + static const Cmdline empty_cmdline; + GtkWidget *overlay_widget; + GtkWidget *info_content; + + /* + * g_thread_init() is required prior to v2.32 + * (we still support v2.28) but generates a warning + * on newer versions. + */ +#if !GLIB_CHECK_VERSION(2,32,0) + g_thread_init(NULL); +#endif + gdk_threads_init(); + gtk_init(&argc, &argv); + + /* + * The event queue is initialized now, so we can + * pass it as user data to C-linkage callbacks. + */ + event_queue = g_async_queue_new(); + + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), PACKAGE_NAME); + g_signal_connect(G_OBJECT(window), "delete-event", + G_CALLBACK(window_delete_cb), event_queue); + + vbox = gtk_vbox_new(FALSE, 0); + + info_current = g_strdup(PACKAGE_NAME); + + /* + * 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. + */ + overlay_widget = gtk_overlay_new(); + event_box_widget = gtk_event_box_new(); + gtk_event_box_set_above_child(GTK_EVENT_BOX(event_box_widget), TRUE); + gtk_container_add(GTK_CONTAINER(overlay_widget), event_box_widget); + gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0); + + info_widget = gtk_info_bar_new(); + info_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_widget)); + message_widget = gtk_label_new(""); + gtk_misc_set_alignment(GTK_MISC(message_widget), 0., 0.); + gtk_container_add(GTK_CONTAINER(info_content), message_widget); + gtk_box_pack_start(GTK_BOX(vbox), info_widget, FALSE, FALSE, 0); + + cmdline_widget = gtk_entry_new(); + gtk_entry_set_has_frame(GTK_ENTRY(cmdline_widget), FALSE); + gtk_editable_set_editable(GTK_EDITABLE(cmdline_widget), FALSE); + widget_set_font(cmdline_widget, "Courier"); + g_signal_connect(G_OBJECT(cmdline_widget), "key-press-event", + G_CALLBACK(cmdline_key_pressed_cb), event_queue); + gtk_box_pack_start(GTK_BOX(vbox), cmdline_widget, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(window), vbox); + + /* + * Popup widget will be shown in the bottom + * of the overlay widget (i.e. the Scintilla views), + * filling the entire width. + */ + popup_widget = gtk_info_popup_new(); + gtk_widget_set_halign(popup_widget, GTK_ALIGN_FILL); + gtk_widget_set_valign(popup_widget, GTK_ALIGN_END); + gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), popup_widget); + + gtk_widget_grab_focus(cmdline_widget); + + cmdline_update(&empty_cmdline); +} + +void +InterfaceGtk::vmsg_impl(MessageType type, const gchar *fmt, va_list ap) +{ + static const GtkMessageType type2gtk[] = { + /* [MSG_USER] = */ GTK_MESSAGE_OTHER, + /* [MSG_INFO] = */ GTK_MESSAGE_INFO, + /* [MSG_WARNING] = */ GTK_MESSAGE_WARNING, + /* [MSG_ERROR] = */ GTK_MESSAGE_ERROR + }; + + va_list aq; + gchar buf[255]; + + /* + * stdio_vmsg() leaves `ap` undefined and we are expected + * to do the same and behave like vprintf(). + */ + va_copy(aq, ap); + stdio_vmsg(type, fmt, ap); + g_vsnprintf(buf, sizeof(buf), fmt, aq); + va_end(aq); + + gdk_threads_enter(); + + gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget), + type2gtk[type]); + gtk_label_set_text(GTK_LABEL(message_widget), buf); + + if (type == MSG_ERROR) + gtk_widget_error_bell(window); + + gdk_threads_leave(); +} + +void +InterfaceGtk::msg_clear(void) +{ + gdk_threads_enter(); + + gtk_info_bar_set_message_type(GTK_INFO_BAR(info_widget), + GTK_MESSAGE_OTHER); + gtk_label_set_text(GTK_LABEL(message_widget), ""); + + gdk_threads_leave(); +} + +void +InterfaceGtk::show_view_impl(ViewGtk *view) +{ + current_view = view; +} + +void +InterfaceGtk::info_update_impl(const QRegister *reg) +{ + gchar *name = String::canonicalize_ctl(reg->name); + + g_free(info_current); + info_current = g_strconcat(PACKAGE_NAME " - ", + name, NIL); + g_free(name); +} + +void +InterfaceGtk::info_update_impl(const Buffer *buffer) +{ + g_free(info_current); + info_current = g_strconcat(PACKAGE_NAME " - ", + buffer->filename ? : UNNAMED_FILE, + buffer->dirty ? "*" : "", NIL); +} + +void +InterfaceGtk::cmdline_insert_chr(gint &pos, gchar chr) +{ + gchar buffer[5+1]; + + /* + * NOTE: This mapping is similar to + * View::set_representations() + */ + switch (chr) { + case CTL_KEY_ESC: + strcpy(buffer, "$"); + break; + case '\r': + strcpy(buffer, ""); + break; + case '\n': + strcpy(buffer, ""); + break; + case '\t': + strcpy(buffer, ""); + break; + default: + if (IS_CTL(chr)) { + buffer[0] = '^'; + buffer[1] = CTL_ECHO(chr); + buffer[2] = '\0'; + } else { + buffer[0] = chr; + buffer[1] = '\0'; + } + } + + gtk_editable_insert_text(GTK_EDITABLE(cmdline_widget), + buffer, -1, &pos); +} + +void +InterfaceGtk::cmdline_update_impl(const Cmdline *cmdline) +{ + gint pos = 1; + gint cmdline_len; + + gdk_threads_enter(); + + /* + * We don't know if the new command line is similar to + * the old one, so we can just as well rebuild it. + */ + gtk_entry_set_text(GTK_ENTRY(cmdline_widget), "*"); + + /* format effective command line */ + for (guint i = 0; i < cmdline->len; i++) + cmdline_insert_chr(pos, (*cmdline)[i]); + /* save end of effective command line */ + cmdline_len = pos; + + /* format rubbed out command line */ + for (guint i = cmdline->len; i < cmdline->len+cmdline->rubout_len; i++) + cmdline_insert_chr(pos, (*cmdline)[i]); + + /* set cursor after effective command line */ + gtk_editable_set_position(GTK_EDITABLE(cmdline_widget), cmdline_len); + + gdk_threads_leave(); +} + +void +InterfaceGtk::popup_add_impl(PopupEntryType type, + const gchar *name, bool highlight) +{ + static const GtkInfoPopupEntryType type2gtk[] = { + /* [POPUP_PLAIN] = */ GTK_INFO_POPUP_PLAIN, + /* [POPUP_FILE] = */ GTK_INFO_POPUP_FILE, + /* [POPUP_DIRECTORY] = */ GTK_INFO_POPUP_DIRECTORY + }; + + gdk_threads_enter(); + + gtk_info_popup_add(GTK_INFO_POPUP(popup_widget), + type2gtk[type], name, highlight); + + gdk_threads_leave(); +} + +void +InterfaceGtk::popup_show_impl(void) +{ + gdk_threads_enter(); + + if (gtk_widget_get_visible(popup_widget)) + gtk_info_popup_scroll_page(GTK_INFO_POPUP(popup_widget)); + else + gtk_widget_show(popup_widget); + + gdk_threads_leave(); +} + +void +InterfaceGtk::popup_clear_impl(void) +{ + gdk_threads_enter(); + + if (gtk_widget_get_visible(popup_widget)) { + gtk_widget_hide(popup_widget); + gtk_info_popup_clear(GTK_INFO_POPUP(popup_widget)); + } + + gdk_threads_leave(); +} + +void +InterfaceGtk::widget_set_font(GtkWidget *widget, const gchar *font_name) +{ + PangoFontDescription *font_desc; + + font_desc = pango_font_description_from_string(font_name); + gtk_widget_modify_font(widget, font_desc); + pango_font_description_free(font_desc); +} + +void +InterfaceGtk::event_loop_impl(void) +{ + GThread *thread; + + /* + * Assign an icon to the window. + * If the file could not be found, we fail silently. + * FIXME: On Windows, it may be better to load the icon compiled + * as a resource into the binary. + * FIXME: Provide all the different icon sizes we have + * (gtk_window_set_default_icon_list()). + */ + gtk_window_set_default_icon_from_file(SCITECODATADIR G_DIR_SEPARATOR_S + "sciteco-48.png", + NULL); + + /* + * When changing views, the new widget is not + * added immediately to avoid flickering in the GUI. + * It is only updated once per key press and only + * if it really changed. + * Therefore we must add the current view to the + * window initially. + * For the same reason, window title updates are + * deferred to once after every key press, so we must + * set the window title initially. + */ + if (current_view) { + current_view_widget = current_view->get_widget(); + gtk_container_add(GTK_CONTAINER(event_box_widget), + current_view_widget); + } + gtk_window_set_title(GTK_WINDOW(window), info_current); + + gtk_widget_show_all(window); + /* don't show popup by default */ + gtk_widget_hide(popup_widget); + + /* + * SIGTERM emulates the "Close" key just like when + * closing the window if supported by this version of glib. + * Note that this replaces SciTECO's default SIGTERM handler + * so it will additionally raise(SIGINT). + */ +#ifdef SCITECO_HANDLE_SIGNALS + g_unix_signal_add(SIGTERM, sigterm_handler, event_queue); +#endif + + /* + * Start up SciTECO execution thread. + * Whenever it needs to send a Scintilla message + * it locks the GDK mutex. + */ + thread = g_thread_new("sciteco-exec", + exec_thread_cb, event_queue); + + /* + * NOTE: The watchers do not modify any GTK objects + * using one of the methods that lock the GDK mutex. + * This is from now on reserved to the execution + * thread. Therefore there can be no dead-locks. + */ + gdk_threads_enter(); + gtk_main(); + gdk_threads_leave(); + + /* + * This usually means that the user requested program + * termination and the execution thread called + * gtk_main_quit(). + * We still wait for the execution thread to shut down + * properly. This also frees `thread`. + */ + g_thread_join(thread); + + /* + * Make sure the window is hidden + * now already, as there may be code that has to be + * executed in batch mode. + */ + gtk_widget_hide(window); +} + +static gpointer +exec_thread_cb(gpointer data) +{ + GAsyncQueue *event_queue = (GAsyncQueue *)data; + + for (;;) { + GdkEventKey *event = (GdkEventKey *)g_async_queue_pop(event_queue); + + bool is_shift = event->state & GDK_SHIFT_MASK; + bool is_ctl = event->state & GDK_CONTROL_MASK; + + try { + sigint_occurred = FALSE; + interface.handle_key_press(is_shift, is_ctl, event->keyval); + sigint_occurred = FALSE; + } catch (Quit) { + /* + * SciTECO should terminate, so we exit + * this thread. + * The main loop will terminate and + * event_loop() will return. + */ + gdk_event_free((GdkEvent *)event); + + gdk_threads_enter(); + gtk_main_quit(); + gdk_threads_leave(); + break; + } + + gdk_event_free((GdkEvent *)event); + } + + return NULL; +} + +void +InterfaceGtk::handle_key_press(bool is_shift, bool is_ctl, guint keyval) +{ + GdkWindow *view_window; + ViewGtk *last_view = current_view; + + /* + * Avoid redraws of the current view by freezing updates + * on the view's GDK window (we're running in parallel + * to the main loop so there could be frequent redraws). + * By freezing updates, the behaviour is similar to + * the Curses UI. + */ + gdk_threads_enter(); + view_window = gtk_widget_get_parent_window(event_box_widget); + gdk_window_freeze_updates(view_window); + gdk_threads_leave(); + + switch (keyval) { + case GDK_KEY_Escape: + cmdline.keypress(CTL_KEY_ESC); + break; + case GDK_KEY_BackSpace: + cmdline.keypress(CTL_KEY('H')); + break; + case GDK_KEY_Tab: + cmdline.keypress('\t'); + break; + case GDK_KEY_Return: + cmdline.keypress('\n'); + break; + + /* + * Function key macros + */ +#define FN(KEY, MACRO) \ + case GDK_KEY_##KEY: cmdline.fnmacro(#MACRO); break +#define FNS(KEY, MACRO) \ + case GDK_KEY_##KEY: cmdline.fnmacro(is_shift ? "S" #MACRO : #MACRO); break + FN(Down, DOWN); FN(Up, UP); + FNS(Left, LEFT); FNS(Right, RIGHT); + FN(KP_Down, DOWN); FN(KP_Up, UP); + FNS(KP_Left, LEFT); FNS(KP_Right, RIGHT); + FNS(Home, HOME); + case GDK_KEY_F1...GDK_KEY_F35: { + gchar macro_name[3+1]; + + g_snprintf(macro_name, sizeof(macro_name), + "F%d", keyval - GDK_KEY_F1 + 1); + cmdline.fnmacro(macro_name); + break; + } + FNS(Delete, DC); + FNS(Insert, IC); + FN(Page_Down, NPAGE); FN(Page_Up, PPAGE); + FNS(Print, PRINT); + FN(KP_Home, A1); FN(KP_Prior, A3); + FN(KP_Begin, B2); + FN(KP_End, C1); FN(KP_Next, C3); + FNS(End, END); + FNS(Help, HELP); + FN(Close, CLOSE); +#undef FNS +#undef FN + + /* + * Control keys and keys with printable representation + */ + default: + gunichar u = gdk_keyval_to_unicode(keyval); + + if (u && g_unichar_to_utf8(u, NULL) == 1) { + gchar key; + + g_unichar_to_utf8(u, &key); + if (key > 0x7F) + break; + if (is_ctl) + key = CTL_KEY(g_ascii_toupper(key)); + + cmdline.keypress(key); + } + } + + /* + * The info area is updated very often and setting the + * window title each time it is updated is VERY costly. + * So we set it here once after every keypress even if the + * info line did not change. + * View changes are also only applied here to the GTK + * window even though GDK updates have been frozen since + * the size reallocations are very costly. + */ + gdk_threads_enter(); + + if (current_view != last_view) { + /* + * The last view's object is not guaranteed to + * still exist. + * However its widget is, due to reference counting. + */ + if (current_view_widget) + gtk_container_remove(GTK_CONTAINER(event_box_widget), + current_view_widget); + + current_view_widget = current_view->get_widget(); + + gtk_container_add(GTK_CONTAINER(event_box_widget), + current_view_widget); + gtk_widget_show(current_view_widget); + } + + gtk_window_set_title(GTK_WINDOW(window), info_current); + + gdk_window_thaw_updates(view_window); + + gdk_threads_leave(); +} + +InterfaceGtk::~InterfaceGtk() +{ + g_free(info_current); + if (popup_widget) + gtk_widget_destroy(popup_widget); + if (window) + gtk_widget_destroy(window); + + scintilla_release_resources(); + + if (event_queue) { + GdkEvent *e; + + while ((e = (GdkEvent *)g_async_queue_try_pop(event_queue))) + gdk_event_free(e); + + g_async_queue_unref(event_queue); + } +} + +/* + * GTK+ callbacks + */ + +static void +scintilla_notify(ScintillaObject *sci, uptr_t idFrom, + SCNotification *notify, gpointer user_data) +{ + interface.process_notify(notify); +} + +static gboolean +cmdline_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, + gpointer user_data) +{ + GAsyncQueue *event_queue = (GAsyncQueue *)user_data; + + bool is_ctl = event->state & GDK_CONTROL_MASK; + +#ifdef DEBUG + g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n", + event->string, *event->string, + event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK); +#endif + + g_async_queue_lock(event_queue); + + if (g_async_queue_length_unlocked(event_queue) >= 0 && + is_ctl && gdk_keyval_to_upper(event->keyval) == GDK_KEY_C) { + /* + * Handle asynchronous interruptions if CTRL+C is pressed. + * This will usually send SIGINT to the entire process + * group and set `sigint_occurred`. + * If the execution thread is currently blocking, + * the key is delivered like an ordinary key press. + */ + interrupt(); + } else { + /* + * Copies the key-press event, since it must be evaluated + * by the exec_thread_cb. This is costly, but since we're + * using the event queue as a kind of keyboard buffer, + * who cares? + */ + g_async_queue_push_unlocked(event_queue, + gdk_event_copy((GdkEvent *)event)); + } + + g_async_queue_unlock(event_queue); + + return TRUE; +} + +static gboolean +window_delete_cb(GtkWidget *w, GdkEventAny *e, gpointer user_data) +{ + GAsyncQueue *event_queue = (GAsyncQueue *)user_data; + GdkEventKey *close_event; + + /* + * Emulate that the "close" key was pressed + * which may then be handled by the execution thread + * which invokes the appropriate "function key macro" + * if it exists. Its default action will ensure that + * the execution thread shuts down and the main loop + * will eventually terminate. + */ + close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS); + close_event->window = gtk_widget_get_parent_window(w); + close_event->keyval = GDK_KEY_Close; + + g_async_queue_push(event_queue, close_event); + + return TRUE; +} + +static gboolean +sigterm_handler(gpointer user_data) +{ + GAsyncQueue *event_queue = (GAsyncQueue *)user_data; + GdkEventKey *close_event; + + /* + * Since this handler replaces the default one, we + * also have to make sure it interrupts. + */ + interrupt(); + + /* + * Similar to window deletion - emulate "close" key press. + */ + close_event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS); + close_event->keyval = GDK_KEY_Close; + + g_async_queue_push(event_queue, close_event); + + return G_SOURCE_CONTINUE; +} + +} /* namespace SciTECO */ diff --git a/src/interface-gtk/interface-gtk.h b/src/interface-gtk/interface-gtk.h new file mode 100644 index 0000000..b5200ea --- /dev/null +++ b/src/interface-gtk/interface-gtk.h @@ -0,0 +1,171 @@ +/* + * 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 . + */ + +#ifndef __INTERFACE_GTK_H +#define __INTERFACE_GTK_H + +#include + +#include + +/* FIXME: see interface-gtk.cpp */ +#define GDK_DISABLE_DEPRECATION_WARNINGS +#include +#include + +#include +#include + +#include "interface.h" + +namespace SciTECO { + +typedef class ViewGtk : public View { + ScintillaObject *sci; + +public: + ViewGtk() : sci(NULL) {} + + /* implementation of View::initialize() */ + void initialize_impl(void); + + inline ~ViewGtk() + { + /* + * This does NOT destroy the Scintilla object + * and GTK widget, if it is the current view + * (and therefore added to the vbox). + */ + if (sci) + g_object_unref(G_OBJECT(sci)); + } + + inline GtkWidget * + get_widget(void) + { + return GTK_WIDGET(sci); + } + + /* implementation of View::ssm() */ + inline sptr_t + ssm_impl(unsigned int iMessage, uptr_t wParam = 0, sptr_t lParam = 0) + { + sptr_t ret; + + gdk_threads_enter(); + ret = scintilla_send_message(sci, iMessage, wParam, lParam); + gdk_threads_leave(); + + return ret; + } +} ViewCurrent; + +typedef class InterfaceGtk : public Interface { + GtkWidget *window; + GtkWidget *vbox; + + GtkWidget *event_box_widget; + + gchar *info_current; + GtkWidget *info_widget; + + GtkWidget *message_widget; + GtkWidget *cmdline_widget; + + GtkWidget *popup_widget; + + GtkWidget *current_view_widget; + + GAsyncQueue *event_queue; + +public: + InterfaceGtk() : Interface(), + window(NULL), + vbox(NULL), + event_box_widget(NULL), + info_current(NULL), info_widget(NULL), + message_widget(NULL), + cmdline_widget(NULL), + popup_widget(NULL), + current_view_widget(NULL), + event_queue(NULL) {} + ~InterfaceGtk(); + + /* overrides Interface::get_options() */ + inline GOptionGroup * + get_options(void) + { + return gtk_get_option_group(TRUE); + } + + /* implementation of Interface::main() */ + void main_impl(int &argc, char **&argv); + + /* implementation of Interface::vmsg() */ + void vmsg_impl(MessageType type, const gchar *fmt, va_list ap); + /* overrides Interface::msg_clear() */ + void msg_clear(void); + + /* implementation of Interface::show_view() */ + void show_view_impl(ViewGtk *view); + + /* implementation of Interface::info_update() */ + void info_update_impl(const QRegister *reg); + void info_update_impl(const Buffer *buffer); + + /* implementation of Interface::cmdline_update() */ + void cmdline_update_impl(const Cmdline *cmdline); + + /* implementation of Interface::popup_add() */ + void popup_add_impl(PopupEntryType type, + const gchar *name, bool highlight = false); + /* implementation of Interface::popup_show() */ + void popup_show_impl(void); + + /* implementation of Interface::popup_is_shown() */ + inline bool + popup_is_shown_impl(void) + { + bool ret; + + gdk_threads_enter(); + ret = gtk_widget_get_visible(popup_widget); + gdk_threads_leave(); + + return ret; + } + /* implementation of Interface::popup_clear() */ + void popup_clear_impl(void); + + /* main entry point (implementation) */ + void event_loop_impl(void); + + /* + * FIXME: This is for internal use only and could be + * hidden in a nested forward-declared friend struct. + */ + void handle_key_press(bool is_shift, bool is_ctl, guint keyval); + +private: + static void widget_set_font(GtkWidget *widget, const gchar *font_name); + + void cmdline_insert_chr(gint &pos, gchar chr); +} InterfaceCurrent; + +} /* namespace SciTECO */ + +#endif diff --git a/src/interface.h b/src/interface.h index 062c377..9552984 100644 --- a/src/interface.h +++ b/src/interface.h @@ -340,9 +340,9 @@ public: } /* namespace SciTECO */ #ifdef INTERFACE_GTK -#include "interface-gtk.h" +#include "interface-gtk/interface-gtk.h" #elif defined(INTERFACE_CURSES) -#include "interface-curses.h" +#include "interface-curses/interface-curses.h" #else #error No interface selected! #endif -- cgit v1.2.3