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

896 lines
24 KiB
C

/* ide-page.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-page"
#include "config.h"
#include <libide-threading.h>
#include <string.h>
#include "ide-gui-global.h"
#include "ide-gui-private.h"
#include "ide-page.h"
#include "ide-workspace.h"
typedef struct
{
GList mru_link;
const gchar *menu_id;
const gchar *icon_name;
gchar *title;
GIcon *icon;
GdkRGBA primary_color_bg;
GdkRGBA primary_color_fg;
guint failed : 1;
guint modified : 1;
guint can_split : 1;
guint primary_color_bg_set : 1;
guint primary_color_fg_set : 1;
} IdePagePrivate;
enum {
PROP_0,
PROP_CAN_SPLIT,
PROP_FAILED,
PROP_ICON,
PROP_ICON_NAME,
PROP_MENU_ID,
PROP_MODIFIED,
PROP_PRIMARY_COLOR_BG,
PROP_PRIMARY_COLOR_FG,
PROP_TITLE,
N_PROPS
};
enum {
CREATE_SPLIT,
N_SIGNALS
};
G_DEFINE_TYPE_WITH_PRIVATE (IdePage, ide_page, GTK_TYPE_BOX)
static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];
static void
ide_page_real_agree_to_close_async (IdePage *self,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(IdeTask) task = NULL;
g_assert (IDE_IS_PAGE (self));
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
task = ide_task_new (self, cancellable, callback, user_data);
ide_task_set_priority (task, G_PRIORITY_LOW);
ide_task_set_source_tag (task, ide_page_agree_to_close_async);
ide_task_return_boolean (task, TRUE);
}
static gboolean
ide_page_real_agree_to_close_finish (IdePage *self,
GAsyncResult *result,
GError **error)
{
g_assert (IDE_IS_PAGE (self));
g_assert (IDE_IS_TASK (result));
return ide_task_propagate_boolean (IDE_TASK (result), error);
}
static void
find_focus_child (GtkWidget *widget,
gboolean *handled)
{
if (!*handled)
*handled = gtk_widget_child_focus (widget, GTK_DIR_TAB_FORWARD);
}
static void
ide_page_grab_focus (GtkWidget *widget)
{
gboolean handled = FALSE;
g_assert (IDE_IS_PAGE (widget));
/*
* This default grab_focus override just looks for the first child (generally
* something like a scrolled window) and tries to move forward on focusing
* the child widget. In most cases, this should work without intervention
* from the child subclass.
*/
gtk_container_foreach (GTK_CONTAINER (widget), (GtkCallback) find_focus_child, &handled);
}
static void
ide_page_hierarchy_changed (GtkWidget *widget,
GtkWidget *previous_toplevel)
{
IdePage *self = (IdePage *)widget;
IdePagePrivate *priv = ide_page_get_instance_private (self);
GtkWidget *toplevel;
g_assert (IDE_IS_PAGE (self));
g_assert (!previous_toplevel || GTK_IS_WIDGET (previous_toplevel));
if (IDE_IS_WORKSPACE (previous_toplevel))
_ide_workspace_remove_page_mru (IDE_WORKSPACE (previous_toplevel), &priv->mru_link);
if (GTK_WIDGET_CLASS (ide_page_parent_class)->hierarchy_changed)
GTK_WIDGET_CLASS (ide_page_parent_class)->hierarchy_changed (widget, previous_toplevel);
toplevel = gtk_widget_get_toplevel (widget);
if (IDE_IS_WORKSPACE (toplevel))
_ide_workspace_add_page_mru (IDE_WORKSPACE (toplevel), &priv->mru_link);
}
/**
* ide_page_mark_used:
* @self: a #IdePage
*
* This function marks the page as used by updating it's position in the
* workspaces MRU (most-recently-used) queue.
*
* Pages should call this when their contents have been focused.
*
* Since: 3.32
*/
void
ide_page_mark_used (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
IdeWorkspace *workspace;
g_return_if_fail (IDE_IS_PAGE (self));
if ((workspace = ide_widget_get_workspace (GTK_WIDGET (self))))
_ide_workspace_move_front_page_mru (workspace, &priv->mru_link);
}
static void
ide_page_finalize (GObject *object)
{
IdePage *self = (IdePage *)object;
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_clear_pointer (&priv->title, g_free);
g_clear_object (&priv->icon);
G_OBJECT_CLASS (ide_page_parent_class)->finalize (object);
}
static void
ide_page_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
IdePage *self = IDE_PAGE (object);
switch (prop_id)
{
case PROP_CAN_SPLIT:
g_value_set_boolean (value, ide_page_get_can_split (self));
break;
case PROP_FAILED:
g_value_set_boolean (value, ide_page_get_failed (self));
break;
case PROP_ICON_NAME:
g_value_set_static_string (value, ide_page_get_icon_name (self));
break;
case PROP_ICON:
g_value_set_object (value, ide_page_get_icon (self));
break;
case PROP_MENU_ID:
g_value_set_static_string (value, ide_page_get_menu_id (self));
break;
case PROP_MODIFIED:
g_value_set_boolean (value, ide_page_get_modified (self));
break;
case PROP_PRIMARY_COLOR_BG:
g_value_set_boxed (value, ide_page_get_primary_color_bg (self));
break;
case PROP_PRIMARY_COLOR_FG:
g_value_set_boxed (value, ide_page_get_primary_color_fg (self));
break;
case PROP_TITLE:
g_value_set_string (value, ide_page_get_title (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_page_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
IdePage *self = IDE_PAGE (object);
switch (prop_id)
{
case PROP_CAN_SPLIT:
ide_page_set_can_split (self, g_value_get_boolean (value));
break;
case PROP_FAILED:
ide_page_set_failed (self, g_value_get_boolean (value));
break;
case PROP_ICON_NAME:
ide_page_set_icon_name (self, g_value_get_string (value));
break;
case PROP_ICON:
ide_page_set_icon (self, g_value_get_object (value));
break;
case PROP_MENU_ID:
ide_page_set_menu_id (self, g_value_get_string (value));
break;
case PROP_MODIFIED:
ide_page_set_modified (self, g_value_get_boolean (value));
break;
case PROP_PRIMARY_COLOR_BG:
ide_page_set_primary_color_bg (self, g_value_get_boxed (value));
break;
case PROP_PRIMARY_COLOR_FG:
ide_page_set_primary_color_fg (self, g_value_get_boxed (value));
break;
case PROP_TITLE:
ide_page_set_title (self, g_value_get_string (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_page_class_init (IdePageClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->finalize = ide_page_finalize;
object_class->get_property = ide_page_get_property;
object_class->set_property = ide_page_set_property;
widget_class->grab_focus = ide_page_grab_focus;
widget_class->hierarchy_changed = ide_page_hierarchy_changed;
klass->agree_to_close_async = ide_page_real_agree_to_close_async;
klass->agree_to_close_finish = ide_page_real_agree_to_close_finish;
properties [PROP_CAN_SPLIT] =
g_param_spec_boolean ("can-split",
"Can Split",
"If the view can be split into a second view",
FALSE,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
properties [PROP_FAILED] =
g_param_spec_boolean ("failed",
"Failed",
"If the view has failed or crashed",
FALSE,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
properties [PROP_ICON] =
g_param_spec_object ("icon",
"Icon",
"A GIcon for the view",
G_TYPE_ICON,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
properties [PROP_ICON_NAME] =
g_param_spec_string ("icon-name",
"Icon Name",
"The icon-name describing the view content",
"text-x-generic-symbolic",
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
properties [PROP_MENU_ID] =
g_param_spec_string ("menu-id",
"Menu ID",
"The identifier of the GMenu to use in the document popover",
NULL,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
properties [PROP_MODIFIED] =
g_param_spec_boolean ("modified",
"Modified",
"If the view has been modified from the saved content",
FALSE,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
/**
* IdePage:primary-color-bg:
*
* The "primary-color-bg" property should describe the primary color
* of the content of the view (if any).
*
* This can be used by the layout stack to alter the color of the
* header to match that of the content.
*
* Since: 3.32
*/
properties [PROP_PRIMARY_COLOR_BG] =
g_param_spec_boxed ("primary-color-bg",
"Primary Color Background",
"The primary foreground color of the content",
GDK_TYPE_RGBA,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
/**
* IdePage:primary-color-fg:
*
* The "primary-color-fg" property should describe the foreground
* to use for content above primary-color-bg.
*
* This can be used by the layout stack to alter the color of the
* foreground to match that of the content.
*
* Since: 3.32
*/
properties [PROP_PRIMARY_COLOR_FG] =
g_param_spec_boxed ("primary-color-fg",
"Primary Color Foreground",
"The primary foreground color of the content",
GDK_TYPE_RGBA,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
properties [PROP_TITLE] =
g_param_spec_string ("title",
"Title",
"The title of the document or view",
NULL,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, N_PROPS, properties);
/**
* IdePage::create-split:
* @self: an #IdePage
*
* This signal is emitted when the view is requested to make a split
* version of itself. This happens when the user requests that a second
* version of the file to be displayed, often side-by-side.
*
* This signal will only be emitted when #IdePage:can-split is
* set to %TRUE. The default is %FALSE.
*
* Returns: (transfer full): A newly created #IdePage
*
* Since: 3.32
*/
signals [CREATE_SPLIT] =
g_signal_new (g_intern_static_string ("create-split"),
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (IdePageClass, create_split),
g_signal_accumulator_first_wins, NULL,
NULL, IDE_TYPE_PAGE, 0);
}
static void
ide_page_init (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_autoptr(GSimpleActionGroup) group = g_simple_action_group_new ();
gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL);
priv->mru_link.data = self;
priv->icon_name = g_intern_string ("text-x-generic-symbolic");
/* Add an action group out of convenience to plugins that want to
* stash a simple action somewhere.
*/
gtk_widget_insert_action_group (GTK_WIDGET (self), "view", G_ACTION_GROUP (group));
}
GtkWidget *
ide_page_new (void)
{
return g_object_new (IDE_TYPE_PAGE, NULL);
}
const gchar *
ide_page_get_title (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
return priv->title;
}
void
ide_page_set_title (IdePage *self,
const gchar *title)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_if_fail (IDE_IS_PAGE (self));
if (g_strcmp0 (title, priv->title) != 0)
{
g_free (priv->title);
priv->title = g_strdup (title);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
}
}
const gchar *
ide_page_get_menu_id (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
return priv->menu_id;
}
void
ide_page_set_menu_id (IdePage *self,
const gchar *menu_id)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_if_fail (IDE_IS_PAGE (self));
menu_id = g_intern_string (menu_id);
if (menu_id != priv->menu_id)
{
priv->menu_id = menu_id;
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU_ID]);
}
}
void
ide_page_agree_to_close_async (IdePage *self,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_return_if_fail (IDE_IS_PAGE (self));
g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
IDE_PAGE_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
}
gboolean
ide_page_agree_to_close_finish (IdePage *self,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
return IDE_PAGE_GET_CLASS (self)->agree_to_close_finish (self, result, error);
}
gboolean
ide_page_get_failed (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
return priv->failed;
}
void
ide_page_set_failed (IdePage *self,
gboolean failed)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_if_fail (IDE_IS_PAGE (self));
failed = !!failed;
if (failed != priv->failed)
{
priv->failed = failed;
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FAILED]);
}
}
gboolean
ide_page_get_modified (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
return priv->modified;
}
void
ide_page_set_modified (IdePage *self,
gboolean modified)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_if_fail (IDE_IS_PAGE (self));
modified = !!modified;
if (priv->modified != modified)
{
priv->modified = modified;
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODIFIED]);
}
}
/**
* ide_page_get_icon:
* @self: a #IdePage
*
* Gets the #GIcon to represent the view.
*
* Returns: (transfer none) (nullable): A #GIcon or %NULL
*
* Since: 3.32
*/
GIcon *
ide_page_get_icon (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
if (priv->icon == NULL)
{
if (priv->icon_name != NULL)
priv->icon = g_icon_new_for_string (priv->icon_name, NULL);
}
return priv->icon;
}
void
ide_page_set_icon (IdePage *self,
GIcon *icon)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_if_fail (IDE_IS_PAGE (self));
if (g_set_object (&priv->icon, icon))
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
}
const gchar *
ide_page_get_icon_name (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
return priv->icon_name;
}
void
ide_page_set_icon_name (IdePage *self,
const gchar *icon_name)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_if_fail (IDE_IS_PAGE (self));
icon_name = g_intern_string (icon_name);
if (icon_name != priv->icon_name)
{
priv->icon_name = icon_name;
g_clear_object (&priv->icon);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
}
}
gboolean
ide_page_get_can_split (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
return priv->can_split;
}
void
ide_page_set_can_split (IdePage *self,
gboolean can_split)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_if_fail (IDE_IS_PAGE (self));
can_split = !!can_split;
if (priv->can_split != can_split)
{
priv->can_split = can_split;
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_SPLIT]);
}
}
/**
* ide_page_create_split:
* @self: an #IdePage
*
* This function requests that the #IdePage create a split version
* of itself so that the user may view the document in multiple views.
*
* The view should be added to an #IdeLayoutStack where appropriate.
*
* Returns: (nullable) (transfer full): A newly created #IdePage or %NULL.
*
* Since: 3.32
*/
IdePage *
ide_page_create_split (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
IdePage *ret = NULL;
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
if (priv->can_split)
{
g_signal_emit (self, signals [CREATE_SPLIT], 0, &ret);
g_return_val_if_fail (!ret || IDE_IS_PAGE (ret), NULL);
}
return ret;
}
/**
* ide_page_get_primary_color_bg:
* @self: a #IdePage
*
* Gets the #IdePage:primary-color-bg property if it has been set.
*
* The primary-color-bg can be used to alter the color of the layout
* stack header to match the document contents.
*
* Returns: (transfer none) (nullable): a #GdkRGBA or %NULL.
*
* Since: 3.32
*/
const GdkRGBA *
ide_page_get_primary_color_bg (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
return priv->primary_color_bg_set ? &priv->primary_color_bg : NULL;
}
/**
* ide_page_set_primary_color_bg:
* @self: a #IdePage
* @primary_color_bg: (nullable): a #GdkRGBA or %NULL
*
* Sets the #IdePage:primary-color-bg property.
* If @primary_color_bg is %NULL, the property is unset.
*
* Since: 3.32
*/
void
ide_page_set_primary_color_bg (IdePage *self,
const GdkRGBA *primary_color_bg)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
gboolean old_set;
GdkRGBA old;
g_return_if_fail (IDE_IS_PAGE (self));
old_set = priv->primary_color_bg_set;
old = priv->primary_color_bg;
if (primary_color_bg != NULL)
{
priv->primary_color_bg = *primary_color_bg;
priv->primary_color_bg_set = TRUE;
}
else
{
memset (&priv->primary_color_bg, 0, sizeof priv->primary_color_bg);
priv->primary_color_bg_set = FALSE;
}
if (old_set != priv->primary_color_bg_set ||
!gdk_rgba_equal (&old, &priv->primary_color_bg))
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIMARY_COLOR_BG]);
}
/**
* ide_page_get_primary_color_fg:
* @self: a #IdePage
*
* Gets the #IdePage:primary-color-fg property if it has been set.
*
* The primary-color-fg can be used to alter the foreground color of the layout
* stack header to match the document contents.
*
* Returns: (transfer none) (nullable): a #GdkRGBA or %NULL.
*
* Since: 3.32
*/
const GdkRGBA *
ide_page_get_primary_color_fg (IdePage *self)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
return priv->primary_color_fg_set ? &priv->primary_color_fg : NULL;
}
/**
* ide_page_set_primary_color_fg:
* @self: a #IdePage
* @primary_color_fg: (nullable): a #GdkRGBA or %NULL
*
* Sets the #IdePage:primary-color-fg property.
* If @primary_color_fg is %NULL, the property is unset.
*
* Since: 3.32
*/
void
ide_page_set_primary_color_fg (IdePage *self,
const GdkRGBA *primary_color_fg)
{
IdePagePrivate *priv = ide_page_get_instance_private (self);
gboolean old_set;
GdkRGBA old;
g_return_if_fail (IDE_IS_PAGE (self));
old_set = priv->primary_color_fg_set;
old = priv->primary_color_fg;
if (primary_color_fg != NULL)
{
priv->primary_color_fg = *primary_color_fg;
priv->primary_color_fg_set = TRUE;
}
else
{
memset (&priv->primary_color_fg, 0, sizeof priv->primary_color_fg);
priv->primary_color_fg_set = FALSE;
}
if (old_set != priv->primary_color_fg_set ||
!gdk_rgba_equal (&old, &priv->primary_color_fg))
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIMARY_COLOR_FG]);
}
/**
* ide_page_report_error:
* @self: a #IdePage
* @format: a printf-style format string
*
* This function reports an error to the user in the layout view.
*
* @format should be a printf-style format string followed by the
* arguments for the format.
*
* Since: 3.32
*/
void
ide_page_report_error (IdePage *self,
const gchar *format,
...)
{
g_autofree gchar *message = NULL;
GtkInfoBar *infobar;
GtkWidget *content_area;
GtkLabel *label;
va_list args;
g_return_if_fail (IDE_IS_PAGE (self));
va_start (args, format);
message = g_strdup_vprintf (format, args);
va_end (args);
infobar = g_object_new (GTK_TYPE_INFO_BAR,
"message-type", GTK_MESSAGE_WARNING,
"show-close-button", TRUE,
"visible", TRUE,
NULL);
g_signal_connect (infobar,
"response",
G_CALLBACK (gtk_widget_destroy),
NULL);
g_signal_connect (infobar,
"close",
G_CALLBACK (gtk_widget_destroy),
NULL);
label = g_object_new (GTK_TYPE_LABEL,
"label", message,
"visible", TRUE,
"wrap", TRUE,
"xalign", 0.0f,
NULL);
content_area = gtk_info_bar_get_content_area (infobar);
gtk_container_add (GTK_CONTAINER (content_area), GTK_WIDGET (label));
gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (infobar),
"position", 0,
NULL);
}
/**
* ide_page_get_file_or_directory:
* @self: a #IdePage
*
* Gets a #GFile representing a file or directory that best maps to this
* page. A terminal might use the current working directory while an editor
* or designer might use the backing file.
*
* Returns: (transfer full) (nullable): a #GFile or %NULL
*
* Since: 3.40
*/
GFile *
ide_page_get_file_or_directory (IdePage *self)
{
g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
if (IDE_PAGE_GET_CLASS (self)->get_file_or_directory)
return IDE_PAGE_GET_CLASS (self)->get_file_or_directory (self);
return NULL;
}