/* ide-grid.c * * Copyright 2017-2019 Christian Hergert * * 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-grid" #include "config.h" #include #include "ide-grid.h" #include "ide-gui-private.h" /** * SECTION:ide-grid * @title: IdeGrid * @short_description: A grid for #IdePage * * The #IdeGrid provides a grid of pages that the user may * manipulate. * * Internally, this is implemented with #IdeGrid at the top * containing one or more of #IdeGridColumn. Those columns * contain one or more #IdeFrame. The stack can contain many * #IdePage. * * #IdeGrid implements the #GListModel interface to simplify * the process of listing (with deduplication) the pages that are * contianed within the #IdeGrid. If you would instead like * to see all possible pages in the stack, use the * ide_grid_foreach_page() API. * * Since: 3.32 */ typedef struct { /* Owned references */ DzlSignalGroup *toplevel_signals; GQueue focus_column; GArray *stack_info; /* * This owned reference is our box highlight theatric that we * animate while doing a DnD drop interaction. */ DzlBoxTheatric *drag_theatric; DzlAnimation *drag_anim; /* * This unowned reference is simply used to compare to a new focus * page to see if we have changed our current page. It is not to * be used directly, only for pointer comparison. */ IdePage *_last_focused_page; /* * A GSource that is used to remove empty stacks that are unnecessary * (after a last stack item is removed). */ guint cull_source; } IdeGridPrivate; typedef struct { IdeGridColumn *column; IdeFrame *stack; GdkRectangle area; gint drop; gint x; gint y; } DropLocate; typedef struct { IdeFrame *stack; guint len; } StackInfo; enum { PROP_0, PROP_CURRENT_COLUMN, PROP_CURRENT_STACK, PROP_CURRENT_PAGE, N_PROPS }; enum { CREATE_FRAME, CREATE_VIEW, N_SIGNALS }; enum { DROP_ONTO, DROP_ABOVE, DROP_BELOW, DROP_LEFT_OF, DROP_RIGHT_OF, }; static void list_model_iface_init (GListModelInterface *iface); G_DEFINE_TYPE_WITH_CODE (IdeGrid, ide_grid, DZL_TYPE_MULTI_PANED, G_ADD_PRIVATE (IdeGrid) G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) static GParamSpec *properties [N_PROPS]; static guint signals [N_SIGNALS]; static void ide_grid_cull (IdeGrid *self) { guint n_columns; g_assert (IDE_IS_GRID (self)); n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)); for (guint i = n_columns; i > 0; i--) { IdeGridColumn *column; guint n_stacks; column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i - 1)); n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)); if (n_columns == 1 && n_stacks == 1) return; for (guint j = n_stacks; j > 0; j--) { IdeFrame *stack; guint n_items; stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j - 1)); n_items = g_list_model_get_n_items (G_LIST_MODEL (stack)); if (n_items == 0) gtk_widget_destroy (GTK_WIDGET (stack)); } if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)) == 0) gtk_widget_destroy (GTK_WIDGET (column)); } } static gboolean ide_grid_do_cull (gpointer data) { IdeGrid *self = data; IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); priv->cull_source = 0; ide_grid_cull (self); return G_SOURCE_REMOVE; } static void ide_grid_queue_cull (IdeGrid *self) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); if (priv->cull_source != 0) return; priv->cull_source = gdk_threads_add_idle_full (G_PRIORITY_HIGH, ide_grid_do_cull, g_object_ref (self), g_object_unref); } static void ide_grid_update_actions (IdeGrid *self) { guint n_children; g_assert (IDE_IS_GRID (self)); n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)); for (guint i = 0; i < n_children; i++) { GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i); g_assert (IDE_IS_GRID_COLUMN (column)); _ide_grid_column_update_actions (IDE_GRID_COLUMN (column)); } } static IdeFrame * ide_grid_real_create_frame (IdeGrid *self) { return g_object_new (IDE_TYPE_FRAME, "expand", TRUE, "visible", TRUE, NULL); } static GtkWidget * ide_grid_create_frame (IdeGrid *self) { IdeFrame *ret = NULL; g_assert (IDE_IS_GRID (self)); g_signal_emit (self, signals [CREATE_FRAME], 0, &ret); g_return_val_if_fail (IDE_IS_FRAME (ret), NULL); return GTK_WIDGET (ret); } static GtkWidget * ide_grid_create_column (IdeGrid *self) { GtkWidget *stack; g_assert (IDE_IS_GRID (self)); stack = ide_grid_create_frame (self); if (stack != NULL) { GtkWidget *column = g_object_new (IDE_TYPE_GRID_COLUMN, "visible", TRUE, NULL); gtk_container_add (GTK_CONTAINER (column), stack); return column; } return NULL; } static void ide_grid_after_set_focus (IdeGrid *self, GtkWidget *widget, GtkWidget *toplevel) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); g_assert (!widget || GTK_IS_WIDGET (widget)); g_assert (GTK_IS_WINDOW (toplevel)); if (widget != NULL) { GtkWidget *column = NULL; GtkWidget *page; if (gtk_widget_is_ancestor (widget, GTK_WIDGET (self))) { column = gtk_widget_get_ancestor (widget, IDE_TYPE_GRID_COLUMN); if (column != NULL) ide_grid_set_current_column (self, IDE_GRID_COLUMN (column)); } /* * self->_last_focused_page is an unowned reference, we only * use it for pointer comparison, nothing more. */ page = gtk_widget_get_ancestor (widget, IDE_TYPE_PAGE); if (page != (GtkWidget *)priv->_last_focused_page) { priv->_last_focused_page = (IdePage *)page; ide_object_notify_in_main (self, properties [PROP_CURRENT_PAGE]); if (page != NULL && column != NULL) { GtkWidget *stack; stack = gtk_widget_get_ancestor (GTK_WIDGET (page), IDE_TYPE_FRAME); if (stack != NULL) ide_grid_column_set_current_stack (IDE_GRID_COLUMN (column), IDE_FRAME (stack)); } } } } static void ide_grid_hierarchy_changed (GtkWidget *widget, GtkWidget *old_toplevel) { IdeGrid *self = (IdeGrid *)widget; IdeGridPrivate *priv = ide_grid_get_instance_private (self); GtkWidget *toplevel; g_assert (IDE_IS_GRID (self)); g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel)); /* * Setup focus tracking so that we can update our "current stack" when the * user selected focus changes. */ toplevel = gtk_widget_get_toplevel (widget); if (GTK_IS_WINDOW (toplevel)) dzl_signal_group_set_target (priv->toplevel_signals, toplevel); else dzl_signal_group_set_target (priv->toplevel_signals, NULL); /* * If we've been added to a widget and still do not have a stack added, then * we'll emit our ::create-frame signal to create that now. We do this here * to allow the consumer to connect to ::create-frame before adding the * widget to the hierarchy. */ if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (widget)) == 0) { GtkWidget *column = ide_grid_create_column (self); gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (column)); } } static void ide_grid_add (GtkContainer *container, GtkWidget *widget) { IdeGrid *self = (IdeGrid *)container; IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); g_assert (GTK_IS_WIDGET (widget)); if (IDE_IS_GRID_COLUMN (widget)) { GList *children; /* Add our column to the grid */ g_queue_push_head (&priv->focus_column, widget); GTK_CONTAINER_CLASS (ide_grid_parent_class)->add (container, widget); ide_grid_set_current_column (self, IDE_GRID_COLUMN (widget)); _ide_grid_column_update_actions (IDE_GRID_COLUMN (widget)); /* Start monitoring all the stacks in the grid for pages */ children = gtk_container_get_children (GTK_CONTAINER (widget)); for (const GList *iter = children; iter; iter = iter->next) if (IDE_IS_FRAME (iter->data)) _ide_grid_stack_added (self, iter->data); g_list_free (children); } else if (IDE_IS_FRAME (widget)) { IdeGridColumn *column; column = ide_grid_get_current_column (self); gtk_container_add (GTK_CONTAINER (column), widget); ide_grid_set_current_column (self, column); } else if (IDE_IS_PAGE (widget)) { IdeGridColumn *column = NULL; guint n_columns; /* If we have an empty layout stack, we'll prefer to add the * page to that. If we don't find an empty stack, we'll add * the page to the most recently focused stack. */ n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)); for (guint i = 0; i < n_columns; i++) { GtkWidget *ele = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i); g_assert (IDE_IS_GRID_COLUMN (ele)); if (_ide_grid_column_is_empty (IDE_GRID_COLUMN (ele))) { column = IDE_GRID_COLUMN (ele); break; } } if (column == NULL) column = ide_grid_get_current_column (self); g_assert (IDE_IS_GRID_COLUMN (column)); gtk_container_add (GTK_CONTAINER (column), widget); } else { g_warning ("%s must be one of IdeFrame, IdePage, or IdeGrid", G_OBJECT_TYPE_NAME (self)); return; } ide_grid_update_actions (self); } static void ide_grid_remove (GtkContainer *container, GtkWidget *widget) { IdeGrid *self = (IdeGrid *)container; IdeGridPrivate *priv = ide_grid_get_instance_private (self); gboolean notify = FALSE; g_assert (IDE_IS_GRID (self)); g_assert (IDE_IS_GRID_COLUMN (widget)); notify = g_queue_peek_head (&priv->focus_column) == (gpointer)widget; g_queue_remove (&priv->focus_column, widget); GTK_CONTAINER_CLASS (ide_grid_parent_class)->remove (container, widget); ide_grid_update_actions (self); if (notify) { GtkWidget *head = g_queue_peek_head (&priv->focus_column); if (head != NULL) gtk_widget_grab_focus (head); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_COLUMN]); } } static gboolean ide_grid_get_drop_area (IdeGrid *self, gint x, gint y, GdkRectangle *out_area, IdeGridColumn **out_column, IdeFrame **out_stack, gint *out_drop) { GtkAllocation alloc; GtkWidget *column; GtkWidget *stack = NULL; g_assert (IDE_IS_GRID (self)); g_assert (out_area != NULL); g_assert (out_column != NULL); g_assert (out_stack != NULL); g_assert (out_drop != NULL); gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); column = dzl_multi_paned_get_at_point (DZL_MULTI_PANED (self), x + alloc.x, 0); if (column != NULL) stack = dzl_multi_paned_get_at_point (DZL_MULTI_PANED (column), 0, y + alloc.y); if (column != NULL && stack != NULL) { GtkAllocation stack_alloc; gtk_widget_get_allocation (stack, &stack_alloc); gtk_widget_translate_coordinates (stack, GTK_WIDGET (self), 0, 0, &stack_alloc.x, &stack_alloc.y); *out_area = stack_alloc; *out_column = IDE_GRID_COLUMN (column); *out_stack = IDE_FRAME (stack); *out_drop = DROP_ONTO; gtk_widget_translate_coordinates (GTK_WIDGET (self), stack, x, y, &x, &y); if (FALSE) {} else if (x < (stack_alloc.width / 4)) { out_area->y = 0; out_area->height = alloc.height; out_area->width = stack_alloc.width / 4; *out_drop = DROP_LEFT_OF; } else if (x > (stack_alloc.width / 4 * 3)) { out_area->y = 0; out_area->height = alloc.height; out_area->x = dzl_cairo_rectangle_x2 (&stack_alloc) - (stack_alloc.width / 4); out_area->width = stack_alloc.width / 4; *out_drop = DROP_RIGHT_OF; } else if (y < (stack_alloc.height / 4)) { out_area->height = stack_alloc.height / 4; *out_drop = DROP_ABOVE; } else if (y > (stack_alloc.height / 4 * 3)) { out_area->y = dzl_cairo_rectangle_y2 (&stack_alloc) - (stack_alloc.height / 4); out_area->height = stack_alloc.height / 4; *out_drop = DROP_BELOW; } return TRUE; } return FALSE; } static gboolean ide_grid_drag_motion (GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time_) { IdeGrid *self = (IdeGrid *)widget; IdeGridPrivate *priv = ide_grid_get_instance_private (self); IdeGridColumn *column = NULL; IdeFrame *stack = NULL; DzlAnimation *drag_anim; GdkRectangle area = {0}; GtkAllocation alloc; gint drop = DROP_ONTO; g_assert (IDE_IS_GRID (self)); g_assert (GDK_IS_DRAG_CONTEXT (context)); if (priv->drag_anim != NULL) { dzl_animation_stop (priv->drag_anim); g_clear_weak_pointer (&priv->drag_anim); } gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop)) return GDK_EVENT_PROPAGATE; if (priv->drag_theatric == NULL) { priv->drag_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC, "x", area.x, "y", area.y, "width", area.width, "height", area.height, "alpha", 0.3, "background", "#729fcf", "target", self, NULL); return GDK_EVENT_STOP; } drag_anim = dzl_object_animate (priv->drag_theatric, DZL_ANIMATION_EASE_OUT_CUBIC, 100, gtk_widget_get_frame_clock (GTK_WIDGET (self)), "x", area.x, "width", area.width, "y", area.y, "height", area.height, NULL); g_set_weak_pointer (&priv->drag_anim, drag_anim); gtk_widget_queue_draw (GTK_WIDGET (self)); return GDK_EVENT_STOP; } static void ide_grid_drag_data_received (GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *data, guint info, guint time_) { IdeGrid *self = (IdeGrid *)widget; IdeGridColumn *column = NULL; IdeFrame *stack = NULL; g_auto(GStrv) uris = NULL; GdkRectangle area = {0}; gint drop = DROP_ONTO; g_assert (IDE_IS_GRID (self)); g_assert (GDK_IS_DRAG_CONTEXT (context)); if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop)) return; g_assert (IDE_IS_GRID_COLUMN (column)); g_assert (IDE_IS_FRAME (stack)); if (!(uris = gtk_selection_data_get_uris (data))) return; for (guint i = 0; uris[i] != NULL; i++) { const gchar *uri = uris[i]; IdePage *page = NULL; gint column_index = 0; gint stack_index = 0; g_signal_emit (self, signals [CREATE_VIEW], 0, uri, &page); if (page == NULL) { g_debug ("Failed to load IdePage for \"%s\"", uri); continue; } gtk_container_child_get (GTK_CONTAINER (self), GTK_WIDGET (column), "index", &column_index, NULL); gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (stack), "index", &stack_index, NULL); switch (drop) { case DROP_ONTO: gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page)); break; case DROP_ABOVE: stack = IDE_FRAME (ide_grid_create_frame (self)); gtk_container_add_with_properties (GTK_CONTAINER (column), GTK_WIDGET (stack), "index", stack_index, NULL); gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page)); break; case DROP_BELOW: stack = IDE_FRAME (ide_grid_create_frame (self)); gtk_container_add_with_properties (GTK_CONTAINER (column), GTK_WIDGET (stack), "index", stack_index + 1, NULL); gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page)); break; case DROP_LEFT_OF: column = IDE_GRID_COLUMN (ide_grid_create_column (self)); gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (column), "index", column_index, NULL); gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (page)); break; case DROP_RIGHT_OF: column = IDE_GRID_COLUMN (ide_grid_create_column (self)); gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (column), "index", column_index + 1, NULL); gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (page)); break; default: g_assert_not_reached (); } } } static void ide_grid_drag_leave (GtkWidget *widget, GdkDragContext *context, guint time_) { IdeGrid *self = (IdeGrid *)widget; IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); g_assert (GDK_IS_DRAG_CONTEXT (context)); if (priv->drag_anim != NULL) { dzl_animation_stop (priv->drag_anim); g_clear_weak_pointer (&priv->drag_anim); } g_clear_object (&priv->drag_theatric); gtk_widget_queue_draw (GTK_WIDGET (self)); } static gboolean ide_grid_drag_failed (GtkWidget *widget, GdkDragContext *context, GtkDragResult result) { IdeGrid *self = (IdeGrid *)widget; IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); g_assert (GDK_IS_DRAG_CONTEXT (context)); if (priv->drag_anim != NULL) { dzl_animation_stop (priv->drag_anim); g_clear_weak_pointer (&priv->drag_anim); } g_clear_object (&priv->drag_theatric); gtk_widget_queue_draw (GTK_WIDGET (self)); return GDK_EVENT_PROPAGATE; } static void ide_grid_grab_focus (GtkWidget *widget) { IdeGrid *self = (IdeGrid *)widget; IdeFrame *stack; g_assert (IDE_IS_GRID (self)); stack = ide_grid_get_current_stack (self); if (stack != NULL) gtk_widget_grab_focus (GTK_WIDGET (stack)); else GTK_WIDGET_CLASS (ide_grid_parent_class)->grab_focus (widget); } static void ide_grid_destroy (GtkWidget *widget) { IdeGrid *self = (IdeGrid *)widget; IdeGridPrivate *priv = ide_grid_get_instance_private (self); dzl_clear_source (&priv->cull_source); GTK_WIDGET_CLASS (ide_grid_parent_class)->destroy (widget); } static void ide_grid_finalize (GObject *object) { IdeGrid *self = (IdeGrid *)object; IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); g_assert (priv->focus_column.head == NULL); g_assert (priv->focus_column.tail == NULL); g_assert (priv->focus_column.length == 0); g_clear_pointer (&priv->stack_info, g_array_unref); g_clear_object (&priv->toplevel_signals); G_OBJECT_CLASS (ide_grid_parent_class)->finalize (object); } static void ide_grid_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { IdeGrid *self = IDE_GRID (object); switch (prop_id) { case PROP_CURRENT_COLUMN: g_value_set_object (value, ide_grid_get_current_column (self)); break; case PROP_CURRENT_STACK: g_value_set_object (value, ide_grid_get_current_stack (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_grid_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { IdeGrid *self = IDE_GRID (object); switch (prop_id) { case PROP_CURRENT_COLUMN: ide_grid_set_current_column (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_grid_class_init (IdeGridClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); object_class->finalize = ide_grid_finalize; object_class->get_property = ide_grid_get_property; object_class->set_property = ide_grid_set_property; widget_class->destroy = ide_grid_destroy; widget_class->drag_data_received = ide_grid_drag_data_received; widget_class->drag_motion = ide_grid_drag_motion; widget_class->drag_leave = ide_grid_drag_leave; widget_class->drag_failed = ide_grid_drag_failed; widget_class->grab_focus = ide_grid_grab_focus; widget_class->hierarchy_changed = ide_grid_hierarchy_changed; container_class->add = ide_grid_add; container_class->remove = ide_grid_remove; klass->create_frame = ide_grid_real_create_frame; properties [PROP_CURRENT_COLUMN] = g_param_spec_object ("current-column", "Current Column", "The most recently focused grid column", IDE_TYPE_GRID_COLUMN, (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); properties [PROP_CURRENT_STACK] = g_param_spec_object ("current-stack", "Current Stack", "The most recently focused IdeFrame", IDE_TYPE_FRAME, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_CURRENT_PAGE] = g_param_spec_object ("current-page", "Current View", "The most recently focused IdePage", IDE_TYPE_PAGE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); gtk_widget_class_set_css_name (widget_class, "idegrid"); /** * IdeGrid::create-frame: * @self: an #IdeGrid * * Creates a new stack to be added to the grid. * * Returns: (transfer full): A newly created #IdeFrame * * Since: 3.34 */ signals [CREATE_FRAME] = g_signal_new (g_intern_static_string ("create-frame"), G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (IdeGridClass, create_frame), g_signal_accumulator_first_wins, NULL, NULL, IDE_TYPE_FRAME, 0); /** * IdeGrid::create-page: * @self: an #IdeGrid * @uri: the URI to open * * Creates a new page for @uri to be added to the grid. * * Returns: (transfer full): A newly created #IdePage * * Since: 3.32 */ signals [CREATE_VIEW] = g_signal_new (g_intern_static_string ("create-page"), G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (IdeGridClass, create_page), g_signal_accumulator_first_wins, NULL, NULL, IDE_TYPE_PAGE, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE); } static void ide_grid_init (IdeGrid *self) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); static const GtkTargetEntry target_entries[] = { { (gchar *)"text/uri-list", 0, 0 }, }; gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL); gtk_drag_dest_set (GTK_WIDGET (self), GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP, target_entries, G_N_ELEMENTS (target_entries), GDK_ACTION_COPY); priv->stack_info = g_array_new (FALSE, FALSE, sizeof (StackInfo)); priv->toplevel_signals = dzl_signal_group_new (GTK_TYPE_WINDOW); dzl_signal_group_connect_object (priv->toplevel_signals, "set-focus", G_CALLBACK (ide_grid_after_set_focus), self, G_CONNECT_SWAPPED | G_CONNECT_AFTER); _ide_grid_init_actions (self); } /** * ide_grid_new: * * Creates a new #IdeGrid. * * Returns: (transfer full): A newly created #IdeGrid * * Since: 3.32 */ GtkWidget * ide_grid_new (void) { return g_object_new (IDE_TYPE_GRID, NULL); } /** * ide_grid_get_current_stack: * @self: a #IdeGrid * * Gets the most recently focused stack. This is useful when you want to open * a document on the stack the user last focused. * * Returns: (transfer none) (nullable): an #IdeFrame or %NULL. * * Since: 3.32 */ IdeFrame * ide_grid_get_current_stack (IdeGrid *self) { IdeGridColumn *column; g_return_val_if_fail (IDE_IS_GRID (self), NULL); column = ide_grid_get_current_column (self); if (column != NULL) return ide_grid_column_get_current_stack (column); return NULL; } /** * ide_grid_get_nth_column: * @self: a #IdeGrid * @nth: the index of the column, or -1 * * Gets the @nth column from the grid. * * If @nth is -1, then a new column at the beginning of the * grid is created. * * If @nth is >= the number of columns in the grid, then a new * column at the end of the grid is created. * * Returns: (transfer none): An #IdeGridColumn. * * Since: 3.32 */ IdeGridColumn * ide_grid_get_nth_column (IdeGrid *self, gint nth) { GtkWidget *column; g_return_val_if_fail (IDE_IS_GRID (self), NULL); if (nth < 0) { column = ide_grid_create_column (self); gtk_container_add_with_properties (GTK_CONTAINER (self), column, "index", 0, NULL); } else if (nth >= dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self))) { column = ide_grid_create_column (self); gtk_container_add (GTK_CONTAINER (self), column); } else { column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), nth); } g_return_val_if_fail (IDE_IS_GRID_COLUMN (column), NULL); return IDE_GRID_COLUMN (column); } /* * _ide_grid_get_nth_stack: * * This will get the @nth stack. If it does not yet exist, * it will be created. * * If nth == -1, a new stack will be created at index 0. * * If nth >= the number of stacks, a new stack will be created * at the end of the grid. * * Returns: (not nullable) (transfer none): An #IdeFrame. */ IdeFrame * _ide_grid_get_nth_stack (IdeGrid *self, gint nth) { IdeGridColumn *column; IdeFrame *stack; g_return_val_if_fail (IDE_IS_GRID (self), NULL); column = ide_grid_get_nth_column (self, nth); stack = ide_grid_column_get_current_stack (IDE_GRID_COLUMN (column)); g_return_val_if_fail (IDE_IS_FRAME (stack), NULL); return stack; } /** * _ide_grid_get_nth_stack_for_column: * @self: an #IdeGrid * @column: an #IdeGridColumn * @nth: the index of the column, between -1 and G_MAXINT * * This will get the @nth stack within @column. If a matching stack * cannot be found, it will be created. * * If @nth is less-than 0, a new column will be inserted at the top. * * If @nth is greater-than the number of stacks, then a new stack * will be created at the bottom. * * Returns: (not nullable) (transfer none): An #IdeFrame. * * Since: 3.32 */ IdeFrame * _ide_grid_get_nth_stack_for_column (IdeGrid *self, IdeGridColumn *column, gint nth) { GtkWidget *stack; g_return_val_if_fail (IDE_IS_GRID (self), NULL); g_return_val_if_fail (IDE_IS_GRID_COLUMN (column), NULL); g_return_val_if_fail (gtk_widget_get_parent (GTK_WIDGET (column)) == GTK_WIDGET (self), NULL); if (nth < 0) { stack = ide_grid_create_frame (self); gtk_container_add_with_properties (GTK_CONTAINER (column), stack, "index", 0, NULL); } else if (nth >= dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column))) { stack = ide_grid_create_frame (self); gtk_container_add (GTK_CONTAINER (self), stack); } else { stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), nth); } g_assert (IDE_IS_FRAME (stack)); return IDE_FRAME (stack); } /** * ide_grid_get_current_column: * @self: a #IdeGrid * * Gets the most recently focused column of the grid. * * Returns: (transfer none) (not nullable): An #IdeGridColumn * * Since: 3.32 */ IdeGridColumn * ide_grid_get_current_column (IdeGrid *self) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); GtkWidget *ret = NULL; g_return_val_if_fail (IDE_IS_GRID (self), NULL); if (priv->focus_column.head != NULL) ret = priv->focus_column.head->data; else if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)) > 0) ret = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0); if (ret == NULL) { ret = ide_grid_create_column (self); gtk_container_add (GTK_CONTAINER (self), ret); } g_return_val_if_fail (IDE_IS_GRID_COLUMN (ret), NULL); return IDE_GRID_COLUMN (ret); } /** * ide_grid_set_current_column: * @self: an #IdeGrid * @column: (nullable): an #IdeGridColumn or %NULL * * Sets the current column for the grid. Generally this is automatically * updated for you when the focus changes within the workbench. * * @column can be %NULL out of convenience. * * Since: 3.32 */ void ide_grid_set_current_column (IdeGrid *self, IdeGridColumn *column) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); GList *iter; g_return_if_fail (IDE_IS_GRID (self)); g_return_if_fail (!column || IDE_IS_GRID_COLUMN (column)); if (column == NULL) return; if (gtk_widget_get_parent (GTK_WIDGET (column)) != GTK_WIDGET (self)) { g_warning ("Attempt to set current column with non-descendant"); return; } if (NULL != (iter = g_queue_find (&priv->focus_column, column))) { g_queue_unlink (&priv->focus_column, iter); g_queue_push_head_link (&priv->focus_column, iter); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_COLUMN]); ide_grid_update_actions (self); return; } g_warning ("%s does not contain %s", G_OBJECT_TYPE_NAME (self), G_OBJECT_TYPE_NAME (column)); } /** * ide_grid_get_current_page: * @self: a #IdeGrid * * Gets the most recent page used by the user as determined by tracking * the window focus. * * Returns: (transfer none): An #IdePage or %NULL * * Since: 3.32 */ IdePage * ide_grid_get_current_page (IdeGrid *self) { IdeFrame *stack; g_return_val_if_fail (IDE_IS_GRID (self), NULL); stack = ide_grid_get_current_stack (self); if (stack != NULL) return ide_frame_get_visible_child (stack); return NULL; } static void collect_pages (GtkWidget *widget, GPtrArray *ar) { if (IDE_IS_PAGE (widget)) g_ptr_array_add (ar, widget); } /** * ide_grid_foreach_page: * @self: a #IdeGrid * @callback: (scope call) (closure user_data): A callback for each page * @user_data: user data for @callback * * This function will call @callback for every page found in @self. * * Since: 3.32 */ void ide_grid_foreach_page (IdeGrid *self, GtkCallback callback, gpointer user_data) { g_autoptr(GPtrArray) pages = NULL; guint n_columns; g_return_if_fail (IDE_IS_GRID (self)); g_return_if_fail (callback != NULL); pages = g_ptr_array_new (); n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)); for (guint i = 0; i < n_columns; i++) { GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i); guint n_stacks; g_assert (IDE_IS_GRID_COLUMN (column)); n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)); for (guint j = 0; j < n_stacks; j++) { GtkWidget *stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j); g_assert (IDE_IS_FRAME (stack)); ide_frame_foreach_page (IDE_FRAME (stack), (GtkCallback) collect_pages, pages); } } for (guint i = 0; i < pages->len; i++) callback (g_ptr_array_index (pages, i), user_data); } static GType ide_grid_get_item_type (GListModel *model) { return IDE_TYPE_PAGE; } static guint ide_grid_get_n_items (GListModel *model) { IdeGrid *self = (IdeGrid *)model; IdeGridPrivate *priv = ide_grid_get_instance_private (self); guint n_items = 0; g_assert (IDE_IS_GRID (self)); for (guint i = 0; i < priv->stack_info->len; i++) n_items += g_array_index (priv->stack_info, StackInfo, i).len; return n_items; } static gpointer ide_grid_get_item (GListModel *model, guint position) { IdeGrid *self = (IdeGrid *)model; IdeGridPrivate *priv = ide_grid_get_instance_private (self); g_assert (IDE_IS_GRID (self)); g_assert (position < ide_grid_get_n_items (model)); for (guint i = 0; i < priv->stack_info->len; i++) { const StackInfo *info = &g_array_index (priv->stack_info, StackInfo, i); if (position >= info->len) { position -= info->len; continue; } return g_list_model_get_item (G_LIST_MODEL (info->stack), position); } g_warning ("Failed to locate position %u within %s", position, G_OBJECT_TYPE_NAME (self)); return NULL; } static void list_model_iface_init (GListModelInterface *iface) { iface->get_item_type = ide_grid_get_item_type; iface->get_n_items = ide_grid_get_n_items; iface->get_item = ide_grid_get_item; } static void ide_grid_stack_items_changed (IdeGrid *self, guint position, guint removed, guint added, IdeFrame *stack) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); guint real_position = 0; g_assert (IDE_IS_GRID (self)); g_assert (IDE_IS_FRAME (stack)); for (guint i = 0; i < priv->stack_info->len; i++) { StackInfo *info = &g_array_index (priv->stack_info, StackInfo, i); if (info->stack == stack) { info->len -= removed; info->len += added; g_list_model_items_changed (G_LIST_MODEL (self), real_position + position, removed, added); ide_object_notify_in_main (G_OBJECT (self), properties [PROP_CURRENT_PAGE]); ide_grid_queue_cull (self); return; } real_position += info->len; } g_warning ("Failed to locate %s within %s", G_OBJECT_TYPE_NAME (stack), G_OBJECT_TYPE_NAME (self)); } void _ide_grid_stack_added (IdeGrid *self, IdeFrame *stack) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); StackInfo info = { 0 }; guint n_items; g_return_if_fail (IDE_IS_GRID (self)); g_return_if_fail (IDE_IS_FRAME (stack)); g_return_if_fail (G_IS_LIST_MODEL (stack)); info.stack = stack; info.len = 0; g_array_append_val (priv->stack_info, info); g_signal_connect_object (stack, "items-changed", G_CALLBACK (ide_grid_stack_items_changed), self, G_CONNECT_SWAPPED); n_items = g_list_model_get_n_items (G_LIST_MODEL (stack)); ide_grid_stack_items_changed (self, 0, 0, n_items, stack); } void _ide_grid_stack_removed (IdeGrid *self, IdeFrame *stack) { IdeGridPrivate *priv = ide_grid_get_instance_private (self); guint position = 0; g_return_if_fail (IDE_IS_GRID (self)); g_return_if_fail (IDE_IS_FRAME (stack)); g_signal_handlers_disconnect_by_func (stack, G_CALLBACK (ide_grid_stack_items_changed), self); for (guint i = 0; i < priv->stack_info->len; i++) { const StackInfo info = g_array_index (priv->stack_info, StackInfo, i); if (info.stack == stack) { g_array_remove_index (priv->stack_info, i); g_list_model_items_changed (G_LIST_MODEL (self), position, info.len, 0); break; } } } static void count_pages_cb (GtkWidget *widget, gpointer data) { (*(guint *)data)++; } guint ide_grid_count_pages (IdeGrid *self) { guint count = 0; g_return_val_if_fail (IDE_IS_GRID (self), 0); ide_grid_foreach_page (self, count_pages_cb, &count); return count; } /** * ide_grid_focus_neighbor: * @self: An #IdeGrid * @dir: the direction for the focus change * * Attempts to focus a neighbor #IdePage in the grid based on * the direction requested. * * If an #IdePage was focused, it will be returned to the caller. * * Returns: (transfer none) (nullable): An #IdePage or %NULL * * Since: 3.32 */ IdePage * ide_grid_focus_neighbor (IdeGrid *self, GtkDirectionType dir) { IdeGridColumn *column; IdeFrame *stack; IdePage *page = NULL; guint stack_pos = 0; guint column_pos = 0; guint n_children; g_return_val_if_fail (IDE_IS_GRID (self), NULL); g_return_val_if_fail (dir <= GTK_DIR_RIGHT, NULL); /* Make sure we have a current page and stack */ if (NULL == (stack = ide_grid_get_current_stack (self)) || NULL == (column = ide_grid_get_current_column (self))) return NULL; gtk_container_child_get (GTK_CONTAINER (self), GTK_WIDGET (column), "index", &column_pos, NULL); gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (stack), "index", &stack_pos, NULL); switch (dir) { case GTK_DIR_DOWN: n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)); if (n_children - stack_pos == 1) return NULL; stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos + 1)); page = ide_frame_get_visible_child (stack); break; case GTK_DIR_RIGHT: n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)); if (n_children - column_pos == 1) return NULL; column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos + 1)); stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0)); page = ide_frame_get_visible_child (stack); break; case GTK_DIR_UP: if (stack_pos == 0) return NULL; stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos - 1)); page = ide_frame_get_visible_child (stack); break; case GTK_DIR_LEFT: if (column_pos == 0) return NULL; column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos - 1)); stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0)); page = ide_frame_get_visible_child (stack); break; case GTK_DIR_TAB_FORWARD: if (!ide_grid_focus_neighbor (self, GTK_DIR_DOWN) && !ide_grid_focus_neighbor (self, GTK_DIR_RIGHT)) { column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0)); stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0)); page = ide_frame_get_visible_child (stack); } break; case GTK_DIR_TAB_BACKWARD: if (!ide_grid_focus_neighbor (self, GTK_DIR_UP) && !ide_grid_focus_neighbor (self, GTK_DIR_LEFT)) { n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)); column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), n_children - 1)); stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0)); page = ide_frame_get_visible_child (stack); } break; default: g_assert_not_reached (); } if (page != NULL) gtk_widget_child_focus (GTK_WIDGET (page), GTK_DIR_TAB_FORWARD); return page; }