1468 lines
45 KiB
C
1468 lines
45 KiB
C
/* ide-frame.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-frame"
|
|
|
|
#include "config.h"
|
|
|
|
#include <dazzle.h>
|
|
#include <glib/gi18n.h>
|
|
#include <libide-core.h>
|
|
#include <libide-threading.h>
|
|
#include <libpeas/peas.h>
|
|
|
|
#include "ide-frame.h"
|
|
#include "ide-frame-addin.h"
|
|
#include "ide-frame-header.h"
|
|
#include "ide-frame-wrapper.h"
|
|
#include "ide-gui-private.h"
|
|
|
|
#define TRANSITION_DURATION 300
|
|
#define DISTANCE_THRESHOLD(alloc) (MIN(250, (gint)((alloc)->width * .333)))
|
|
|
|
/**
|
|
* SECTION:ide-frame
|
|
* @title: IdeFrame
|
|
* @short_description: A stack of #IdePage
|
|
*
|
|
* This widget is used to represent a stack of #IdePage widgets. it
|
|
* includes an #IdeFrameHeader at the top, and then a stack of pages
|
|
* below.
|
|
*
|
|
* If there are no #IdePage visibile, then an empty state widget is
|
|
* displayed with some common information for the user.
|
|
*
|
|
* To simplify integration with other systems, #IdeFrame implements
|
|
* the #GListModel interface for each of the #IdePage.
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
|
|
typedef struct
|
|
{
|
|
DzlBindingGroup *bindings;
|
|
DzlSignalGroup *signals;
|
|
GPtrArray *pages;
|
|
GPtrArray *in_transition;
|
|
PeasExtensionSet *addins;
|
|
|
|
/*
|
|
* Our gestures are used to do interactive moves when the user
|
|
* does a three finger swipe. We create the dummy gesture to
|
|
* ensure things work, because it for some reason does not without
|
|
* the dummy gesture set.
|
|
*
|
|
* https://bugzilla.gnome.org/show_bug.cgi?id=788914
|
|
*/
|
|
GtkGesture *dummy;
|
|
GtkGesture *pan;
|
|
DzlBoxTheatric *pan_theatric;
|
|
IdePage *pan_page;
|
|
|
|
/* Template references */
|
|
GtkBox *empty_placeholder;
|
|
DzlEmptyState *failed_state;
|
|
IdeFrameHeader *header;
|
|
GtkStack *stack;
|
|
GtkStack *top_stack;
|
|
GtkEventBox *event_box;
|
|
} IdeFramePrivate;
|
|
|
|
typedef struct
|
|
{
|
|
IdeFrame *source;
|
|
IdeFrame *dest;
|
|
IdePage *page;
|
|
DzlBoxTheatric *theatric;
|
|
} AnimationState;
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_HAS_VIEW,
|
|
PROP_VISIBLE_CHILD,
|
|
N_PROPS
|
|
};
|
|
|
|
enum {
|
|
CHANGE_CURRENT_PAGE,
|
|
N_SIGNALS
|
|
};
|
|
|
|
static void buildable_iface_init (GtkBuildableIface *iface);
|
|
static void list_model_iface_init (GListModelInterface *iface);
|
|
static void animation_state_complete (gpointer data);
|
|
|
|
G_DEFINE_TYPE_WITH_CODE (IdeFrame, ide_frame, GTK_TYPE_BOX,
|
|
G_ADD_PRIVATE (IdeFrame)
|
|
G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)
|
|
G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
|
|
|
|
static GParamSpec *properties [N_PROPS];
|
|
static guint signals [N_SIGNALS];
|
|
|
|
static inline gboolean
|
|
is_uninitialized (GtkAllocation *alloc)
|
|
{
|
|
return (alloc->x == -1 && alloc->y == -1 &&
|
|
alloc->width == 1 && alloc->height == 1);
|
|
}
|
|
|
|
static void
|
|
ide_frame_set_cursor (IdeFrame *self,
|
|
const gchar *name)
|
|
{
|
|
GdkWindow *window;
|
|
GdkDisplay *display;
|
|
GdkCursor *cursor;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (name != NULL);
|
|
|
|
window = gtk_widget_get_window (GTK_WIDGET (self));
|
|
display = gtk_widget_get_display (GTK_WIDGET (self));
|
|
cursor = gdk_cursor_new_from_name (display, name);
|
|
|
|
gdk_window_set_cursor (window, cursor);
|
|
|
|
g_clear_object (&cursor);
|
|
}
|
|
|
|
static void
|
|
ide_frame_page_failed (IdeFrame *self,
|
|
GParamSpec *pspec,
|
|
IdePage *page)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (IDE_IS_PAGE (page));
|
|
|
|
if (ide_page_get_failed (page))
|
|
gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
|
|
else
|
|
gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
|
|
}
|
|
|
|
static void
|
|
ide_frame_bindings_notify_source (IdeFrame *self,
|
|
GParamSpec *pspec,
|
|
DzlBindingGroup *bindings)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
GObject *source;
|
|
|
|
g_assert (DZL_IS_BINDING_GROUP (bindings));
|
|
g_assert (pspec != NULL);
|
|
g_assert (IDE_IS_FRAME (self));
|
|
|
|
source = dzl_binding_group_get_source (bindings);
|
|
|
|
if (source == NULL)
|
|
{
|
|
_ide_frame_header_set_title (priv->header, _("No Open Pages"));
|
|
_ide_frame_header_set_modified (priv->header, FALSE);
|
|
_ide_frame_header_set_background_rgba (priv->header, NULL);
|
|
_ide_frame_header_set_foreground_rgba (priv->header, NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ide_frame_notify_addin_of_page (PeasExtensionSet *set,
|
|
PeasPluginInfo *plugin_info,
|
|
PeasExtension *exten,
|
|
gpointer user_data)
|
|
{
|
|
IdeFrameAddin *addin = (IdeFrameAddin *)exten;
|
|
IdePage *page = user_data;
|
|
|
|
g_assert (PEAS_IS_EXTENSION_SET (set));
|
|
g_assert (plugin_info != NULL);
|
|
g_assert (IDE_IS_FRAME_ADDIN (addin));
|
|
g_assert (!page || IDE_IS_PAGE (page));
|
|
|
|
ide_frame_addin_set_page (addin, page);
|
|
}
|
|
|
|
static void
|
|
ide_frame_notify_visible_child (IdeFrame *self,
|
|
GParamSpec *pspec,
|
|
GtkStack *stack)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
GtkWidget *visible_child;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (GTK_IS_STACK (stack));
|
|
|
|
if (gtk_widget_in_destruction (GTK_WIDGET (self)))
|
|
return;
|
|
|
|
if ((visible_child = gtk_stack_get_visible_child (priv->stack)))
|
|
{
|
|
if (gtk_widget_in_destruction (visible_child))
|
|
visible_child = NULL;
|
|
}
|
|
|
|
/*
|
|
* Mux/Proxy actions to our level so that they also be activated
|
|
* from the header bar without any weirdness by the View.
|
|
*/
|
|
dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self), visible_child,
|
|
"IDE_FRAME_MUXED_ACTION");
|
|
|
|
/* Update our bindings targets */
|
|
dzl_binding_group_set_source (priv->bindings, visible_child);
|
|
dzl_signal_group_set_target (priv->signals, visible_child);
|
|
|
|
/* Show either the empty state, failed state, or actual page */
|
|
if (visible_child != NULL &&
|
|
ide_page_get_failed (IDE_PAGE (visible_child)))
|
|
gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
|
|
else if (visible_child != NULL)
|
|
gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
|
|
else
|
|
gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->empty_placeholder));
|
|
|
|
/* Allow the header to update settings */
|
|
_ide_frame_header_update (priv->header, IDE_PAGE (visible_child));
|
|
|
|
/* Ensure action state is up to date */
|
|
_ide_frame_update_actions (self);
|
|
|
|
if (priv->addins != NULL)
|
|
peas_extension_set_foreach (priv->addins,
|
|
ide_frame_notify_addin_of_page,
|
|
visible_child);
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_CHILD]);
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_VIEW]);
|
|
}
|
|
|
|
static void
|
|
collect_widgets (GtkWidget *widget,
|
|
gpointer user_data)
|
|
{
|
|
g_ptr_array_add (user_data, widget);
|
|
}
|
|
|
|
static void
|
|
ide_frame_change_current_page (IdeFrame *self,
|
|
gint direction)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
g_autoptr(GPtrArray) ar = NULL;
|
|
GtkWidget *visible_child;
|
|
gint position = 0;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
|
|
if (direction < -1)
|
|
direction = -1;
|
|
else if (direction > 1)
|
|
direction = 1;
|
|
|
|
visible_child = gtk_stack_get_visible_child (priv->stack);
|
|
|
|
if (visible_child == NULL)
|
|
return;
|
|
|
|
gtk_container_child_get (GTK_CONTAINER (priv->stack), visible_child,
|
|
"position", &position,
|
|
NULL);
|
|
|
|
ar = g_ptr_array_new ();
|
|
gtk_container_foreach (GTK_CONTAINER (priv->stack), collect_widgets, ar);
|
|
if (ar->len == 0)
|
|
g_return_if_reached ();
|
|
|
|
position = (position + (int)ar->len - direction) % (int)ar->len;
|
|
|
|
visible_child = g_ptr_array_index (ar, position);
|
|
gtk_stack_set_visible_child (priv->stack, visible_child);
|
|
}
|
|
|
|
static void
|
|
ide_frame_add (GtkContainer *container,
|
|
GtkWidget *widget)
|
|
{
|
|
IdeFrame *self = (IdeFrame *)container;
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (GTK_IS_WIDGET (widget));
|
|
|
|
if (IDE_IS_PAGE (widget))
|
|
gtk_container_add (GTK_CONTAINER (priv->stack), widget);
|
|
else
|
|
GTK_CONTAINER_CLASS (ide_frame_parent_class)->add (container, widget);
|
|
|
|
gtk_widget_queue_resize (GTK_WIDGET (self));
|
|
}
|
|
|
|
static void
|
|
ide_frame_page_added (IdeFrame *self,
|
|
IdePage *page)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
guint position;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (IDE_IS_PAGE (page));
|
|
|
|
/*
|
|
* Make sure that the header has dismissed all of the popovers immediately.
|
|
* We don't want them lingering while we do other UI work which might want to
|
|
* grab focus, etc.
|
|
*/
|
|
_ide_frame_header_popdown (priv->header);
|
|
|
|
/* Notify GListModel consumers of the new page and it's position within
|
|
* our stack of page widgets.
|
|
*/
|
|
position = priv->pages->len;
|
|
g_ptr_array_add (priv->pages, page);
|
|
g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
|
|
|
|
/*
|
|
* Now ensure that the page is displayed and focus the widget so the
|
|
* user can immediately start typing.
|
|
*/
|
|
ide_frame_set_visible_child (self, page);
|
|
gtk_widget_grab_focus (GTK_WIDGET (page));
|
|
}
|
|
|
|
static void
|
|
ide_frame_page_removed (IdeFrame *self,
|
|
IdePage *page)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (IDE_IS_PAGE (page));
|
|
|
|
if (priv->pages != NULL)
|
|
{
|
|
guint position = 0;
|
|
|
|
/* If this is the last page, hide the popdown now. We use our hide
|
|
* variant instead of popdown so that we don't have jittery animations.
|
|
*/
|
|
if (priv->pages->len == 1)
|
|
_ide_frame_header_hide (priv->header);
|
|
|
|
/*
|
|
* Only remove the page if it is not in transition. We hold onto the
|
|
* page during the transition so that we keep the list stable.
|
|
*/
|
|
if (!g_ptr_array_find_with_equal_func (priv->in_transition, page, NULL, &position))
|
|
{
|
|
for (guint i = 0; i < priv->pages->len; i++)
|
|
{
|
|
if ((gpointer)page == g_ptr_array_index (priv->pages, i))
|
|
{
|
|
g_ptr_array_remove_index (priv->pages, i);
|
|
g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
ide_frame_real_agree_to_close_async (IdeFrame *self,
|
|
GCancellable *cancellable,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
g_autoptr(IdeTask) task = NULL;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
|
|
|
|
task = ide_task_new (self, cancellable, callback, user_data);
|
|
ide_task_set_source_tag (task, ide_frame_real_agree_to_close_async);
|
|
ide_task_set_priority (task, G_PRIORITY_LOW);
|
|
ide_task_return_boolean (task, TRUE);
|
|
}
|
|
|
|
static gboolean
|
|
ide_frame_real_agree_to_close_finish (IdeFrame *self,
|
|
GAsyncResult *result,
|
|
GError **error)
|
|
{
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (IDE_IS_TASK (result));
|
|
|
|
return ide_task_propagate_boolean (IDE_TASK (result), error);
|
|
}
|
|
|
|
static void
|
|
ide_frame_addin_added (PeasExtensionSet *set,
|
|
PeasPluginInfo *plugin_info,
|
|
PeasExtension *exten,
|
|
gpointer user_data)
|
|
{
|
|
IdeFrameAddin *addin = (IdeFrameAddin *)exten;
|
|
IdeFrame *self = user_data;
|
|
IdePage *visible_child;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (PEAS_IS_EXTENSION_SET (set));
|
|
g_assert (plugin_info != NULL);
|
|
g_assert (IDE_IS_FRAME_ADDIN (addin));
|
|
|
|
ide_frame_addin_load (addin, self);
|
|
|
|
visible_child = ide_frame_get_visible_child (self);
|
|
|
|
if (visible_child != NULL)
|
|
ide_frame_addin_set_page (addin, visible_child);
|
|
}
|
|
|
|
static void
|
|
ide_frame_addin_removed (PeasExtensionSet *set,
|
|
PeasPluginInfo *plugin_info,
|
|
PeasExtension *exten,
|
|
gpointer user_data)
|
|
{
|
|
IdeFrameAddin *addin = (IdeFrameAddin *)exten;
|
|
IdeFrame *self = user_data;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (PEAS_IS_EXTENSION_SET (set));
|
|
g_assert (plugin_info != NULL);
|
|
g_assert (IDE_IS_FRAME_ADDIN (addin));
|
|
|
|
ide_frame_addin_set_page (addin, NULL);
|
|
ide_frame_addin_unload (addin, self);
|
|
}
|
|
|
|
static gboolean
|
|
ide_frame_pan_begin (IdeFrame *self,
|
|
GdkEventSequence *sequence,
|
|
GtkGesturePan *gesture)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
GtkAllocation alloc;
|
|
cairo_surface_t *surface = NULL;
|
|
GdkDevice *device;
|
|
IdePage *page;
|
|
GdkWindow *window;
|
|
GtkWidget *grid;
|
|
cairo_t *cr;
|
|
gdouble x, y;
|
|
gboolean enable_animations;
|
|
GdkModifierType state = 0;
|
|
|
|
IDE_ENTRY;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (GTK_IS_GESTURE_PAN (gesture));
|
|
g_assert (priv->pan_theatric == NULL);
|
|
|
|
if (!(page = ide_frame_get_visible_child (self)) ||
|
|
!(device = gtk_gesture_get_device (GTK_GESTURE (gesture))) ||
|
|
!(window = gtk_widget_get_window (GTK_WIDGET (page))))
|
|
goto failure;
|
|
|
|
gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
|
|
gdk_device_get_state (device, window, NULL, &state);
|
|
|
|
g_object_get (gtk_settings_get_default (),
|
|
"gtk-enable-animations", &enable_animations,
|
|
NULL);
|
|
|
|
if (sequence != NULL ||
|
|
!enable_animations ||
|
|
(state & GDK_SHIFT_MASK) == 0 ||
|
|
is_uninitialized (&alloc) ||
|
|
NULL == (surface = gdk_window_create_similar_surface (window,
|
|
CAIRO_CONTENT_COLOR,
|
|
alloc.width,
|
|
alloc.height)))
|
|
goto failure;
|
|
|
|
gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
|
|
|
|
cr = cairo_create (surface);
|
|
gtk_widget_draw (GTK_WIDGET (page), cr);
|
|
cairo_destroy (cr);
|
|
|
|
grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
|
|
gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0,
|
|
&alloc.x, &alloc.y);
|
|
|
|
priv->pan_page = g_object_ref (page);
|
|
priv->pan_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
|
|
"surface", surface,
|
|
"target", grid,
|
|
"x", alloc.x + (gint)x,
|
|
"y", alloc.y,
|
|
"width", alloc.width,
|
|
"height", alloc.height,
|
|
NULL);
|
|
|
|
g_clear_pointer (&surface, cairo_surface_destroy);
|
|
|
|
/* Hide the page while we begin the possible transition to another
|
|
* layout stack.
|
|
*/
|
|
gtk_widget_hide (GTK_WIDGET (priv->pan_page));
|
|
|
|
/*
|
|
* Hide the mouse cursor until ide_frame_pan_end() is called.
|
|
* It can be distracting otherwise (and we want to warp it to the new
|
|
* grid column too).
|
|
*/
|
|
ide_frame_set_cursor (self, "none");
|
|
|
|
IDE_RETURN (TRUE);
|
|
|
|
failure:
|
|
if (sequence != NULL)
|
|
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
|
|
|
|
IDE_RETURN (FALSE);
|
|
}
|
|
|
|
static void
|
|
ide_frame_pan_update (IdeFrame *self,
|
|
GdkEventSequence *sequence,
|
|
GtkGestureSwipe *gesture)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
GtkAllocation alloc;
|
|
GtkWidget *grid;
|
|
gdouble x, y;
|
|
|
|
IDE_ENTRY;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (GTK_IS_GESTURE_PAN (gesture));
|
|
g_assert (!priv->pan_theatric || DZL_IS_BOX_THEATRIC (priv->pan_theatric));
|
|
|
|
if (priv->pan_theatric == NULL)
|
|
{
|
|
if (sequence != NULL)
|
|
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
|
|
IDE_EXIT;
|
|
}
|
|
|
|
gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
|
|
gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
|
|
|
|
grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
|
|
gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0,
|
|
&alloc.x, &alloc.y);
|
|
|
|
g_object_set (priv->pan_theatric,
|
|
"x", alloc.x + (gint)x,
|
|
NULL);
|
|
|
|
IDE_EXIT;
|
|
}
|
|
|
|
static void
|
|
ide_frame_pan_end (IdeFrame *self,
|
|
GdkEventSequence *sequence,
|
|
GtkGesturePan *gesture)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
IdeFramePrivate *dest_priv;
|
|
IdeFrame *dest;
|
|
GtkAllocation alloc;
|
|
GtkWidget *grid;
|
|
GtkWidget *column;
|
|
gdouble x, y;
|
|
gint direction;
|
|
gint index = 0;
|
|
|
|
IDE_ENTRY;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (GTK_IS_GESTURE_PAN (gesture));
|
|
|
|
if (priv->pan_theatric == NULL || priv->pan_page == NULL)
|
|
IDE_GOTO (cleanup);
|
|
|
|
gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
|
|
|
|
gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
|
|
|
|
if (x > DISTANCE_THRESHOLD (&alloc))
|
|
direction = 1;
|
|
else if (x < -DISTANCE_THRESHOLD (&alloc))
|
|
direction = -1;
|
|
else
|
|
direction = 0;
|
|
|
|
grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
|
|
g_assert (grid != NULL);
|
|
g_assert (IDE_IS_GRID (grid));
|
|
|
|
column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
|
|
g_assert (column != NULL);
|
|
g_assert (IDE_IS_GRID_COLUMN (column));
|
|
|
|
gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
|
|
"index", &index,
|
|
NULL);
|
|
|
|
dest = _ide_grid_get_nth_stack (IDE_GRID (grid), index + direction);
|
|
dest_priv = ide_frame_get_instance_private (dest);
|
|
g_assert (dest != NULL);
|
|
g_assert (IDE_IS_FRAME (dest));
|
|
|
|
gtk_widget_get_allocation (GTK_WIDGET (dest), &alloc);
|
|
|
|
if (!is_uninitialized (&alloc))
|
|
{
|
|
AnimationState *state;
|
|
|
|
state = g_slice_new0 (AnimationState);
|
|
state->source = g_object_ref (self);
|
|
state->dest = g_object_ref (dest);
|
|
state->page = g_object_ref (priv->pan_page);
|
|
state->theatric = g_object_ref (priv->pan_theatric);
|
|
|
|
gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->top_stack), grid, 0, 0,
|
|
&alloc.x, &alloc.y);
|
|
|
|
/*
|
|
* Use EASE_OUT_CUBIC, because user initiated the beginning of the
|
|
* acceleration curve just by swiping. No need to duplicate.
|
|
*/
|
|
dzl_object_animate_full (state->theatric,
|
|
DZL_ANIMATION_EASE_OUT_CUBIC,
|
|
TRANSITION_DURATION,
|
|
gtk_widget_get_frame_clock (GTK_WIDGET (self)),
|
|
animation_state_complete,
|
|
state,
|
|
"x", alloc.x,
|
|
"width", alloc.width,
|
|
NULL);
|
|
|
|
if (dest != self)
|
|
{
|
|
g_ptr_array_add (priv->in_transition, g_object_ref (priv->pan_page));
|
|
gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (priv->pan_page));
|
|
}
|
|
|
|
IDE_TRACE_MSG ("Animating transition to %s column",
|
|
dest != self ? "another" : "same");
|
|
}
|
|
else
|
|
{
|
|
g_autoptr(IdePage) page = g_object_ref (priv->pan_page);
|
|
|
|
IDE_TRACE_MSG ("Moving page to a previously non-existant column");
|
|
|
|
gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
|
|
gtk_widget_show (GTK_WIDGET (page));
|
|
gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page));
|
|
}
|
|
|
|
cleanup:
|
|
g_clear_object (&priv->pan_theatric);
|
|
g_clear_object (&priv->pan_page);
|
|
|
|
gtk_widget_queue_draw (gtk_widget_get_toplevel (GTK_WIDGET (self)));
|
|
|
|
ide_frame_set_cursor (self, "arrow");
|
|
|
|
IDE_EXIT;
|
|
}
|
|
|
|
static void
|
|
ide_frame_constructed (GObject *object)
|
|
{
|
|
IdeFrame *self = (IdeFrame *)object;
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
|
|
G_OBJECT_CLASS (ide_frame_parent_class)->constructed (object);
|
|
|
|
priv->addins = peas_extension_set_new (peas_engine_get_default (),
|
|
IDE_TYPE_FRAME_ADDIN,
|
|
NULL);
|
|
|
|
g_signal_connect (priv->addins,
|
|
"extension-added",
|
|
G_CALLBACK (ide_frame_addin_added),
|
|
self);
|
|
|
|
g_signal_connect (priv->addins,
|
|
"extension-removed",
|
|
G_CALLBACK (ide_frame_addin_removed),
|
|
self);
|
|
|
|
peas_extension_set_foreach (priv->addins,
|
|
ide_frame_addin_added,
|
|
self);
|
|
|
|
gtk_widget_add_events (GTK_WIDGET (priv->event_box), GDK_TOUCH_MASK);
|
|
priv->pan = g_object_new (GTK_TYPE_GESTURE_PAN,
|
|
"widget", priv->event_box,
|
|
"orientation", GTK_ORIENTATION_HORIZONTAL,
|
|
"n-points", 3,
|
|
NULL);
|
|
g_signal_connect_swapped (priv->pan,
|
|
"begin",
|
|
G_CALLBACK (ide_frame_pan_begin),
|
|
self);
|
|
g_signal_connect_swapped (priv->pan,
|
|
"update",
|
|
G_CALLBACK (ide_frame_pan_update),
|
|
self);
|
|
g_signal_connect_swapped (priv->pan,
|
|
"end",
|
|
G_CALLBACK (ide_frame_pan_end),
|
|
self);
|
|
gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->pan),
|
|
GTK_PHASE_BUBBLE);
|
|
|
|
/*
|
|
* FIXME: Our priv->pan gesture does not activate unless we add another
|
|
* dummy gesture. I currently have no idea why that is.
|
|
*
|
|
* https://bugzilla.gnome.org/show_bug.cgi?id=788914
|
|
*/
|
|
priv->dummy = gtk_gesture_rotate_new (GTK_WIDGET (priv->event_box));
|
|
gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->dummy),
|
|
GTK_PHASE_BUBBLE);
|
|
}
|
|
|
|
static void
|
|
ide_frame_grab_focus (GtkWidget *widget)
|
|
{
|
|
IdeFrame *self = (IdeFrame *)widget;
|
|
IdePage *child;
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
|
|
child = ide_frame_get_visible_child (self);
|
|
|
|
if (child != NULL)
|
|
gtk_widget_grab_focus (GTK_WIDGET (child));
|
|
else
|
|
GTK_WIDGET_CLASS (ide_frame_parent_class)->grab_focus (widget);
|
|
}
|
|
|
|
static void
|
|
ide_frame_destroy (GtkWidget *widget)
|
|
{
|
|
IdeFrame *self = (IdeFrame *)widget;
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
|
|
g_clear_object (&priv->addins);
|
|
|
|
g_clear_pointer (&priv->in_transition, g_ptr_array_unref);
|
|
|
|
if (priv->pages != NULL)
|
|
{
|
|
g_list_model_items_changed (G_LIST_MODEL (self), 0, priv->pages->len, 0);
|
|
g_clear_pointer (&priv->pages, g_ptr_array_unref);
|
|
}
|
|
|
|
if (priv->bindings != NULL)
|
|
{
|
|
dzl_binding_group_set_source (priv->bindings, NULL);
|
|
g_clear_object (&priv->bindings);
|
|
}
|
|
|
|
if (priv->signals != NULL)
|
|
{
|
|
dzl_signal_group_set_target (priv->signals, NULL);
|
|
g_clear_object (&priv->signals);
|
|
}
|
|
|
|
g_clear_object (&priv->pan);
|
|
|
|
GTK_WIDGET_CLASS (ide_frame_parent_class)->destroy (widget);
|
|
}
|
|
|
|
static void
|
|
ide_frame_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
IdeFrame *self = IDE_FRAME (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_HAS_VIEW:
|
|
g_value_set_boolean (value, ide_frame_get_has_page (self));
|
|
break;
|
|
|
|
case PROP_VISIBLE_CHILD:
|
|
g_value_set_object (value, ide_frame_get_visible_child (self));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ide_frame_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
IdeFrame *self = IDE_FRAME (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_VISIBLE_CHILD:
|
|
ide_frame_set_visible_child (self, g_value_get_object (value));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ide_frame_class_init (IdeFrameClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
|
|
|
|
object_class->constructed = ide_frame_constructed;
|
|
object_class->get_property = ide_frame_get_property;
|
|
object_class->set_property = ide_frame_set_property;
|
|
|
|
widget_class->destroy = ide_frame_destroy;
|
|
widget_class->grab_focus = ide_frame_grab_focus;
|
|
|
|
container_class->add = ide_frame_add;
|
|
|
|
klass->agree_to_close_async = ide_frame_real_agree_to_close_async;
|
|
klass->agree_to_close_finish = ide_frame_real_agree_to_close_finish;
|
|
|
|
properties [PROP_HAS_VIEW] =
|
|
g_param_spec_boolean ("has-page", NULL, NULL,
|
|
FALSE,
|
|
(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
|
|
|
properties [PROP_VISIBLE_CHILD] =
|
|
g_param_spec_object ("visible-child",
|
|
"Visible Child",
|
|
"The current page to be displayed",
|
|
IDE_TYPE_PAGE,
|
|
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_properties (object_class, N_PROPS, properties);
|
|
|
|
signals [CHANGE_CURRENT_PAGE] =
|
|
g_signal_new_class_handler ("change-current-page",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
G_CALLBACK (ide_frame_change_current_page),
|
|
NULL, NULL,
|
|
g_cclosure_marshal_VOID__INT,
|
|
G_TYPE_NONE, 1, G_TYPE_INT);
|
|
|
|
gtk_widget_class_set_css_name (widget_class, "ideframe");
|
|
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-frame.ui");
|
|
gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, empty_placeholder);
|
|
gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, failed_state);
|
|
gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, header);
|
|
gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, stack);
|
|
gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, top_stack);
|
|
gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, event_box);
|
|
|
|
g_type_ensure (IDE_TYPE_FRAME_HEADER);
|
|
g_type_ensure (IDE_TYPE_FRAME_WRAPPER);
|
|
g_type_ensure (IDE_TYPE_SHORTCUT_LABEL);
|
|
}
|
|
|
|
static void
|
|
ide_frame_init (IdeFrame *self)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
gtk_widget_init_template (GTK_WIDGET (self));
|
|
|
|
_ide_frame_init_actions (self);
|
|
_ide_frame_init_shortcuts (self);
|
|
|
|
priv->pages = g_ptr_array_new ();
|
|
priv->in_transition = g_ptr_array_new_with_free_func (g_object_unref);
|
|
|
|
priv->signals = dzl_signal_group_new (IDE_TYPE_PAGE);
|
|
|
|
dzl_signal_group_connect_swapped (priv->signals,
|
|
"notify::failed",
|
|
G_CALLBACK (ide_frame_page_failed),
|
|
self);
|
|
|
|
priv->bindings = dzl_binding_group_new ();
|
|
|
|
g_signal_connect_object (priv->bindings,
|
|
"notify::source",
|
|
G_CALLBACK (ide_frame_bindings_notify_source),
|
|
self,
|
|
G_CONNECT_SWAPPED);
|
|
|
|
dzl_binding_group_bind (priv->bindings, "title",
|
|
priv->header, "title",
|
|
G_BINDING_SYNC_CREATE);
|
|
|
|
dzl_binding_group_bind (priv->bindings, "modified",
|
|
priv->header, "modified",
|
|
G_BINDING_SYNC_CREATE);
|
|
|
|
dzl_binding_group_bind (priv->bindings, "primary-color-bg",
|
|
priv->header, "background-rgba",
|
|
G_BINDING_SYNC_CREATE);
|
|
|
|
dzl_binding_group_bind (priv->bindings, "primary-color-fg",
|
|
priv->header, "foreground-rgba",
|
|
G_BINDING_SYNC_CREATE);
|
|
|
|
g_signal_connect_object (priv->stack,
|
|
"notify::visible-child",
|
|
G_CALLBACK (ide_frame_notify_visible_child),
|
|
self,
|
|
G_CONNECT_SWAPPED);
|
|
|
|
g_signal_connect_object (priv->stack,
|
|
"add",
|
|
G_CALLBACK (ide_frame_page_added),
|
|
self,
|
|
G_CONNECT_SWAPPED | G_CONNECT_AFTER);
|
|
|
|
g_signal_connect_object (priv->stack,
|
|
"remove",
|
|
G_CALLBACK (ide_frame_page_removed),
|
|
self,
|
|
G_CONNECT_SWAPPED);
|
|
|
|
_ide_frame_header_set_pages (priv->header, G_LIST_MODEL (self));
|
|
_ide_frame_header_update (priv->header, NULL);
|
|
}
|
|
|
|
GtkWidget *
|
|
ide_frame_new (void)
|
|
{
|
|
return g_object_new (IDE_TYPE_FRAME, NULL);
|
|
}
|
|
|
|
/**
|
|
* ide_frame_set_visible_child:
|
|
* @self: a #IdeFrame
|
|
*
|
|
* Sets the current page for the stack.
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
void
|
|
ide_frame_set_visible_child (IdeFrame *self,
|
|
IdePage *page)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (IDE_IS_PAGE (page));
|
|
g_return_if_fail (gtk_widget_get_parent (GTK_WIDGET (page)) == (GtkWidget *)priv->stack);
|
|
|
|
gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (page));
|
|
}
|
|
|
|
/**
|
|
* ide_frame_get_visible_child:
|
|
* @self: a #IdeFrame
|
|
*
|
|
* Gets the visible #IdePage if there is one; otherwise %NULL.
|
|
*
|
|
* Returns: (nullable) (transfer none): An #IdePage or %NULL
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
IdePage *
|
|
ide_frame_get_visible_child (IdeFrame *self)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
|
|
|
|
return IDE_PAGE (gtk_stack_get_visible_child (priv->stack));
|
|
}
|
|
|
|
/**
|
|
* ide_frame_get_titlebar:
|
|
* @self: a #IdeFrame
|
|
*
|
|
* Gets the #IdeFrameHeader header that is at the top of the stack.
|
|
*
|
|
* Returns: (transfer none) (type IdeFrameHeader): The layout stack header.
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
GtkWidget *
|
|
ide_frame_get_titlebar (IdeFrame *self)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
|
|
|
|
return GTK_WIDGET (priv->header);
|
|
}
|
|
|
|
/**
|
|
* ide_frame_get_has_page:
|
|
* @self: an #IdeFrame
|
|
*
|
|
* Gets the "has-page" property.
|
|
*
|
|
* This property is a convenience to allow widgets to easily bind
|
|
* properties based on whether or not a page is visible in the stack.
|
|
*
|
|
* Returns: %TRUE if the stack has a page
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
gboolean
|
|
ide_frame_get_has_page (IdeFrame *self)
|
|
{
|
|
IdePage *visible_child;
|
|
|
|
g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
|
|
|
|
visible_child = ide_frame_get_visible_child (self);
|
|
|
|
return visible_child != NULL;
|
|
}
|
|
|
|
static void
|
|
ide_frame_close_page_cb (GObject *object,
|
|
GAsyncResult *result,
|
|
gpointer user_data)
|
|
{
|
|
IdePage *page = (IdePage *)object;
|
|
g_autoptr(IdeFrame) self = user_data;
|
|
g_autoptr(GError) error = NULL;
|
|
GtkWidget *toplevel;
|
|
GtkWidget *focus;
|
|
gboolean had_focus = FALSE;
|
|
|
|
g_assert (IDE_IS_PAGE (page));
|
|
g_assert (G_IS_ASYNC_RESULT (result));
|
|
g_assert (IDE_IS_FRAME (self));
|
|
|
|
if (!ide_page_agree_to_close_finish (page, result, &error))
|
|
{
|
|
g_message ("%s does not agree to close: %s",
|
|
G_OBJECT_TYPE_NAME (page),
|
|
error ? error->message : "No reason");
|
|
return;
|
|
}
|
|
|
|
/* Keep track of whether or not the widget had focus (which
|
|
* would happen if we were activated from a keybinding.
|
|
*/
|
|
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (page));
|
|
if (GTK_IS_WINDOW (toplevel) &&
|
|
NULL != (focus = gtk_window_get_focus (GTK_WINDOW (toplevel))) &&
|
|
(focus == GTK_WIDGET (page) ||
|
|
gtk_widget_is_ancestor (focus, GTK_WIDGET (page))))
|
|
had_focus = TRUE;
|
|
|
|
/* Now we can destroy the child */
|
|
gtk_widget_destroy (GTK_WIDGET (page));
|
|
|
|
/* We don't want to leave the widget focus in an indeterminate
|
|
* state so we immediately focus the next child in the stack.
|
|
* But only do so if we had focus previously.
|
|
*/
|
|
if (had_focus)
|
|
{
|
|
IdePage *visible_child = ide_frame_get_visible_child (self);
|
|
|
|
if (visible_child != NULL)
|
|
gtk_widget_grab_focus (GTK_WIDGET (visible_child));
|
|
}
|
|
}
|
|
|
|
void
|
|
_ide_frame_request_close (IdeFrame *self,
|
|
IdePage *page)
|
|
{
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (IDE_IS_PAGE (page));
|
|
|
|
ide_page_agree_to_close_async (page,
|
|
NULL,
|
|
ide_frame_close_page_cb,
|
|
g_object_ref (self));
|
|
}
|
|
|
|
static GType
|
|
ide_frame_get_item_type (GListModel *model)
|
|
{
|
|
return IDE_TYPE_PAGE;
|
|
}
|
|
|
|
static guint
|
|
ide_frame_get_n_items (GListModel *model)
|
|
{
|
|
IdeFrame *self = (IdeFrame *)model;
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
|
|
return priv->pages ? priv->pages->len : 0;
|
|
}
|
|
|
|
static gpointer
|
|
ide_frame_get_item (GListModel *model,
|
|
guint position)
|
|
{
|
|
IdeFrame *self = (IdeFrame *)model;
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_assert (IDE_IS_FRAME (self));
|
|
g_assert (position < priv->pages->len);
|
|
|
|
return g_object_ref (g_ptr_array_index (priv->pages, position));
|
|
}
|
|
|
|
static void
|
|
list_model_iface_init (GListModelInterface *iface)
|
|
{
|
|
iface->get_n_items = ide_frame_get_n_items;
|
|
iface->get_item = ide_frame_get_item;
|
|
iface->get_item_type = ide_frame_get_item_type;
|
|
}
|
|
|
|
void
|
|
ide_frame_agree_to_close_async (IdeFrame *self,
|
|
GCancellable *cancellable,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
|
|
|
|
IDE_FRAME_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
|
|
}
|
|
|
|
gboolean
|
|
ide_frame_agree_to_close_finish (IdeFrame *self,
|
|
GAsyncResult *result,
|
|
GError **error)
|
|
{
|
|
g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
|
|
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
|
|
|
|
return IDE_FRAME_GET_CLASS (self)->agree_to_close_finish (self, result, error);
|
|
}
|
|
|
|
static void
|
|
animation_state_complete (gpointer data)
|
|
{
|
|
IdeFramePrivate *priv;
|
|
AnimationState *state = data;
|
|
|
|
g_assert (state != NULL);
|
|
g_assert (IDE_IS_FRAME (state->source));
|
|
g_assert (IDE_IS_FRAME (state->dest));
|
|
g_assert (IDE_IS_PAGE (state->page));
|
|
|
|
/* Add the widget to the new stack */
|
|
if (state->dest != state->source)
|
|
{
|
|
gtk_container_add (GTK_CONTAINER (state->dest), GTK_WIDGET (state->page));
|
|
|
|
/* Now remove it from our temporary transition. Be careful in case we were
|
|
* destroyed in the mean time.
|
|
*/
|
|
priv = ide_frame_get_instance_private (state->source);
|
|
|
|
if (priv->in_transition != NULL)
|
|
{
|
|
guint position = 0;
|
|
|
|
if (g_ptr_array_find_with_equal_func (priv->pages, state->page, NULL, &position))
|
|
{
|
|
g_ptr_array_remove (priv->in_transition, state->page);
|
|
g_ptr_array_remove_index (priv->pages, position);
|
|
g_list_model_items_changed (G_LIST_MODEL (state->source), position, 1, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We might need to reshow the widget in cases where we are in a
|
|
* three-finger-swipe of the page. There is also a chance that we
|
|
* aren't the proper visible child and that needs to be restored now.
|
|
*/
|
|
gtk_widget_show (GTK_WIDGET (state->page));
|
|
ide_frame_set_visible_child (state->dest, state->page);
|
|
|
|
g_clear_object (&state->source);
|
|
g_clear_object (&state->dest);
|
|
g_clear_object (&state->page);
|
|
g_clear_object (&state->theatric);
|
|
g_slice_free (AnimationState, state);
|
|
}
|
|
|
|
void
|
|
_ide_frame_transfer (IdeFrame *self,
|
|
IdeFrame *dest,
|
|
IdePage *page)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
IdeFramePrivate *dest_priv = ide_frame_get_instance_private (dest);
|
|
const GdkRGBA *fg;
|
|
const GdkRGBA *bg;
|
|
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (IDE_IS_FRAME (dest));
|
|
g_return_if_fail (IDE_IS_PAGE (page));
|
|
g_return_if_fail (GTK_WIDGET (priv->stack) == gtk_widget_get_parent (GTK_WIDGET (page)));
|
|
|
|
/*
|
|
* Inform the destination stack about our new primary colors so that it can
|
|
* begin a transition to the new colors. We also want to do this upfront so
|
|
* that we can reduce the amount of style invalidation caused during the
|
|
* transitions.
|
|
*/
|
|
|
|
fg = ide_page_get_primary_color_fg (page);
|
|
bg = ide_page_get_primary_color_bg (page);
|
|
_ide_frame_header_set_foreground_rgba (dest_priv->header, fg);
|
|
_ide_frame_header_set_background_rgba (dest_priv->header, bg);
|
|
|
|
/*
|
|
* If both the old and the new stacks are mapped, we can animate
|
|
* between them using a snapshot of the page. Well, we also need
|
|
* to be sure they have a valid allocation, but that check is done
|
|
* slightly after this because it makes things easier.
|
|
*/
|
|
if (gtk_widget_get_mapped (GTK_WIDGET (self)) &&
|
|
gtk_widget_get_mapped (GTK_WIDGET (dest)) &&
|
|
gtk_widget_get_mapped (GTK_WIDGET (page)))
|
|
{
|
|
GtkAllocation alloc, dest_alloc;
|
|
cairo_surface_t *surface = NULL;
|
|
GdkWindow *window;
|
|
GtkWidget *grid;
|
|
gboolean enable_animations;
|
|
|
|
grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
|
|
|
|
gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
|
|
gtk_widget_get_allocation (GTK_WIDGET (dest), &dest_alloc);
|
|
|
|
g_object_get (gtk_settings_get_default (),
|
|
"gtk-enable-animations", &enable_animations,
|
|
NULL);
|
|
|
|
if (enable_animations &&
|
|
grid != NULL &&
|
|
!is_uninitialized (&alloc) &&
|
|
!is_uninitialized (&dest_alloc) &&
|
|
dest_alloc.width > 0 && dest_alloc.height > 0 &&
|
|
NULL != (window = gtk_widget_get_window (GTK_WIDGET (page))) &&
|
|
NULL != (surface = gdk_window_create_similar_surface (window,
|
|
CAIRO_CONTENT_COLOR,
|
|
alloc.width,
|
|
alloc.height)))
|
|
{
|
|
DzlBoxTheatric *theatric = NULL;
|
|
AnimationState *state;
|
|
cairo_t *cr;
|
|
|
|
cr = cairo_create (surface);
|
|
gtk_widget_draw (GTK_WIDGET (page), cr);
|
|
cairo_destroy (cr);
|
|
|
|
gtk_widget_translate_coordinates (GTK_WIDGET (priv->stack), grid, 0, 0,
|
|
&alloc.x, &alloc.y);
|
|
gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->stack), grid, 0, 0,
|
|
&dest_alloc.x, &dest_alloc.y);
|
|
|
|
theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
|
|
"surface", surface,
|
|
"height", alloc.height,
|
|
"target", grid,
|
|
"width", alloc.width,
|
|
"x", alloc.x,
|
|
"y", alloc.y,
|
|
NULL);
|
|
|
|
state = g_slice_new0 (AnimationState);
|
|
state->source = g_object_ref (self);
|
|
state->dest = g_object_ref (dest);
|
|
state->page = g_object_ref (page);
|
|
state->theatric = theatric;
|
|
|
|
dzl_object_animate_full (theatric,
|
|
DZL_ANIMATION_EASE_IN_OUT_CUBIC,
|
|
TRANSITION_DURATION,
|
|
gtk_widget_get_frame_clock (GTK_WIDGET (self)),
|
|
animation_state_complete,
|
|
state,
|
|
"x", dest_alloc.x,
|
|
"width", dest_alloc.width,
|
|
"y", dest_alloc.y,
|
|
"height", dest_alloc.height,
|
|
NULL);
|
|
|
|
/*
|
|
* Mark the page as in-transition so that when we remove it
|
|
* we can ignore the items-changed until the animation completes.
|
|
*/
|
|
g_ptr_array_add (priv->in_transition, g_object_ref (page));
|
|
gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
|
|
|
|
cairo_surface_destroy (surface);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
g_object_ref (page);
|
|
gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
|
|
gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page));
|
|
g_object_unref (page);
|
|
}
|
|
|
|
/**
|
|
* ide_frame_foreach_page:
|
|
* @self: a #IdeFrame
|
|
* @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_frame_foreach_page (IdeFrame *self,
|
|
GtkCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (callback != NULL);
|
|
|
|
gtk_container_foreach (GTK_CONTAINER (priv->stack), callback, user_data);
|
|
}
|
|
|
|
/**
|
|
* ide_frame_addin_find_by_module_name:
|
|
* @frame: An #IdeFrame
|
|
* @module_name: the module name which provides the addin
|
|
*
|
|
* This function will locate the #IdeFrameAddin that was registered by
|
|
* the plugin named @module_name (which should match the "Module" field
|
|
* provided in the .plugin file).
|
|
*
|
|
* If no module was found or that module does not implement the
|
|
* #IdeFrameAddinInterface, then %NULL is returned.
|
|
*
|
|
* Returns: (transfer none) (nullable): An #IdeFrameAddin or %NULL
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
IdeFrameAddin *
|
|
ide_frame_addin_find_by_module_name (IdeFrame *frame,
|
|
const gchar *module_name)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (frame);
|
|
PeasExtension *ret = NULL;
|
|
PeasPluginInfo *plugin_info;
|
|
|
|
g_return_val_if_fail (IDE_IS_FRAME (frame), NULL);
|
|
g_return_val_if_fail (priv->addins != NULL, NULL);
|
|
g_return_val_if_fail (module_name != NULL, NULL);
|
|
|
|
plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
|
|
|
|
if (plugin_info != NULL)
|
|
ret = peas_extension_set_get_extension (priv->addins, plugin_info);
|
|
else
|
|
g_warning ("No addin could be found matching module \"%s\"", module_name);
|
|
|
|
return ret ? IDE_FRAME_ADDIN (ret) : NULL;
|
|
}
|
|
|
|
void
|
|
ide_frame_add_with_depth (IdeFrame *self,
|
|
GtkWidget *widget,
|
|
guint position)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (GTK_IS_WIDGET (widget));
|
|
|
|
gtk_container_add_with_properties (GTK_CONTAINER (priv->stack), widget,
|
|
"position", position,
|
|
NULL);
|
|
}
|
|
|
|
static void
|
|
ide_frame_add_child (GtkBuildable *buildable,
|
|
GtkBuilder *builder,
|
|
GObject *object,
|
|
const gchar *type)
|
|
{
|
|
IdeFrame *self = (IdeFrame *)buildable;
|
|
GtkBuildableIface *parent = g_type_interface_peek_parent (GTK_BUILDABLE_GET_IFACE (buildable));
|
|
|
|
if (g_strcmp0 (type, "placeholder") == 0 && GTK_IS_WIDGET (object))
|
|
ide_frame_set_placeholder (self, GTK_WIDGET (object));
|
|
else
|
|
parent->add_child (buildable, builder, object, type);
|
|
}
|
|
|
|
static void
|
|
buildable_iface_init (GtkBuildableIface *iface)
|
|
{
|
|
iface->add_child = ide_frame_add_child;
|
|
}
|
|
|
|
void
|
|
ide_frame_set_placeholder (IdeFrame *self,
|
|
GtkWidget *placeholder)
|
|
{
|
|
IdeFramePrivate *priv = ide_frame_get_instance_private (self);
|
|
|
|
g_return_if_fail (IDE_IS_FRAME (self));
|
|
g_return_if_fail (!placeholder || GTK_IS_WIDGET (placeholder));
|
|
|
|
gtk_container_foreach (GTK_CONTAINER (priv->empty_placeholder),
|
|
(GtkCallback) gtk_widget_destroy,
|
|
NULL);
|
|
|
|
if (placeholder != NULL)
|
|
gtk_container_add (GTK_CONTAINER (priv->empty_placeholder), placeholder);
|
|
}
|