diff --git a/gresources/nemo-file-management-properties.glade b/gresources/nemo-file-management-properties.glade index 02b8e4cb4..bf1783e35 100644 --- a/gresources/nemo-file-management-properties.glade +++ b/gresources/nemo-file-management-properties.glade @@ -609,6 +609,22 @@ along with . If not, see . 5 + + + Show preview pane + True + True + True + True + 0 + True + + + False + False + 6 + + diff --git a/gresources/nemo-shell-ui.xml b/gresources/nemo-shell-ui.xml index 50ce6d15f..e51b39a69 100644 --- a/gresources/nemo-shell-ui.xml +++ b/gresources/nemo-shell-ui.xml @@ -72,7 +72,8 @@ - + + diff --git a/libnemo-private/meson.build b/libnemo-private/meson.build index 16c061882..4607e6c72 100644 --- a/libnemo-private/meson.build +++ b/libnemo-private/meson.build @@ -55,6 +55,8 @@ nemo_private_sources = [ 'nemo-monitor.c', 'nemo-placement-grid.c', 'nemo-places-tree-view.c', + 'nemo-preview-details.c', + 'nemo-preview-image.c', 'nemo-program-choosing.c', 'nemo-progress-info-manager.c', 'nemo-progress-info.c', diff --git a/libnemo-private/nemo-file.c b/libnemo-private/nemo-file.c index 01d23be22..738c22124 100644 --- a/libnemo-private/nemo-file.c +++ b/libnemo-private/nemo-file.c @@ -4555,6 +4555,18 @@ nemo_file_has_loaded_thumbnail (NemoFile *file) return file->details->thumbnail_is_up_to_date; } +char * +nemo_file_get_thumbnail_path (NemoFile *file) +{ + g_return_val_if_fail (NEMO_IS_FILE (file), NULL); + + if (file->details->thumbnail_path != NULL) { + return g_strdup (file->details->thumbnail_path); + } + + return NULL; +} + static void prepend_icon_name (const char *name, GThemedIcon *icon) diff --git a/libnemo-private/nemo-file.h b/libnemo-private/nemo-file.h index 8b0f76975..73055daef 100644 --- a/libnemo-private/nemo-file.h +++ b/libnemo-private/nemo-file.h @@ -252,6 +252,7 @@ NemoRequestStatus nemo_file_get_deep_counts (NemoFile gboolean nemo_file_should_show_thumbnail (NemoFile *file); void nemo_file_delete_thumbnail (NemoFile *file); gboolean nemo_file_has_loaded_thumbnail (NemoFile *file); +char * nemo_file_get_thumbnail_path (NemoFile *file); gboolean nemo_file_should_show_directory_item_count (NemoFile *file); gboolean nemo_file_should_show_type (NemoFile *file); GList * nemo_file_get_keywords (NemoFile *file); diff --git a/libnemo-private/nemo-global-preferences.c b/libnemo-private/nemo-global-preferences.c index f57fcbcb4..6e6ec5f97 100644 --- a/libnemo-private/nemo-global-preferences.c +++ b/libnemo-private/nemo-global-preferences.c @@ -50,6 +50,7 @@ GSettings *gtk_filechooser_preferences; GSettings *nemo_plugin_preferences; GSettings *nemo_menu_config_preferences; GSettings *nemo_search_preferences; +GSettings *nemo_preview_pane_preferences; GSettings *gnome_lockdown_preferences; GSettings *gnome_background_preferences; GSettings *gnome_media_handling_preferences; @@ -473,6 +474,7 @@ nemo_global_preferences_init (void) nemo_plugin_preferences = g_settings_new("org.nemo.plugins"); nemo_menu_config_preferences = g_settings_new("org.nemo.preferences.menu-config"); nemo_search_preferences = g_settings_new("org.nemo.search"); + nemo_preview_pane_preferences = g_settings_new("org.nemo.preview-pane"); gnome_lockdown_preferences = g_settings_new("org.cinnamon.desktop.lockdown"); gnome_background_preferences = g_settings_new("org.cinnamon.desktop.background"); gnome_media_handling_preferences = g_settings_new("org.cinnamon.desktop.media-handling"); @@ -506,6 +508,7 @@ nemo_global_preferences_finalize (void) g_object_unref (nemo_plugin_preferences); g_object_unref (nemo_menu_config_preferences); g_object_unref (nemo_search_preferences); + g_object_unref (nemo_preview_pane_preferences); g_object_unref (gnome_lockdown_preferences); g_object_unref (gnome_background_preferences); g_object_unref (gnome_media_handling_preferences); diff --git a/libnemo-private/nemo-global-preferences.h b/libnemo-private/nemo-global-preferences.h index 576a4616d..00a6d6a34 100644 --- a/libnemo-private/nemo-global-preferences.h +++ b/libnemo-private/nemo-global-preferences.h @@ -42,6 +42,7 @@ G_BEGIN_DECLS /* Display */ #define NEMO_PREFERENCES_SHOW_HIDDEN_FILES "show-hidden-files" #define NEMO_PREFERENCES_SHOW_ADVANCED_PERMISSIONS "show-advanced-permissions" +#define NEMO_PREFERENCES_SHOW_PREVIEW_PANE "show-preview-pane" #define NEMO_PREFERENCES_DATE_FORMAT "date-format" #define NEMO_PREFERENCES_DATE_FONT_CHOICE "date-font-choice" #define NEMO_PREFERENCES_MONO_FONT_NAME "monospace-font-name" @@ -315,6 +316,7 @@ extern GSettings *gtk_filechooser_preferences; extern GSettings *nemo_plugin_preferences; extern GSettings *nemo_menu_config_preferences; extern GSettings *nemo_search_preferences; +extern GSettings *nemo_preview_pane_preferences; extern GSettings *gnome_lockdown_preferences; extern GSettings *gnome_background_preferences; extern GSettings *gnome_media_handling_preferences; diff --git a/libnemo-private/nemo-metadata.c b/libnemo-private/nemo-metadata.c index c31231c03..d7613ee28 100644 --- a/libnemo-private/nemo-metadata.c +++ b/libnemo-private/nemo-metadata.c @@ -46,6 +46,7 @@ static char *used_metadata_names[] = { (char *)NEMO_METADATA_KEY_WINDOW_MAXIMIZED, (char *)NEMO_METADATA_KEY_WINDOW_STICKY, (char *)NEMO_METADATA_KEY_WINDOW_KEEP_ABOVE, + (char *)NEMO_METADATA_KEY_WINDOW_SHOW_PREVIEW_PANE, (char *)NEMO_METADATA_KEY_SIDEBAR_BACKGROUND_COLOR, (char *)NEMO_METADATA_KEY_SIDEBAR_BACKGROUND_IMAGE, (char *)NEMO_METADATA_KEY_SIDEBAR_BUTTONS, diff --git a/libnemo-private/nemo-metadata.h b/libnemo-private/nemo-metadata.h index da4142fa1..fcbc4ee54 100644 --- a/libnemo-private/nemo-metadata.h +++ b/libnemo-private/nemo-metadata.h @@ -60,6 +60,7 @@ #define NEMO_METADATA_KEY_WINDOW_MAXIMIZED "nemo-window-maximized" #define NEMO_METADATA_KEY_WINDOW_STICKY "nemo-window-sticky" #define NEMO_METADATA_KEY_WINDOW_KEEP_ABOVE "nemo-window-keep-above" +#define NEMO_METADATA_KEY_WINDOW_SHOW_PREVIEW_PANE "nemo-window-show-preview-pane" #define NEMO_METADATA_KEY_SIDEBAR_BACKGROUND_COLOR "nemo-sidebar-background-color" #define NEMO_METADATA_KEY_SIDEBAR_BACKGROUND_IMAGE "nemo-sidebar-background-image" diff --git a/libnemo-private/nemo-preview-details.c b/libnemo-private/nemo-preview-details.c new file mode 100644 index 000000000..6f1bd8e90 --- /dev/null +++ b/libnemo-private/nemo-preview-details.c @@ -0,0 +1,257 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* + * nemo-preview-details.c - Widget for displaying file details in preview pane + * + * Copyright (C) 2025 Linux Mint + * + * Nemo 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 2 of the + * License, or (at your option) any later version. + * + * Nemo 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street - Suite 500, + * Boston, MA 02110-1335, USA. + */ + +#include "nemo-preview-details.h" +#include + +struct _NemoPreviewDetails { + GtkBox parent; +}; + +typedef struct { + GtkWidget *grid; + GtkWidget *name_value_label; + GtkWidget *size_value_label; + GtkWidget *type_value_label; + GtkWidget *modified_value_label; + GtkWidget *permissions_value_label; + GtkWidget *location_value_label; + + NemoFile *file; +} NemoPreviewDetailsPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (NemoPreviewDetails, nemo_preview_details, GTK_TYPE_BOX) + +static void +nemo_preview_details_finalize (GObject *object) +{ + NemoPreviewDetails *details; + NemoPreviewDetailsPrivate *priv; + + details = NEMO_PREVIEW_DETAILS (object); + priv = nemo_preview_details_get_instance_private (details); + + if (priv->file != NULL) { + nemo_file_unref (priv->file); + priv->file = NULL; + } + + G_OBJECT_CLASS (nemo_preview_details_parent_class)->finalize (object); +} + +static GtkWidget * +create_label_pair (GtkGrid *grid, const gchar *label_text, gint row) +{ + GtkWidget *label; + GtkWidget *value; + + /* Create the label (left column) */ + label = gtk_label_new (label_text); + gtk_widget_set_halign (label, GTK_ALIGN_END); + gtk_widget_set_valign (label, GTK_ALIGN_START); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_grid_attach (grid, label, 0, row, 1, 1); + gtk_widget_show (label); + + /* Create the value label (right column) */ + value = gtk_label_new (""); + gtk_widget_set_halign (value, GTK_ALIGN_START); + gtk_widget_set_valign (value, GTK_ALIGN_START); + gtk_label_set_selectable (GTK_LABEL (value), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (value), PANGO_ELLIPSIZE_MIDDLE); + gtk_grid_attach (grid, value, 1, row, 1, 1); + gtk_widget_show (value); + + return value; +} + +static void +nemo_preview_details_init (NemoPreviewDetails *details) +{ + NemoPreviewDetailsPrivate *priv; + GtkGrid *grid; + + priv = nemo_preview_details_get_instance_private (details); + + /* Create the grid for label pairs */ + grid = GTK_GRID (gtk_grid_new ()); + gtk_grid_set_row_spacing (grid, 6); + gtk_grid_set_column_spacing (grid, 12); + gtk_widget_set_margin_start (GTK_WIDGET (grid), 12); + gtk_widget_set_margin_end (GTK_WIDGET (grid), 12); + gtk_widget_set_margin_top (GTK_WIDGET (grid), 12); + gtk_widget_set_margin_bottom (GTK_WIDGET (grid), 12); + priv->grid = GTK_WIDGET (grid); + + /* Create all the label pairs */ + priv->name_value_label = create_label_pair (grid, _("Name:"), 0); + priv->size_value_label = create_label_pair (grid, _("Size:"), 1); + priv->type_value_label = create_label_pair (grid, _("Type:"), 2); + priv->modified_value_label = create_label_pair (grid, _("Modified:"), 3); + priv->permissions_value_label = create_label_pair (grid, _("Permissions:"), 4); + priv->location_value_label = create_label_pair (grid, _("Location:"), 5); + + gtk_box_pack_start (GTK_BOX (details), GTK_WIDGET (grid), FALSE, FALSE, 0); + gtk_widget_show (GTK_WIDGET (grid)); +} + +static void +nemo_preview_details_class_init (NemoPreviewDetailsClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = nemo_preview_details_finalize; +} + +GtkWidget * +nemo_preview_details_new (void) +{ + return g_object_new (NEMO_TYPE_PREVIEW_DETAILS, + "orientation", GTK_ORIENTATION_VERTICAL, + "spacing", 0, + NULL); +} + +void +nemo_preview_details_set_file (NemoPreviewDetails *widget, + NemoFile *file) +{ + NemoPreviewDetailsPrivate *priv; + gchar *str; + GFile *location; + GFile *parent; + gchar *parent_path; + + g_return_if_fail (NEMO_IS_PREVIEW_DETAILS (widget)); + + priv = nemo_preview_details_get_instance_private (widget); + + if (priv->file == file) { + return; + } + + if (priv->file != NULL) { + nemo_file_unref (priv->file); + } + + priv->file = file; + + if (file != NULL) { + nemo_file_ref (file); + + /* Name */ + str = nemo_file_get_display_name (file); + gtk_label_set_text (GTK_LABEL (priv->name_value_label), str); + g_free (str); + + /* Size */ + str = nemo_file_get_string_attribute (file, "size"); + if (str != NULL) { + gtk_label_set_text (GTK_LABEL (priv->size_value_label), str); + g_free (str); + } else { + gtk_label_set_text (GTK_LABEL (priv->size_value_label), "—"); + } + + /* Type */ + str = nemo_file_get_string_attribute (file, "type"); + if (str != NULL) { + gtk_label_set_text (GTK_LABEL (priv->type_value_label), str); + g_free (str); + } else { + gtk_label_set_text (GTK_LABEL (priv->type_value_label), "—"); + } + + /* Modified */ + str = nemo_file_get_string_attribute (file, "date_modified"); + if (str != NULL) { + gtk_label_set_text (GTK_LABEL (priv->modified_value_label), str); + g_free (str); + } else { + gtk_label_set_text (GTK_LABEL (priv->modified_value_label), "—"); + } + + /* Permissions */ + str = nemo_file_get_string_attribute (file, "permissions"); + if (str != NULL) { + gtk_label_set_text (GTK_LABEL (priv->permissions_value_label), str); + g_free (str); + } else { + gtk_label_set_text (GTK_LABEL (priv->permissions_value_label), "—"); + } + + /* Location - use activation URI to get real location for virtual folders */ + str = nemo_file_get_activation_uri (file); + if (str != NULL) { + location = g_file_new_for_uri (str); + g_free (str); + + parent = g_file_get_parent (location); + if (parent != NULL) { + parent_path = g_file_get_parse_name (parent); + gtk_label_set_text (GTK_LABEL (priv->location_value_label), parent_path); + g_free (parent_path); + g_object_unref (parent); + } else { + gtk_label_set_text (GTK_LABEL (priv->location_value_label), "—"); + } + g_object_unref (location); + } else { + /* Fallback to regular location if no activation URI */ + location = nemo_file_get_location (file); + parent = g_file_get_parent (location); + if (parent != NULL) { + parent_path = g_file_get_parse_name (parent); + gtk_label_set_text (GTK_LABEL (priv->location_value_label), parent_path); + g_free (parent_path); + g_object_unref (parent); + } else { + gtk_label_set_text (GTK_LABEL (priv->location_value_label), "—"); + } + g_object_unref (location); + } + } +} + +void +nemo_preview_details_clear (NemoPreviewDetails *widget) +{ + NemoPreviewDetailsPrivate *priv; + + g_return_if_fail (NEMO_IS_PREVIEW_DETAILS (widget)); + + priv = nemo_preview_details_get_instance_private (widget); + + if (priv->file != NULL) { + nemo_file_unref (priv->file); + priv->file = NULL; + } + + gtk_label_set_text (GTK_LABEL (priv->name_value_label), ""); + gtk_label_set_text (GTK_LABEL (priv->size_value_label), ""); + gtk_label_set_text (GTK_LABEL (priv->type_value_label), ""); + gtk_label_set_text (GTK_LABEL (priv->modified_value_label), ""); + gtk_label_set_text (GTK_LABEL (priv->permissions_value_label), ""); + gtk_label_set_text (GTK_LABEL (priv->location_value_label), ""); +} diff --git a/libnemo-private/nemo-preview-details.h b/libnemo-private/nemo-preview-details.h new file mode 100644 index 000000000..ccd2735b1 --- /dev/null +++ b/libnemo-private/nemo-preview-details.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* + * nemo-preview-details.h - Widget for displaying file details in preview pane + * + * Copyright (C) 2025 Linux Mint + * + * Nemo 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 2 of the + * License, or (at your option) any later version. + * + * Nemo 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street - Suite 500, + * Boston, MA 02110-1335, USA. + */ + +#ifndef NEMO_PREVIEW_DETAILS_H +#define NEMO_PREVIEW_DETAILS_H + +#include +#include "nemo-file.h" + +#define NEMO_TYPE_PREVIEW_DETAILS (nemo_preview_details_get_type()) + +G_DECLARE_FINAL_TYPE (NemoPreviewDetails, nemo_preview_details, NEMO, PREVIEW_DETAILS, GtkBox) +GtkWidget *nemo_preview_details_new (void); + +void nemo_preview_details_set_file (NemoPreviewDetails *widget, + NemoFile *file); +void nemo_preview_details_clear (NemoPreviewDetails *widget); + +#endif /* NEMO_PREVIEW_DETAILS_H */ diff --git a/libnemo-private/nemo-preview-image.c b/libnemo-private/nemo-preview-image.c new file mode 100644 index 000000000..1212ef09f --- /dev/null +++ b/libnemo-private/nemo-preview-image.c @@ -0,0 +1,695 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* + * nemo-preview-image.c - Widget for displaying image preview in preview pane + * + * Copyright (C) 2025 Linux Mint + * + * Nemo 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 2 of the + * License, or (at your option) any later version. + * + * Nemo 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street - Suite 500, + * Boston, MA 02110-1335, USA. + */ + +#include "nemo-preview-image.h" +#include "nemo-file-attributes.h" +#include "nemo-icon-info.h" +#include "nemo-thumbnails.h" +#include + +#define RESIZE_DEBOUNCE_MS 150 +#define MIN_SIZE_CHANGE 10 + +struct _NemoPreviewImage { + GtkBox parent; +}; + +typedef struct _LoadImageData LoadImageData; + +typedef struct { + GtkWidget *frame; + GtkWidget *drawing_area; + GtkWidget *message_label; + NemoFile *file; + + guint resize_timeout_id; + gint current_width; + gint current_height; + + cairo_surface_t *current_surface; + gint surface_width; + gint surface_height; + + gboolean showing_icon; + + LoadImageData *current_load_data; +} NemoPreviewImagePrivate; + +struct _LoadImageData { + gchar *file_path; /* For direct loading (can be NULL) */ + gchar *thumbnail_path; /* For thumbnail loading (can be NULL) */ + gint width; + gint height; + gint ui_scale; + GCancellable *cancellable; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (NemoPreviewImage, nemo_preview_image, GTK_TYPE_BOX) + +static void on_size_allocate (GtkWidget *widget, GtkAllocation *allocation, gpointer user_data); +static gboolean on_drawing_area_draw (GtkWidget *widget, cairo_t *cr, gpointer user_data); + +static LoadImageData * +load_image_data_new (const gchar *file_path, + const gchar *thumbnail_path, + gint width, + gint height, + gint ui_scale) +{ + LoadImageData *data; + + data = g_new0 (LoadImageData, 1); + data->file_path = g_strdup (file_path); + data->thumbnail_path = g_strdup (thumbnail_path); + data->width = width; + data->height = height; + data->ui_scale = ui_scale; + data->cancellable = g_cancellable_new (); + + return data; +} + +static void +load_image_data_free (LoadImageData *data) +{ + if (data == NULL) { + return; + } + + g_free (data->file_path); + g_free (data->thumbnail_path); + + if (data->cancellable != NULL) { + g_object_unref (data->cancellable); + } + + g_free (data); +} + +static void +nemo_preview_image_finalize (GObject *object) +{ + NemoPreviewImage *preview; + NemoPreviewImagePrivate *priv; + + preview = NEMO_PREVIEW_IMAGE (object); + priv = nemo_preview_image_get_instance_private (preview); + + if (priv->current_load_data != NULL) { + g_cancellable_cancel (priv->current_load_data->cancellable); + priv->current_load_data = NULL; // Forget about it, GTask will clean it up + } + + if (priv->resize_timeout_id != 0) { + g_source_remove (priv->resize_timeout_id); + priv->resize_timeout_id = 0; + } + + if (priv->current_surface != NULL) { + cairo_surface_destroy (priv->current_surface); + priv->current_surface = NULL; + } + + if (priv->file != NULL) { + nemo_file_unref (priv->file); + priv->file = NULL; + } + + G_OBJECT_CLASS (nemo_preview_image_parent_class)->finalize (object); +} + +static void +nemo_preview_image_init (NemoPreviewImage *preview) +{ + NemoPreviewImagePrivate *priv; + + priv = nemo_preview_image_get_instance_private (preview); + + priv->resize_timeout_id = 0; + priv->current_width = 0; + priv->current_height = 0; + priv->current_surface = NULL; + priv->surface_width = 0; + priv->surface_height = 0; + priv->showing_icon = FALSE; + priv->current_load_data = NULL; + + priv->frame = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (priv->frame), GTK_SHADOW_NONE); + gtk_widget_set_halign (priv->frame, GTK_ALIGN_FILL); + gtk_widget_set_valign (priv->frame, GTK_ALIGN_FILL); + gtk_widget_set_hexpand (priv->frame, TRUE); + gtk_widget_set_vexpand (priv->frame, TRUE); + + gtk_box_pack_start (GTK_BOX (preview), priv->frame, TRUE, TRUE, 0); + + priv->drawing_area = gtk_drawing_area_new (); + gtk_widget_set_halign (priv->drawing_area, GTK_ALIGN_FILL); + gtk_widget_set_valign (priv->drawing_area, GTK_ALIGN_FILL); + gtk_widget_set_hexpand (priv->drawing_area, TRUE); + gtk_widget_set_vexpand (priv->drawing_area, TRUE); + g_signal_connect (priv->drawing_area, "draw", + G_CALLBACK (on_drawing_area_draw), preview); + gtk_container_add (GTK_CONTAINER (priv->frame), priv->drawing_area); + + priv->message_label = gtk_label_new (""); + gtk_widget_set_halign (priv->message_label, GTK_ALIGN_CENTER); + gtk_widget_set_valign (priv->message_label, GTK_ALIGN_CENTER); + gtk_style_context_add_class (gtk_widget_get_style_context (priv->message_label), + "dim-label"); + gtk_box_pack_start (GTK_BOX (preview), priv->message_label, TRUE, TRUE, 0); + + gtk_container_set_border_width (GTK_CONTAINER (preview), 4); + gtk_widget_show_all (GTK_WIDGET (preview)); + g_signal_connect (preview, "size-allocate", + G_CALLBACK (on_size_allocate), NULL); +} + +static void +nemo_preview_image_class_init (NemoPreviewImageClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = nemo_preview_image_finalize; +} + +GtkWidget * +nemo_preview_image_new (void) +{ + return g_object_new (NEMO_TYPE_PREVIEW_IMAGE, + "orientation", GTK_ORIENTATION_VERTICAL, + "spacing", 0, + NULL); +} + +static gboolean +on_drawing_area_draw (GtkWidget *widget, + cairo_t *cr, + gpointer user_data) +{ + NemoPreviewImage *preview = NEMO_PREVIEW_IMAGE (user_data); + NemoPreviewImagePrivate *priv; + gint widget_width, widget_height; + gdouble scale_x, scale_y, scale; + gdouble scaled_width, scaled_height; + gdouble x_offset, y_offset; + + priv = nemo_preview_image_get_instance_private (preview); + + if (priv->current_surface == NULL) { + return FALSE; + } + + widget_width = gtk_widget_get_allocated_width (widget); + widget_height = gtk_widget_get_allocated_height (widget); + + scale_x = (gdouble)widget_width / priv->surface_width; + scale_y = (gdouble)widget_height / priv->surface_height; + scale = MIN (scale_x, scale_y); + + scaled_width = priv->surface_width * scale; + scaled_height = priv->surface_height * scale; + + x_offset = (widget_width - scaled_width) / 2.0; + y_offset = (widget_height - scaled_height) / 2.0; + + cairo_save (cr); + cairo_translate (cr, x_offset, y_offset); + cairo_scale (cr, scale, scale); + cairo_set_source_surface (cr, priv->current_surface, 0, 0); + cairo_paint (cr); + cairo_restore (cr); + + /* If showing an image (not an icon), draw a border on top of it */ + if (!priv->showing_icon) { + GtkStyleContext *style_context; + GdkRGBA border_color; + gdouble border_width = 1.0; + gdouble inset = 0.5; + + style_context = gtk_widget_get_style_context (priv->frame); + gtk_style_context_get_border_color (style_context, GTK_STATE_FLAG_NORMAL, &border_color); + + cairo_set_source_rgba (cr, border_color.red, border_color.green, + border_color.blue, border_color.alpha); + cairo_set_line_width (cr, border_width); + cairo_rectangle (cr, x_offset + inset, y_offset + inset, + scaled_width - (inset * 2), scaled_height - (inset * 2)); + cairo_stroke (cr); + } + + return TRUE; +} + +static cairo_surface_t * +create_surface_from_pixbuf (GdkPixbuf *pixbuf, + gint scale_factor) +{ + cairo_surface_t *surface; + cairo_t *cr; + gint width, height; + + g_return_val_if_fail (pixbuf != NULL, NULL); + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cairo_surface_set_device_scale (surface, scale_factor, scale_factor); + + cr = cairo_create (surface); + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + cairo_paint (cr); + cairo_destroy (cr); + + return surface; +} + +static void +load_image_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + LoadImageData *data = task_data; + GdkPixbuf *pixbuf = NULL; + GdkPixbuf *oriented_pixbuf = NULL; + cairo_surface_t *surface = NULL; + GError *error = NULL; + + if (data->file_path != NULL) { + pixbuf = gdk_pixbuf_new_from_file_at_scale (data->file_path, + data->width * data->ui_scale, + data->height * data->ui_scale, + TRUE, + &error); + if (error != NULL) { + g_clear_error (&error); + } + } + + if (g_cancellable_is_cancelled (cancellable)) { + if (pixbuf != NULL) { + g_object_unref (pixbuf); + } + g_task_return_error_if_cancelled (task); + return; + } + + if (pixbuf == NULL && data->thumbnail_path != NULL) { + pixbuf = gdk_pixbuf_new_from_file_at_scale (data->thumbnail_path, + data->width * data->ui_scale, + data->height * data->ui_scale, + TRUE, + &error); + if (error != NULL) { + g_clear_error (&error); + } + } + + if (pixbuf != NULL && !g_cancellable_is_cancelled (cancellable)) { + oriented_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf); + g_object_unref (pixbuf); + pixbuf = oriented_pixbuf; + } + + if (pixbuf != NULL && !g_cancellable_is_cancelled (cancellable)) { + surface = create_surface_from_pixbuf (pixbuf, data->ui_scale); + g_object_unref (pixbuf); + } + + if (!g_cancellable_is_cancelled (cancellable)) { + g_task_return_pointer (task, surface, surface ? (GDestroyNotify)cairo_surface_destroy : NULL); + } else { + if (surface != NULL) { + cairo_surface_destroy (surface); + } + g_task_return_error_if_cancelled (task); + } +} + +static void +load_image_callback (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + NemoPreviewImage *widget = NEMO_PREVIEW_IMAGE (source_object); + NemoPreviewImagePrivate *priv; + GTask *task; + LoadImageData *data; + cairo_surface_t *surface; + gint scale_factor; + GError *error = NULL; + + priv = nemo_preview_image_get_instance_private (widget); + task = G_TASK (result); + data = g_task_get_task_data (task); + + if (priv->current_load_data == data) { + priv->current_load_data = NULL; + } + + if (g_cancellable_is_cancelled (data->cancellable)) + return; + + if (error != NULL) { + g_error_free (error); + return; + } + + surface = g_task_propagate_pointer (task, &error); + + if (surface != NULL) { + if (priv->current_surface != NULL) { + cairo_surface_destroy (priv->current_surface); + } + priv->current_surface = surface; + + scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (widget)); + priv->surface_width = cairo_image_surface_get_width (surface) / scale_factor; + priv->surface_height = cairo_image_surface_get_height (surface) / scale_factor; + + gtk_widget_show (priv->drawing_area); + gtk_widget_queue_draw (priv->drawing_area); + gtk_widget_hide (priv->message_label); + } else { + gtk_label_set_text (GTK_LABEL (priv->message_label), + _("(Failed to load image)")); + gtk_widget_show (priv->message_label); + gtk_widget_hide (priv->drawing_area); + } +} + +static void +load_icon_at_size (NemoPreviewImage *widget, + gint width, + gint height) +{ + NemoPreviewImagePrivate *priv; + NemoIconInfo *icon_info = NULL; + GtkIconTheme *icon_theme; + GdkPixbuf *icon_pixbuf = NULL; + cairo_surface_t *surface = NULL; + gint ui_scale; + gint icon_size; + const char *icon_name; + GError *error = NULL; + + priv = nemo_preview_image_get_instance_private (widget); + + if (priv->file == NULL) { + return; + } + + if (width <= 1 || height <= 1) { + return; + } + + ui_scale = gtk_widget_get_scale_factor (GTK_WIDGET (widget)); + + icon_size = MIN (width, height) * ui_scale; + icon_info = nemo_file_get_icon (priv->file, icon_size, 0, ui_scale, 0); + + if (icon_info != NULL && icon_info->icon_name != NULL) { + icon_name = icon_info->icon_name; + icon_theme = gtk_icon_theme_get_default (); + + icon_pixbuf = gtk_icon_theme_load_icon_for_scale (icon_theme, + icon_name, + icon_size / ui_scale, + ui_scale, + GTK_ICON_LOOKUP_FORCE_SIZE, + &error); + + if (icon_pixbuf != NULL) { + surface = create_surface_from_pixbuf (icon_pixbuf, ui_scale); + + if (surface != NULL) { + if (priv->current_surface != NULL) { + cairo_surface_destroy (priv->current_surface); + } + priv->current_surface = surface; + + priv->surface_width = cairo_image_surface_get_width (surface) / ui_scale; + priv->surface_height = cairo_image_surface_get_height (surface) / ui_scale; + + gtk_widget_show (priv->drawing_area); + gtk_widget_queue_draw (priv->drawing_area); + } + + g_object_unref (icon_pixbuf); + gtk_widget_hide (priv->message_label); + } else { + if (error != NULL) { + g_warning ("Failed to load icon '%s': %s", icon_name, error->message); + g_error_free (error); + } + gtk_label_set_text (GTK_LABEL (priv->message_label), + _("(Failed to load icon)")); + gtk_widget_show (priv->message_label); + gtk_widget_hide (priv->drawing_area); + } + + nemo_icon_info_unref (icon_info); + } else { + if (icon_info != NULL) { + nemo_icon_info_unref (icon_info); + } + gtk_label_set_text (GTK_LABEL (priv->message_label), + _("(No icon available)")); + gtk_widget_show (priv->message_label); + gtk_widget_hide (priv->drawing_area); + } + + priv->current_width = width; + priv->current_height = height; +} + +static void +load_image_at_size (NemoPreviewImage *widget, + gint width, + gint height) +{ + NemoPreviewImagePrivate *priv; + GTask *task; + LoadImageData *data; + gchar *file_path = NULL; + gchar *thumbnail_path = NULL; + gint ui_scale; + + priv = nemo_preview_image_get_instance_private (widget); + + if (priv->file == NULL) { + return; + } + + if (width <= 1 || height <= 1) { + return; + } + + if (priv->current_load_data != NULL) { + g_cancellable_cancel (priv->current_load_data->cancellable); + priv->current_load_data = NULL; /* Forget about it, callback will clean up */ + } + + if (nemo_can_thumbnail_internally (priv->file)) { + file_path = nemo_file_get_path (priv->file); + } + + if (nemo_file_has_loaded_thumbnail (priv->file)) { + thumbnail_path = nemo_file_get_thumbnail_path (priv->file); + } + + if (file_path == NULL && thumbnail_path == NULL) { + gtk_label_set_text (GTK_LABEL (priv->message_label), + _("(No preview available)")); + gtk_widget_show (priv->message_label); + gtk_widget_hide (priv->drawing_area); + return; + } + + ui_scale = gtk_widget_get_scale_factor (GTK_WIDGET (widget)); + + data = load_image_data_new (file_path, thumbnail_path, width, height, ui_scale); + g_free (file_path); + g_free (thumbnail_path); + + priv->current_load_data = data; + + task = g_task_new (widget, data->cancellable, load_image_callback, NULL); + g_task_set_task_data (task, data, (GDestroyNotify) load_image_data_free); + g_task_run_in_thread (task, load_image_thread); + g_object_unref (task); + + priv->current_width = width; + priv->current_height = height; +} + +static void +reload_at_size (NemoPreviewImage *widget, + gint width, + gint height) +{ + NemoPreviewImagePrivate *priv; + + priv = nemo_preview_image_get_instance_private (widget); + + if (priv->file == NULL) { + return; + } + + if (nemo_can_thumbnail_internally (priv->file) || nemo_file_has_loaded_thumbnail (priv->file)) { + priv->showing_icon = FALSE; + load_image_at_size (widget, width, height); + } else { + priv->showing_icon = TRUE; + load_icon_at_size (widget, width, height); + } +} + +static gboolean +on_resize_timeout (gpointer user_data) +{ + NemoPreviewImage *widget = NEMO_PREVIEW_IMAGE (user_data); + NemoPreviewImagePrivate *priv; + GtkAllocation allocation; + + priv = nemo_preview_image_get_instance_private (widget); + priv->resize_timeout_id = 0; + + gtk_widget_get_allocation (GTK_WIDGET (widget), &allocation); + reload_at_size (widget, allocation.width, allocation.height); + + return G_SOURCE_REMOVE; +} + +static void +on_size_allocate (GtkWidget *widget, + GtkAllocation *allocation, + gpointer user_data) +{ + NemoPreviewImage *preview = NEMO_PREVIEW_IMAGE (widget); + NemoPreviewImagePrivate *priv; + + priv = nemo_preview_image_get_instance_private (preview); + + if (priv->current_surface != NULL) { + gtk_widget_queue_draw (priv->drawing_area); + } + + if (priv->resize_timeout_id != 0) { + g_source_remove (priv->resize_timeout_id); + } + + priv->resize_timeout_id = g_timeout_add (RESIZE_DEBOUNCE_MS, + on_resize_timeout, + preview); +} + +void +nemo_preview_image_set_file (NemoPreviewImage *widget, + NemoFile *file) +{ + NemoPreviewImagePrivate *priv; + GtkAllocation allocation; + + g_return_if_fail (NEMO_IS_PREVIEW_IMAGE (widget)); + + priv = nemo_preview_image_get_instance_private (widget); + + if (priv->file == file) { + return; + } + + if (priv->current_load_data != NULL) { + g_cancellable_cancel (priv->current_load_data->cancellable); + priv->current_load_data = NULL; + } + + if (priv->resize_timeout_id != 0) { + g_source_remove (priv->resize_timeout_id); + priv->resize_timeout_id = 0; + } + + if (priv->file != NULL) { + nemo_file_unref (priv->file); + } + + priv->file = file; + + if (priv->current_surface != NULL) { + cairo_surface_destroy (priv->current_surface); + priv->current_surface = NULL; + } + gtk_widget_hide (priv->message_label); + gtk_widget_hide (priv->drawing_area); + priv->current_width = 0; + priv->current_height = 0; + priv->surface_width = 0; + priv->surface_height = 0; + + if (file != NULL) { + nemo_file_ref (file); + gtk_widget_get_allocation (GTK_WIDGET (widget), &allocation); + reload_at_size (widget, allocation.width, allocation.height); + } +} + +void +nemo_preview_image_clear (NemoPreviewImage *widget) +{ + NemoPreviewImagePrivate *priv; + + g_return_if_fail (NEMO_IS_PREVIEW_IMAGE (widget)); + + priv = nemo_preview_image_get_instance_private (widget); + + if (priv->current_load_data != NULL) { + g_cancellable_cancel (priv->current_load_data->cancellable); + priv->current_load_data = NULL; + } + + if (priv->resize_timeout_id != 0) { + g_source_remove (priv->resize_timeout_id); + priv->resize_timeout_id = 0; + } + + if (priv->current_surface != NULL) { + cairo_surface_destroy (priv->current_surface); + priv->current_surface = NULL; + } + + if (priv->file != NULL) { + nemo_file_unref (priv->file); + priv->file = NULL; + } + + gtk_widget_hide (priv->drawing_area); + gtk_widget_hide (priv->message_label); + priv->current_width = 0; + priv->current_height = 0; + priv->surface_width = 0; + priv->surface_height = 0; + priv->showing_icon = FALSE; +} diff --git a/libnemo-private/nemo-preview-image.h b/libnemo-private/nemo-preview-image.h new file mode 100644 index 000000000..d346c02f2 --- /dev/null +++ b/libnemo-private/nemo-preview-image.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* + * nemo-preview-image.h - Widget for displaying image preview in preview pane + * + * Copyright (C) 2025 Linux Mint + * + * Nemo 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 2 of the + * License, or (at your option) any later version. + * + * Nemo 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street - Suite 500, + * Boston, MA 02110-1335, USA. + */ + +#ifndef NEMO_PREVIEW_IMAGE_H +#define NEMO_PREVIEW_IMAGE_H + +#include +#include "nemo-file.h" + +#define NEMO_TYPE_PREVIEW_IMAGE (nemo_preview_image_get_type()) + +G_DECLARE_FINAL_TYPE (NemoPreviewImage, nemo_preview_image, NEMO, PREVIEW_IMAGE, GtkBox) +GtkWidget *nemo_preview_image_new (void); + +void nemo_preview_image_set_file (NemoPreviewImage *widget, + NemoFile *file); +void nemo_preview_image_clear (NemoPreviewImage *widget); + +#endif /* NEMO_PREVIEW_IMAGE_H */ diff --git a/libnemo-private/org.nemo.gschema.xml b/libnemo-private/org.nemo.gschema.xml index a43065d35..39265db27 100644 --- a/libnemo-private/org.nemo.gschema.xml +++ b/libnemo-private/org.nemo.gschema.xml @@ -76,6 +76,7 @@ + @@ -273,6 +274,11 @@ Show favorites first in windows If set to true, then Nemo shows favorites prior to other files in the icon and list views. + + false + Show the preview pane by default in newly visited folders + If set to true, then Nemo shows the preview pane when visiting new folders. + @@ -895,4 +901,17 @@ List of search helper filenames to skip when using content search. + + + + 400 + Width of the preview pane + The width of the preview pane in logical pixels. + + + 200 + Height of the details pane within the preview pane + The height of the NemoPreviewDetails pane in logical pixels. This is the lower part of the vertically-split preview pane showing file metadata. + + diff --git a/src/meson.build b/src/meson.build index 07a56957d..d044f4aea 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,6 +53,7 @@ nemoCommon_sources = [ 'nemo-places-sidebar.c', 'nemo-plugin-manager.c', 'nemo-previewer.c', + 'nemo-preview-pane.c', 'nemo-progress-info-widget.c', 'nemo-progress-ui-handler.c', 'nemo-properties-window.c', diff --git a/src/nemo-actions.h b/src/nemo-actions.h index 0ec1c0722..cd9a87680 100644 --- a/src/nemo-actions.h +++ b/src/nemo-actions.h @@ -46,6 +46,7 @@ #define NEMO_ACTION_SHOW_HIDE_MENUBAR "Show Hide Menubar" #define NEMO_ACTION_SHOW_HIDE_LOCATION_BAR "Show Hide Location Bar" #define NEMO_ACTION_SHOW_HIDE_EXTRA_PANE "Show Hide Extra Pane" +#define NEMO_ACTION_SHOW_HIDE_PREVIEW_PANE "Show Hide Preview Pane" #define NEMO_ACTION_GO_TO_BURN_CD "Go to Burn CD" #define NEMO_ACTION_EDIT_LOCATION "Edit Location" #define NEMO_ACTION_COMPACT_VIEW "CompactView" diff --git a/src/nemo-file-management-properties.c b/src/nemo-file-management-properties.c index d0a1958e1..c26c272b8 100644 --- a/src/nemo-file-management-properties.c +++ b/src/nemo-file-management-properties.c @@ -91,6 +91,7 @@ #define NEMO_FILE_MANAGEMENT_PROPERTIES_SHOW_SHOW_THUMBNAILS_ICON_TOOLBAR_WIDGET "show_show_thumbnails_icon_toolbar_togglebutton" #define NEMO_FILE_MANAGEMENT_PROPERTIES_SHOW_TOGGLE_EXTRA_PANE_ICON_TOOLBAR_WIDGET "show_toggle_extra_pane_icon_toolbar_togglebutton" +#define NEMO_FILE_MANAGEMENT_PROPERTIES_SHOW_PREVIEW_PANE_WIDGET "show_preview_pane_checkbutton" #define NEMO_FILE_MANAGEMENT_PROPERTIES_SHOW_FULL_PATH_IN_TITLE_BARS_WIDGET "show_full_path_in_title_bars_checkbutton" #define NEMO_FILE_MANAGEMENT_PROPERTIES_CLOSE_DEVICE_VIEW_ON_EJECT_WIDGET "close_device_view_on_eject_checkbutton" #define NEMO_FILE_MANAGEMENT_PROPERTIES_AUTOMOUNT_MEDIA_WIDGET "media_automount_checkbutton" @@ -931,6 +932,10 @@ nemo_file_management_properties_dialog_setup (GtkBuilder *builder, NEMO_FILE_MANAGEMENT_PROPERTIES_SHOW_SHOW_THUMBNAILS_ICON_TOOLBAR_WIDGET, NEMO_PREFERENCES_SHOW_SHOW_THUMBNAILS_TOOLBAR); + bind_builder_bool (builder, nemo_preferences, + NEMO_FILE_MANAGEMENT_PROPERTIES_SHOW_PREVIEW_PANE_WIDGET, + NEMO_PREFERENCES_SHOW_PREVIEW_PANE); + /* setup preferences */ bind_builder_bool (builder, nemo_icon_view_preferences, NEMO_FILE_MANAGEMENT_PROPERTIES_LABELS_BESIDE_ICONS_WIDGET, diff --git a/src/nemo-preview-pane.c b/src/nemo-preview-pane.c new file mode 100644 index 000000000..887934e51 --- /dev/null +++ b/src/nemo-preview-pane.c @@ -0,0 +1,291 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* + * nemo-preview-pane.c - Container widget for preview pane + * + * Copyright (C) 2025 Linux Mint + * + * Nemo 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 2 of the + * License, or (at your option) any later version. + * + * Nemo 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street - Suite 500, + * Boston, MA 02110-1335, USA. + */ + +#include "nemo-preview-pane.h" +#include +#include +#include +#include + +#define PREVIEW_IMAGE_HEIGHT 200 + +struct _NemoPreviewPane { + GtkBox parent; +}; + +typedef struct { + NemoWindow *window; + + GtkWidget *vpaned; + GtkWidget *image_widget; + GtkWidget *details_widget; + GtkWidget *empty_label; + + NemoFile *current_file; + gulong file_changed_id; + + gboolean initial_position_set; +} NemoPreviewPanePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (NemoPreviewPane, nemo_preview_pane, GTK_TYPE_BOX) + +static void +vpaned_size_allocate_callback (GtkWidget *widget, GtkAllocation *allocation, gpointer user_data) +{ + NemoPreviewPane *pane = NEMO_PREVIEW_PANE (user_data); + NemoPreviewPanePrivate *priv = nemo_preview_pane_get_instance_private (pane); + gint saved_height, position; + + /* Only set initial position once */ + if (priv->initial_position_set) { + return; + } + + priv->initial_position_set = TRUE; + + /* Set position based on saved details height */ + saved_height = g_settings_get_int (nemo_preview_pane_preferences, "details-height"); + if (saved_height > 50 && allocation->height > saved_height) { + /* Position is from top, so subtract details height from total */ + position = allocation->height - saved_height; + gtk_paned_set_position (GTK_PANED (widget), position); + } else { + /* Fallback: make image section PREVIEW_IMAGE_HEIGHT */ + gtk_paned_set_position (GTK_PANED (widget), PREVIEW_IMAGE_HEIGHT); + } +} + +static void +details_pane_position_changed_callback (GObject *paned, GParamSpec *pspec, gpointer user_data) +{ + NemoPreviewPane *pane = NEMO_PREVIEW_PANE (user_data); + NemoPreviewPanePrivate *priv = nemo_preview_pane_get_instance_private (pane); + gint position, total_height, details_height; + + /* Don't save position until initial position has been set */ + if (!priv->initial_position_set) { + return; + } + + position = gtk_paned_get_position (GTK_PANED (paned)); + total_height = gtk_widget_get_allocated_height (GTK_WIDGET (paned)); + + /* Calculate height of details pane (bottom side) */ + details_height = total_height - position; + + /* Only save if details height is reasonable */ + if (details_height > 50 && total_height > 0) { + g_settings_set_int (nemo_preview_pane_preferences, "details-height", details_height); + } +} + +static void +file_changed_callback (NemoFile *file, gpointer user_data) +{ + NemoPreviewPane *pane; + NemoPreviewPanePrivate *priv; + + pane = NEMO_PREVIEW_PANE (user_data); + priv = nemo_preview_pane_get_instance_private (pane); + + /* Refresh the preview if the file has changed */ + if (file == priv->current_file) { + nemo_preview_pane_set_file (pane, file); + } +} + +static void +nemo_preview_pane_finalize (GObject *object) +{ + NemoPreviewPane *pane; + NemoPreviewPanePrivate *priv; + + pane = NEMO_PREVIEW_PANE (object); + priv = nemo_preview_pane_get_instance_private (pane); + + if (priv->current_file != NULL) { + if (priv->file_changed_id != 0) { + g_signal_handler_disconnect (priv->current_file, + priv->file_changed_id); + priv->file_changed_id = 0; + } + nemo_file_unref (priv->current_file); + priv->current_file = NULL; + } + + G_OBJECT_CLASS (nemo_preview_pane_parent_class)->finalize (object); +} + +static void +nemo_preview_pane_init (NemoPreviewPane *pane) +{ + NemoPreviewPanePrivate *priv; + GtkWidget *scrolled; + + priv = nemo_preview_pane_get_instance_private (pane); + + /* Create empty state label */ + priv->empty_label = gtk_label_new (_("No file selected")); + gtk_widget_set_halign (priv->empty_label, GTK_ALIGN_CENTER); + gtk_widget_set_valign (priv->empty_label, GTK_ALIGN_CENTER); + gtk_style_context_add_class (gtk_widget_get_style_context (priv->empty_label), + "dim-label"); + gtk_box_pack_start (GTK_BOX (pane), priv->empty_label, TRUE, TRUE, 0); + gtk_widget_show (priv->empty_label); + + /* Create vertical paned widget */ + priv->vpaned = gtk_paned_new (GTK_ORIENTATION_VERTICAL); + gtk_box_pack_start (GTK_BOX (pane), priv->vpaned, TRUE, TRUE, 0); + + /* Create image preview widget (top) */ + priv->image_widget = nemo_preview_image_new (); + gtk_paned_pack1 (GTK_PANED (priv->vpaned), + priv->image_widget, FALSE, FALSE); + gtk_widget_show (priv->image_widget); + + /* Create details widget (bottom) in a scrolled window */ + scrolled = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + + priv->details_widget = nemo_preview_details_new (); + gtk_container_add (GTK_CONTAINER (scrolled), priv->details_widget); + gtk_widget_show (priv->details_widget); + + gtk_paned_pack2 (GTK_PANED (priv->vpaned), + scrolled, TRUE, FALSE); + gtk_widget_show (scrolled); + + /* Initialize flag */ + priv->initial_position_set = FALSE; + + /* Connect size-allocate to set initial position from saved settings */ + g_signal_connect (priv->vpaned, "size-allocate", + G_CALLBACK (vpaned_size_allocate_callback), + pane); + + /* Connect signal to save position on resize */ + g_signal_connect (priv->vpaned, "notify::position", + G_CALLBACK (details_pane_position_changed_callback), + pane); +} + +static void +nemo_preview_pane_class_init (NemoPreviewPaneClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = nemo_preview_pane_finalize; +} + +GtkWidget * +nemo_preview_pane_new (NemoWindow *window) +{ + NemoPreviewPane *pane; + NemoPreviewPanePrivate *priv; + + pane = g_object_new (NEMO_TYPE_PREVIEW_PANE, + "orientation", GTK_ORIENTATION_VERTICAL, + "spacing", 0, + NULL); + + priv = nemo_preview_pane_get_instance_private (pane); + priv->window = window; + + return GTK_WIDGET (pane); +} + +void +nemo_preview_pane_set_file (NemoPreviewPane *pane, + NemoFile *file) +{ + NemoPreviewPanePrivate *priv; + + g_return_if_fail (NEMO_IS_PREVIEW_PANE (pane)); + + priv = nemo_preview_pane_get_instance_private (pane); + + /* Disconnect from previous file if necessary */ + if (priv->current_file != NULL) { + if (priv->file_changed_id != 0) { + g_signal_handler_disconnect (priv->current_file, + priv->file_changed_id); + priv->file_changed_id = 0; + } + nemo_file_unref (priv->current_file); + priv->current_file = NULL; + } + + priv->current_file = file; + + if (file != NULL) { + nemo_file_ref (file); + + /* Monitor file for changes */ + priv->file_changed_id = + g_signal_connect (file, "changed", + G_CALLBACK (file_changed_callback), + pane); + + /* Update child widgets */ + nemo_preview_image_set_file (NEMO_PREVIEW_IMAGE (priv->image_widget), file); + nemo_preview_details_set_file (NEMO_PREVIEW_DETAILS (priv->details_widget), file); + + /* Show preview, hide empty label */ + gtk_widget_hide (priv->empty_label); + gtk_widget_show (priv->vpaned); + } else { + /* No file selected - show empty state */ + nemo_preview_pane_clear (pane); + } +} + +void +nemo_preview_pane_clear (NemoPreviewPane *pane) +{ + NemoPreviewPanePrivate *priv; + + g_return_if_fail (NEMO_IS_PREVIEW_PANE (pane)); + + priv = nemo_preview_pane_get_instance_private (pane); + + if (priv->current_file != NULL) { + if (priv->file_changed_id != 0) { + g_signal_handler_disconnect (priv->current_file, + priv->file_changed_id); + priv->file_changed_id = 0; + } + nemo_file_unref (priv->current_file); + priv->current_file = NULL; + } + + /* Clear child widgets */ + nemo_preview_image_clear (NEMO_PREVIEW_IMAGE (priv->image_widget)); + nemo_preview_details_clear (NEMO_PREVIEW_DETAILS (priv->details_widget)); + + /* Hide preview, show empty label */ + gtk_widget_hide (priv->vpaned); + gtk_widget_show (priv->empty_label); +} diff --git a/src/nemo-preview-pane.h b/src/nemo-preview-pane.h new file mode 100644 index 000000000..4a08c3283 --- /dev/null +++ b/src/nemo-preview-pane.h @@ -0,0 +1,42 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* + * nemo-preview-pane.h - Container widget for preview pane + * + * Copyright (C) 2025 Linux Mint + * + * Nemo 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 2 of the + * License, or (at your option) any later version. + * + * Nemo 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; see the file COPYING. If not, + * write to the Free Software Foundation, Inc., 51 Franklin Street - Suite 500, + * Boston, MA 02110-1335, USA. + */ + +#ifndef NEMO_PREVIEW_PANE_H +#define NEMO_PREVIEW_PANE_H + +#include +#include + +/* Forward declaration */ +typedef struct NemoWindow NemoWindow; + +#define NEMO_TYPE_PREVIEW_PANE (nemo_preview_pane_get_type()) + +G_DECLARE_FINAL_TYPE (NemoPreviewPane, nemo_preview_pane, NEMO, PREVIEW_PANE, GtkBox); +GtkWidget *nemo_preview_pane_new (NemoWindow *window); + +void nemo_preview_pane_set_file (NemoPreviewPane *pane, + NemoFile *file); +void nemo_preview_pane_clear (NemoPreviewPane *pane); + +#endif /* NEMO_PREVIEW_PANE_H */ diff --git a/src/nemo-statusbar.c b/src/nemo-statusbar.c index 40ff78bc3..09a98167a 100644 --- a/src/nemo-statusbar.c +++ b/src/nemo-statusbar.c @@ -90,6 +90,34 @@ nemo_status_bar_dispose (GObject *object) G_OBJECT_CLASS (nemo_status_bar_parent_class)->dispose (object); } +static void +action_toggle_split_view_callback (GtkButton *button, NemoStatusBar *bar) +{ + NemoWindow *window = bar->window; + + if (!nemo_window_split_view_showing (window)) { + nemo_window_split_view_on (window); + } else { + nemo_window_split_view_off (window); + } + + nemo_status_bar_sync_button_states (bar); +} + +static void +action_toggle_preview_pane_callback (GtkButton *button, NemoStatusBar *bar) +{ + NemoWindow *window = bar->window; + + if (!nemo_window_preview_pane_showing (window)) { + nemo_window_preview_pane_on (window); + } else { + nemo_window_preview_pane_off (window); + } + + nemo_status_bar_sync_button_states (bar); +} + static void action_places_toggle_callback (GtkButton *button, NemoStatusBar *bar) { @@ -130,6 +158,18 @@ sidebar_type_changed_cb (gpointer pointer, const gchar *sidebar_id, gpointer use nemo_status_bar_sync_button_states (NEMO_STATUS_BAR (user_data)); } +static void +preview_pane_state_changed_cb (gpointer pointer, GParamSpec *pspec, gpointer user_data) +{ + nemo_status_bar_sync_button_states (NEMO_STATUS_BAR (user_data)); +} + +static void +split_view_state_changed_cb (gpointer pointer, GParamSpec *pspec, gpointer user_data) +{ + nemo_status_bar_sync_button_states (NEMO_STATUS_BAR (user_data)); +} + static void on_slider_changed_cb (GtkWidget *zoom_slider, gpointer user_data) { @@ -229,6 +269,24 @@ nemo_status_bar_constructed (GObject *object) gtk_range_set_increments (GTK_RANGE (zoom_slider), 1.0, 1.0); gtk_range_set_round_digits (GTK_RANGE (zoom_slider), 0); + button = gtk_toggle_button_new (); + icon = gtk_image_new_from_icon_name ("view-dual-symbolic", size); + gtk_button_set_image (GTK_BUTTON (button), icon); + gtk_widget_set_tooltip_text (GTK_WIDGET (button), _("Show Split View (F3)")); + bar->split_view_button = button; + gtk_box_pack_start (GTK_BOX (bar), button, FALSE, FALSE, 2); + g_signal_connect (GTK_BUTTON (button), "clicked", + G_CALLBACK (action_toggle_split_view_callback), bar); + + button = gtk_toggle_button_new (); + icon = gtk_image_new_from_icon_name ("xsi-preview-symbolic", size); + gtk_button_set_image (GTK_BUTTON (button), icon); + gtk_widget_set_tooltip_text (GTK_WIDGET (button), _("Show the Preview pane (F7)")); + bar->preview_pane_button = button; + gtk_box_pack_start (GTK_BOX (bar), button, FALSE, FALSE, 2); + g_signal_connect (GTK_BUTTON (button), "clicked", + G_CALLBACK (action_toggle_preview_pane_callback), bar); + gtk_widget_show_all (GTK_WIDGET (bar)); g_signal_connect_object (NEMO_WINDOW (bar->window), "notify::show-sidebar", @@ -237,6 +295,12 @@ nemo_status_bar_constructed (GObject *object) g_signal_connect_object (NEMO_WINDOW (bar->window), "notify::sidebar-view-id", G_CALLBACK (sidebar_type_changed_cb), bar, G_CONNECT_AFTER); + g_signal_connect_object (NEMO_WINDOW (bar->window), "notify::show-preview-pane", + G_CALLBACK (preview_pane_state_changed_cb), bar, G_CONNECT_AFTER); + + g_signal_connect_object (NEMO_WINDOW (bar->window), "notify::show-split-view", + G_CALLBACK (split_view_state_changed_cb), bar, G_CONNECT_AFTER); + g_signal_connect (GTK_RANGE (zoom_slider), "value-changed", G_CALLBACK (on_slider_changed_cb), bar); @@ -332,6 +396,23 @@ nemo_status_bar_sync_button_states (NemoStatusBar *bar) gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (bar->places_button), FALSE); } g_signal_handlers_unblock_by_func (GTK_BUTTON (bar->places_button), action_places_toggle_callback, bar); + + g_signal_handlers_block_by_func (GTK_BUTTON (bar->split_view_button), action_toggle_split_view_callback, bar); + if (nemo_window_split_view_showing (bar->window)) { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (bar->split_view_button), TRUE); + } else { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (bar->split_view_button), FALSE); + } + g_signal_handlers_unblock_by_func (GTK_BUTTON (bar->split_view_button), action_toggle_split_view_callback, bar); + + g_signal_handlers_block_by_func (GTK_BUTTON (bar->preview_pane_button), action_toggle_preview_pane_callback, bar); + if (nemo_window_preview_pane_showing (bar->window)) { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (bar->preview_pane_button), TRUE); + } else { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (bar->preview_pane_button), FALSE); + } + g_signal_handlers_unblock_by_func (GTK_BUTTON (bar->preview_pane_button), action_toggle_preview_pane_callback, bar); + } void diff --git a/src/nemo-statusbar.h b/src/nemo-statusbar.h index 5b8af4d02..8e2382843 100644 --- a/src/nemo-statusbar.h +++ b/src/nemo-statusbar.h @@ -54,6 +54,8 @@ struct _NemoStatusBar GtkWidget *show_button; GtkWidget *hide_button; GtkWidget *separator; + GtkWidget *split_view_button; + GtkWidget *preview_pane_button; }; struct _NemoStatusBarClass diff --git a/src/nemo-view.c b/src/nemo-view.c index c211ee5bf..3377e2638 100644 --- a/src/nemo-view.c +++ b/src/nemo-view.c @@ -528,6 +528,7 @@ nemo_view_reset_to_defaults (NemoView *view) file = view->details->slot->viewed_file; nemo_file_set_metadata(file, NEMO_METADATA_KEY_SHOW_THUMBNAILS, NULL, NULL); nemo_file_set_metadata(file, NEMO_METADATA_KEY_DEFAULT_VIEW, NULL, NULL); + nemo_file_set_metadata (file, NEMO_METADATA_KEY_WINDOW_SHOW_PREVIEW_PANE, NULL, NULL); gtk_action_activate (gtk_action_group_get_action (nemo_window_get_main_action_group (window), NEMO_ACTION_RELOAD)); } diff --git a/src/nemo-window-menus.c b/src/nemo-window-menus.c index 46f10b8e6..5d01b7e23 100644 --- a/src/nemo-window-menus.c +++ b/src/nemo-window-menus.c @@ -658,8 +658,36 @@ action_split_view_callback (GtkAction *action, nemo_view_update_menus (slot->content_view); } } +} + +static void +action_preview_pane_callback (GtkAction *action, + gpointer user_data) +{ + NemoWindow *window; + gboolean is_active; + + if (NEMO_IS_DESKTOP_WINDOW (user_data)) { + return; + } - nemo_window_update_show_hide_ui_elements (window); + window = NEMO_WINDOW (user_data); + + is_active = gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action)); + if (is_active != nemo_window_preview_pane_showing (window)) { + NemoWindowSlot *slot; + + if (is_active) { + nemo_window_preview_pane_on (window); + } else { + nemo_window_preview_pane_off (window); + } + + slot = nemo_window_get_active_slot (window); + if (slot != NULL && slot->content_view != NULL) { + nemo_view_update_menus (slot->content_view); + } + } } static void @@ -784,18 +812,33 @@ nemo_window_update_show_hide_ui_elements (NemoWindow *window) NemoWindowPane *pane; GtkActionGroup *action_group; GtkAction *action; + gboolean active, split_view_showing, preview_showing; + split_view_showing = nemo_window_split_view_showing (window); + preview_showing = nemo_window_preview_pane_showing (window); action_group = nemo_window_get_main_action_group (window); action = gtk_action_group_get_action (action_group, NEMO_ACTION_SHOW_HIDE_EXTRA_PANE); - gtk_action_block_activate (action); - gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), - nemo_window_split_view_showing (window)); - gtk_action_unblock_activate (action); + active = gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action)); - nemo_window_update_split_view_actions_sensitivity (window); + if (active != split_view_showing) { + gtk_action_block_activate (action); + gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), split_view_showing); + gtk_action_unblock_activate (action); + } + + action = gtk_action_group_get_action (action_group, + NEMO_ACTION_SHOW_HIDE_PREVIEW_PANE); + active = gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action)); + + if (active != preview_showing) { + gtk_action_block_activate (action); + gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), preview_showing); + gtk_action_unblock_activate (action); + } + nemo_window_update_split_view_actions_sensitivity (window); update_side_bar_radio_buttons (window); pane = nemo_window_get_active_pane (window); @@ -804,10 +847,13 @@ nemo_window_update_show_hide_ui_elements (NemoWindow *window) action = gtk_action_group_get_action (action_group, NEMO_ACTION_SHOW_HIDE_EXTRA_PANE); - gtk_action_block_activate (action); - gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), - nemo_window_split_view_showing (window)); - gtk_action_unblock_activate (action); + active = gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action)); + + if (active != split_view_showing) { + gtk_action_block_activate (action); + gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action), split_view_showing); + gtk_action_unblock_activate (action); + } } } @@ -1573,6 +1619,11 @@ static const GtkToggleActionEntry main_toggle_entries[] = { /* label, accelerator */ N_("E_xtra Pane"), "F3", /* tooltip */ N_("Open an extra folder view side-by-side"), G_CALLBACK (action_split_view_callback), + /* is_active */ FALSE }, + /* name, stock id */ { NEMO_ACTION_SHOW_HIDE_PREVIEW_PANE, NULL, + /* label, accelerator */ N_("_Preview Pane"), "F7", + /* tooltip */ N_("Show or hide the preview pane"), + G_CALLBACK (action_preview_pane_callback), /* is_active */ FALSE }, /* name, stock id */ { NEMO_ACTION_SHOW_THUMBNAILS, NULL, /* label, accelerator */ N_("Show _Thumbnails"), NULL, diff --git a/src/nemo-window-private.h b/src/nemo-window-private.h index b8f8e0444..d203867f2 100644 --- a/src/nemo-window-private.h +++ b/src/nemo-window-private.h @@ -72,6 +72,9 @@ struct NemoWindowDetails NemoWindowPane *active_pane; GtkWidget *content_paned; + GtkWidget *base_paned; + GtkWidget *secondary_paned; + NemoNavigationState *nav_state; /* Side Pane */ @@ -92,13 +95,18 @@ struct NemoWindowDetails guint menu_hide_delay_id; - /* split view */ - GtkWidget *split_view_hpane; - // A closed pane's location, valid until the remaining pane // location changes. GFile *secondary_pane_last_location; + /* preview pane */ + GtkWidget *preview_pane; // NemoPreviewPane instance + gboolean show_preview_pane; // State flag + gboolean preview_pane_width_set; // Initial width has been set from settings + + /* split view */ + gboolean show_split_view; // State flag + gboolean disable_chrome; guint sidebar_width_handler_id; diff --git a/src/nemo-window.c b/src/nemo-window.c index a9e502d72..db168eb56 100644 --- a/src/nemo-window.c +++ b/src/nemo-window.c @@ -48,6 +48,7 @@ #include "nemo-icon-view.h" #include "nemo-list-view.h" #include "nemo-statusbar.h" +#include "nemo-preview-pane.h" #include #include @@ -105,6 +106,8 @@ enum { PROP_DISABLE_CHROME = 1, PROP_SIDEBAR_VIEW_TYPE, PROP_SHOW_SIDEBAR, + PROP_SHOW_PREVIEW_PANE, + PROP_SHOW_SPLIT_VIEW, NUM_PROPERTIES, }; @@ -414,7 +417,7 @@ setup_side_pane_width (NemoWindow *window) g_settings_get_int (nemo_window_state, NEMO_WINDOW_STATE_SIDEBAR_WIDTH); - gtk_paned_set_position (GTK_PANED (window->details->content_paned), + gtk_paned_set_position (GTK_PANED (window->details->base_paned), window->details->side_pane_width); } @@ -429,7 +432,7 @@ nemo_window_set_up_sidebar (NemoWindow *window) gtk_style_context_add_class (gtk_widget_get_style_context (window->details->sidebar), GTK_STYLE_CLASS_SIDEBAR); - gtk_paned_pack1 (GTK_PANED (window->details->content_paned), + gtk_paned_pack1 (GTK_PANED (window->details->base_paned), GTK_WIDGET (window->details->sidebar), FALSE, FALSE); @@ -602,13 +605,40 @@ on_menu_selection_done (GtkMenuShell *menushell, window->details->menu_hide_delay_id = g_timeout_add (0, (GSourceFunc) hide_menu_on_delay, window); } +/* + 3-pane layout + + ------------------------------------------------------ + | |------------------------------------------|| + | ||--------split_view_paned--------| || + | S || | | P || + | I || | | R || + | D || VIEW | VIEW | E || + | E || | | V || + | B || | | I || + | A || | | E || + | R || | | W || + | || | | || + | || | | || + | ||---------------|----------------| || + | |--------------secondary_paned-------------|| + |---------------------base_paned---------------------| + + base_paned: + - L: sidebar widget + - R: secondary_paned: + - L: content_paned: + - content_view + - content_view (when split-view on) + - R: preview pane +*/ + static void nemo_window_constructed (GObject *self) { NemoWindow *window; GtkWidget *grid; GtkWidget *menu; - GtkWidget *hpaned; GtkWidget *vbox; GtkWidget *toolbar_holder; GtkWidget *nemo_statusbar; @@ -685,28 +715,33 @@ nemo_window_constructed (GObject *self) g_signal_connect_object (nemo_signaller_get_current (), "popup_menu_changed", G_CALLBACK (nemo_window_load_extension_menus), window, G_CONNECT_SWAPPED); - window->details->content_paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); - gtk_widget_set_hexpand (window->details->content_paned, TRUE); - gtk_widget_set_vexpand (window->details->content_paned, TRUE); + /* Create base_paned: sidebar | secondary_paned */ + window->details->base_paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_hexpand (window->details->base_paned, TRUE); + gtk_widget_set_vexpand (window->details->base_paned, TRUE); - gtk_container_add (GTK_CONTAINER (grid), window->details->content_paned); - gtk_widget_show (window->details->content_paned); + gtk_container_add (GTK_CONTAINER (grid), window->details->base_paned); + gtk_widget_show (window->details->base_paned); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); - gtk_paned_pack2 (GTK_PANED (window->details->content_paned), vbox, + gtk_paned_pack2 (GTK_PANED (window->details->base_paned), vbox, TRUE, FALSE); gtk_widget_show (vbox); - hpaned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); - gtk_box_pack_start (GTK_BOX (vbox), hpaned, TRUE, TRUE, 0); - gtk_widget_show (hpaned); - window->details->split_view_hpane = hpaned; + /* Create secondary_paned: content_paned | preview_pane */ + window->details->secondary_paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); + gtk_box_pack_start (GTK_BOX (vbox), window->details->secondary_paned, TRUE, TRUE, 0); + gtk_widget_show (window->details->secondary_paned); + + /* Create content_paned: primary_view | secondary_view */ + window->details->content_paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); + gtk_paned_pack1 (GTK_PANED (window->details->secondary_paned), window->details->content_paned, TRUE, FALSE); + gtk_widget_show (window->details->content_paned); pane = nemo_window_pane_new (window); window->details->panes = g_list_prepend (window->details->panes, pane); - gtk_paned_pack1 (GTK_PANED (hpaned), GTK_WIDGET (pane), TRUE, FALSE); - + gtk_paned_pack1 (GTK_PANED (window->details->content_paned), GTK_WIDGET (pane), TRUE, FALSE); nemo_statusbar = nemo_status_bar_new (window); window->details->nemo_status_bar = nemo_statusbar; @@ -785,6 +820,20 @@ nemo_window_set_property (GObject *object, case PROP_SHOW_SIDEBAR: nemo_window_set_show_sidebar (window, g_value_get_boolean (value)); break; + case PROP_SHOW_PREVIEW_PANE: + if (g_value_get_boolean (value)) { + nemo_window_preview_pane_on (window); + } else { + nemo_window_preview_pane_off (window); + } + break; + case PROP_SHOW_SPLIT_VIEW: + if (g_value_get_boolean (value)) { + nemo_window_split_view_on (window); + } else { + nemo_window_split_view_off (window); + } + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, arg_id, pspec); break; @@ -811,6 +860,12 @@ nemo_window_get_property (GObject *object, case PROP_SHOW_SIDEBAR: g_value_set_boolean (value, window->details->show_sidebar); break; + case PROP_SHOW_PREVIEW_PANE: + g_value_set_boolean (value, window->details->show_preview_pane); + break; + case PROP_SHOW_SPLIT_VIEW: + g_value_set_boolean (value, window->details->show_split_view); + break; default: g_assert_not_reached (); break; @@ -1053,6 +1108,10 @@ nemo_window_set_active_pane (NemoWindow *window, } } +/* Forward declarations for preview pane callbacks */ +static void preview_pane_selection_changed_callback (NemoView *view, NemoWindow *window); +static void slot_location_changed_callback (NemoWindowSlot *slot, const char *from_uri, const char *to_uri, NemoWindow *window); + /* Make both, the given slot the active slot and its corresponding * pane the active pane of the associated window. * new_slot may be NULL. */ @@ -1082,6 +1141,10 @@ nemo_window_set_active_slot (NemoWindow *window, NemoWindowSlot *new_slot) if (old_slot->content_view != NULL) { nemo_window_disconnect_content_view (window, old_slot->content_view); } + /* Disconnect from slot's location-changed signal */ + g_signal_handlers_disconnect_by_func (old_slot, + G_CALLBACK (slot_location_changed_callback), + window); gtk_widget_hide (GTK_WIDGET (old_slot->pane->tool_bar)); /* inform slot & view */ g_signal_emit_by_name (old_slot, "inactive"); @@ -1106,6 +1169,11 @@ nemo_window_set_active_slot (NemoWindow *window, NemoWindowSlot *new_slot) nemo_window_connect_content_view (window, new_slot->content_view); } + /* Connect to slot's location-changed signal for preview pane updates */ + g_signal_connect (new_slot, "location-changed", + G_CALLBACK (slot_location_changed_callback), + window); + // Show active toolbar gboolean show_toolbar; show_toolbar = g_settings_get_boolean (nemo_window_state, NEMO_WINDOW_STATE_START_WITH_TOOLBAR); @@ -1558,6 +1626,96 @@ zoom_level_changed_callback (NemoView *view, nemo_window_sync_zoom_widgets (window); } +/* Forward declarations */ +static void nemo_window_preview_pane_on_internal (NemoWindow *window, gboolean write_metadata); +static void nemo_window_preview_pane_off_internal (NemoWindow *window, gboolean write_metadata); + +/* Check if any slot in the window (across all panes and tabs) has + * preview pane enabled in its metadata, excluding the given slot. + */ +static gboolean +any_other_slot_wants_preview_pane (NemoWindow *window, + NemoWindowSlot *exclude_slot) +{ + GList *pane_node; + GList *slot_node; + NemoWindowPane *pane; + NemoWindowSlot *slot; + NemoFile *directory_file; + gboolean show_preview; + + for (pane_node = window->details->panes; pane_node != NULL; pane_node = pane_node->next) { + pane = NEMO_WINDOW_PANE (pane_node->data); + + for (slot_node = pane->slots; slot_node != NULL; slot_node = slot_node->next) { + slot = NEMO_WINDOW_SLOT (slot_node->data); + + /* Skip the slot we're excluding */ + if (slot == exclude_slot) { + continue; + } + + /* Check if this slot has a view with a directory */ + if (slot->content_view != NULL) { + directory_file = nemo_view_get_directory_as_file (slot->content_view); + if (directory_file != NULL) { + show_preview = nemo_file_get_boolean_metadata (directory_file, + NEMO_METADATA_KEY_WINDOW_SHOW_PREVIEW_PANE, + FALSE); + if (show_preview) { + return TRUE; + } + } + } + } + } + + return FALSE; +} + +/* Synchronize the preview pane state based on a slot's directory metadata. + * This checks the slot's current directory's saved preference and applies it, + * taking into account whether other slots in the window want the preview pane. + * If no metadata is saved for this directory, uses the default from GSettings. + */ +static void +sync_preview_pane_from_slot_metadata (NemoWindow *window, + NemoWindowSlot *slot) +{ + NemoFile *directory_file; + gboolean show_preview; + gboolean default_show_preview; + + if (slot == NULL || slot->content_view == NULL) { + return; + } + + directory_file = nemo_view_get_directory_as_file (slot->content_view); + if (directory_file == NULL) { + return; + } + + /* Get the default from GSettings - this is used when directory has no saved metadata */ + default_show_preview = g_settings_get_boolean (nemo_preferences, + NEMO_PREFERENCES_SHOW_PREVIEW_PANE); + + /* Get the preview pane preference: uses directory metadata if saved, + * otherwise falls back to the GSettings default */ + show_preview = nemo_file_get_boolean_metadata (directory_file, + NEMO_METADATA_KEY_WINDOW_SHOW_PREVIEW_PANE, + default_show_preview); + + /* Apply the saved state (don't write metadata - this is automatic sync, not user action) */ + if (show_preview && !window->details->show_preview_pane) { + nemo_window_preview_pane_on_internal (window, FALSE); + } else if (!show_preview && window->details->show_preview_pane) { + /* Only close preview pane if no other slot wants it */ + if (!any_other_slot_wants_preview_pane (window, slot)) { + nemo_window_preview_pane_off_internal (window, FALSE); + } + } +} + /* These are called * A) when switching the view within the active slot @@ -1583,6 +1741,16 @@ nemo_window_connect_content_view (NemoWindow *window, G_CALLBACK (zoom_level_changed_callback), window); + /* Check if this directory has a saved preview pane state */ + sync_preview_pane_from_slot_metadata (window, slot); + + /* Connect preview pane selection updates if preview is showing */ + if (window->details->preview_pane) { + g_signal_connect_object (view, "selection-changed", + G_CALLBACK (preview_pane_selection_changed_callback), + window, 0); + } + /* Update displayed the selected view type in the toolbar and menu. */ if (slot->pending_location == NULL) { nemo_window_sync_view_type (window); @@ -1607,6 +1775,11 @@ nemo_window_disconnect_content_view (NemoWindow *window, } g_signal_handlers_disconnect_by_func (view, G_CALLBACK (zoom_level_changed_callback), window); + + /* Disconnect preview pane selection updates if preview is showing */ + if (window->details->preview_pane) { + g_signal_handlers_disconnect_by_func (view, G_CALLBACK (preview_pane_selection_changed_callback), window); + } } /** @@ -1847,7 +2020,7 @@ create_extra_pane (NemoWindow *window) pane = nemo_window_pane_new (window); window->details->panes = g_list_append (window->details->panes, pane); - paned = GTK_PANED (window->details->split_view_hpane); + paned = GTK_PANED (window->details->content_paned); g_signal_connect_after (paned, "notify::position", @@ -2081,6 +2254,20 @@ nemo_window_class_init (NemoWindowClass *class) FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + properties[PROP_SHOW_PREVIEW_PANE] = + g_param_spec_boolean ("show-preview-pane", + "Show the preview pane", + "Show the preview pane", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + properties[PROP_SHOW_SPLIT_VIEW] = + g_param_spec_boolean ("show-split-view", + "Show split view", + "Show split view", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + signals[GO_UP] = g_signal_new ("go-up", G_TYPE_FROM_CLASS (class), @@ -2187,6 +2374,111 @@ nemo_window_new (GtkApplication *application, NULL); } +static void +slot_location_changed_callback (NemoWindowSlot *slot, + const char *from_uri, + const char *to_uri, + NemoWindow *window) +{ + GList *selection; + NemoFile *file = NULL; + + /* Check if this directory has a saved preview pane state */ + sync_preview_pane_from_slot_metadata (window, slot); + + /* Update preview pane content when location changes */ + if (window->details->preview_pane != NULL) { + /* Clear selection first since we're in a new location */ + nemo_preview_pane_set_file (NEMO_PREVIEW_PANE (window->details->preview_pane), NULL); + + /* If the new location has a selection, show it */ + if (slot->content_view != NULL) { + selection = nemo_view_get_selection (slot->content_view); + if (selection != NULL && selection->data != NULL) { + file = NEMO_FILE (selection->data); + nemo_preview_pane_set_file (NEMO_PREVIEW_PANE (window->details->preview_pane), file); + } + nemo_file_list_free (selection); + } + } +} + +static void +preview_pane_selection_changed_callback (NemoView *view, NemoWindow *window) +{ + GList *selection; + NemoFile *file = NULL; + + if (!window->details->preview_pane) { + return; + } + + selection = nemo_view_get_selection (view); + + if (selection != NULL && selection->data != NULL) { + file = NEMO_FILE (selection->data); /* Show first selected file */ + } + + nemo_preview_pane_set_file (NEMO_PREVIEW_PANE (window->details->preview_pane), file); + + nemo_file_list_free (selection); +} + +static void +set_preview_pane_width_from_settings (NemoWindow *window) +{ + gint saved_width, position, total_width; + + /* Only set initial position once */ + if (window->details->preview_pane_width_set) { + return; + } + + window->details->preview_pane_width_set = TRUE; + + /* Set position based on saved preview pane width */ + saved_width = g_settings_get_int (nemo_preview_pane_preferences, "pane-width"); + total_width = gtk_widget_get_allocated_width (window->details->secondary_paned); + + if (saved_width > 100 && total_width > saved_width) { + /* Position is from left, so subtract preview width from total */ + position = total_width - saved_width; + gtk_paned_set_position (GTK_PANED (window->details->secondary_paned), position); + } else { + /* Fallback: 60/40 split */ + gtk_paned_set_position (GTK_PANED (window->details->secondary_paned), total_width * 0.6); + } +} + +static void +preview_pane_realize_callback (GtkWidget *widget, gpointer user_data) +{ + NemoWindow *window = NEMO_WINDOW (user_data); + set_preview_pane_width_from_settings (window); +} + +static void +preview_pane_position_changed_callback (GObject *paned, GParamSpec *pspec, NemoWindow *window) +{ + gint position, total_width, preview_width; + + /* Don't save position until initial width has been set */ + if (!window->details->preview_pane_width_set) { + return; + } + + position = gtk_paned_get_position (GTK_PANED (paned)); + total_width = gtk_widget_get_allocated_width (GTK_WIDGET (paned)); + + /* Calculate width of preview pane (right side) */ + preview_width = total_width - position; + + /* Only save if preview width is reasonable */ + if (preview_width > 100 && total_width > 0) { + g_settings_set_int (nemo_preview_pane_preferences, "pane-width", preview_width); + } +} + void nemo_window_split_view_on (NemoWindow *window) { @@ -2195,6 +2487,7 @@ nemo_window_split_view_on (NemoWindow *window) old_active_slot = nemo_window_get_active_slot (window); slot = create_extra_pane (window); + gtk_widget_show_all (GTK_WIDGET (slot)); location = window->details->secondary_pane_last_location; @@ -2214,7 +2507,11 @@ nemo_window_split_view_on (NemoWindow *window) nemo_window_slot_open_location (slot, location, 0); g_object_unref (location); + window->details->show_split_view = TRUE; window_set_search_action_text (window, FALSE); + nemo_window_update_show_hide_ui_elements (window); + + g_object_notify_by_pspec (G_OBJECT (window), properties[PROP_SHOW_SPLIT_VIEW]); } void @@ -2236,9 +2533,7 @@ nemo_window_split_view_off (NemoWindow *window) } } - /* Reset split view pane's position so the position can be - * caught again later */ - g_object_set (G_OBJECT (window->details->split_view_hpane), + g_object_set (G_OBJECT (window->details->content_paned), "position", 0, "position-set", FALSE, NULL); @@ -2247,7 +2542,14 @@ nemo_window_split_view_off (NemoWindow *window) nemo_navigation_state_set_master (window->details->nav_state, active_pane->action_group); + window->details->show_split_view = FALSE; nemo_window_update_show_hide_ui_elements (window); + + /* After closing split view, check if the remaining active slot + * wants the preview pane based on its directory metadata */ + sync_preview_pane_from_slot_metadata (window, active_pane->active_slot); + + g_object_notify_by_pspec (G_OBJECT (window), properties[PROP_SHOW_SPLIT_VIEW]); } gboolean @@ -2256,6 +2558,157 @@ nemo_window_split_view_showing (NemoWindow *window) return g_list_length (NEMO_WINDOW (window)->details->panes) > 1; } +static void +nemo_window_preview_pane_on_internal (NemoWindow *window, + gboolean write_metadata) +{ + NemoWindowSlot *slot; + GList *selection; + NemoFile *file = NULL; + + if (nemo_window_is_desktop (window)) { + return; + } + + /* Reset flag so position can be set */ + window->details->preview_pane_width_set = FALSE; + + /* Create preview pane */ + window->details->preview_pane = nemo_preview_pane_new (window); + + /* Pack into right side of secondary_paned */ + gtk_paned_pack2 (GTK_PANED (window->details->secondary_paned), + window->details->preview_pane, + TRUE, FALSE); + + gtk_widget_show (window->details->preview_pane); + + /* Set position from settings - check if paned is already realized */ + if (gtk_widget_get_realized (window->details->secondary_paned)) { + /* Already realized, set position immediately */ + set_preview_pane_width_from_settings (window); + } else { + /* Not realized yet, wait for realize signal */ + g_signal_connect (window->details->secondary_paned, "realize", + G_CALLBACK (preview_pane_realize_callback), + window); + } + + /* Connect signal to save position on resize */ + g_signal_connect (window->details->secondary_paned, "notify::position", + G_CALLBACK (preview_pane_position_changed_callback), + window); + + /* Get current selection and update preview */ + slot = nemo_window_get_active_slot (window); + if (slot != NULL && slot->content_view != NULL) { + selection = nemo_view_get_selection (slot->content_view); + if (selection != NULL && selection->data != NULL) { + file = NEMO_FILE (selection->data); + } + nemo_preview_pane_set_file (NEMO_PREVIEW_PANE (window->details->preview_pane), file); + nemo_file_list_free (selection); + + /* Connect selection-changed signal for the current view */ + /* This will be reconnected automatically when view changes via nemo_window_connect_content_view() */ + g_signal_connect_object (slot->content_view, "selection-changed", + G_CALLBACK (preview_pane_selection_changed_callback), + window, 0); + } + + window->details->show_preview_pane = TRUE; + // nemo_window_update_show_hide_ui_elements (window); + + /* Save preview pane state to directory metadata (only if explicitly requested) */ + if (write_metadata && slot != NULL && slot->content_view != NULL) { + NemoFile *directory_file; + + directory_file = nemo_view_get_directory_as_file (slot->content_view); + if (directory_file != NULL) { + nemo_file_set_boolean_metadata (directory_file, + NEMO_METADATA_KEY_WINDOW_SHOW_PREVIEW_PANE, + FALSE, + TRUE); + } + } + nemo_window_update_show_hide_ui_elements (window); + + g_object_notify_by_pspec (G_OBJECT (window), properties[PROP_SHOW_PREVIEW_PANE]); +} + +void +nemo_window_preview_pane_on (NemoWindow *window) +{ + /* User-triggered action: write metadata to remember this choice */ + nemo_window_preview_pane_on_internal (window, TRUE); +} + +static void +nemo_window_preview_pane_off_internal (NemoWindow *window, + gboolean write_metadata) +{ + GtkPaned *paned; + NemoWindowSlot *slot; + + if (window->details->preview_pane == NULL) { + return; + } + + /* Disconnect signals */ + g_signal_handlers_disconnect_by_func (window->details->secondary_paned, + G_CALLBACK (preview_pane_realize_callback), + window); + g_signal_handlers_disconnect_by_func (window->details->secondary_paned, + G_CALLBACK (preview_pane_position_changed_callback), + window); + + paned = GTK_PANED (window->details->secondary_paned); + + /* Remove from paned */ + gtk_container_remove (GTK_CONTAINER (paned), window->details->preview_pane); + + /* Reset paned position */ + g_object_set (G_OBJECT (paned), + "position", 0, + "position-set", FALSE, + NULL); + + window->details->preview_pane = NULL; + window->details->show_preview_pane = FALSE; + + // nemo_window_update_show_hide_ui_elements (window); + + /* Save preview pane state to directory metadata (only if explicitly requested) */ + slot = nemo_window_get_active_slot (window); + if (write_metadata && slot != NULL && slot->content_view != NULL) { + NemoFile *directory_file; + + directory_file = nemo_view_get_directory_as_file (slot->content_view); + if (directory_file != NULL) { + nemo_file_set_boolean_metadata (directory_file, + NEMO_METADATA_KEY_WINDOW_SHOW_PREVIEW_PANE, + FALSE, + FALSE); + } + } + nemo_window_update_show_hide_ui_elements (window); + + g_object_notify_by_pspec (G_OBJECT (window), properties[PROP_SHOW_PREVIEW_PANE]); +} + +void +nemo_window_preview_pane_off (NemoWindow *window) +{ + /* User-triggered action: write metadata to remember this choice */ + nemo_window_preview_pane_off_internal (window, TRUE); +} + +gboolean +nemo_window_preview_pane_showing (NemoWindow *window) +{ + return window->details->show_preview_pane; +} + void nemo_window_clear_secondary_pane_location (NemoWindow *window) { diff --git a/src/nemo-window.h b/src/nemo-window.h index bb6a4d05a..1ffb07dfb 100644 --- a/src/nemo-window.h +++ b/src/nemo-window.h @@ -157,6 +157,10 @@ void nemo_window_split_view_on (NemoWindow *window); void nemo_window_split_view_off (NemoWindow *window); gboolean nemo_window_split_view_showing (NemoWindow *window); +void nemo_window_preview_pane_on (NemoWindow *window); +void nemo_window_preview_pane_off (NemoWindow *window); +gboolean nemo_window_preview_pane_showing (NemoWindow *window); + gboolean nemo_window_disable_chrome_mapping (GValue *value, GVariant *variant, gpointer user_data);