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