gem-graph-client/libide/gui/ide-grid.c

1534 lines
43 KiB
C
Raw Permalink Normal View History

/* ide-grid.c
*
* Copyright 2017-2019 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "ide-grid"
#include "config.h"
#include <string.h>
#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;
}