From 03e4ee3bd70cd30e6e93c043a29633cfadb9bcc2 Mon Sep 17 00:00:00 2001 From: Eric Blanquer Date: Fri, 13 Mar 2026 19:43:17 +0100 Subject: [PATCH] Add image preview tooltip on clipboard menu hover Show a scaled preview popup when hovering over image clipboard items in the menu. Preview is clamped to screen bounds and framed with a visible border. --- libdiodon/clipboard-item.vala | 7 +++ libdiodon/clipboard-menu-item.vala | 83 +++++++++++++++++++++++++++++ libdiodon/clipboard-menu.vala | 1 + libdiodon/file-clipboard-item.vala | 23 ++++++++ libdiodon/image-clipboard-item.vala | 22 ++++++++ libdiodon/text-clipboard-item.vala | 8 +++ 6 files changed, 144 insertions(+) diff --git a/libdiodon/clipboard-item.vala b/libdiodon/clipboard-item.vala index 02b4f83..93ab0b1 100644 --- a/libdiodon/clipboard-item.vala +++ b/libdiodon/clipboard-item.vala @@ -66,6 +66,13 @@ namespace Diodon */ public abstract Gtk.Image? get_image(); + /** + * preview pixbuf for tooltip display + * + * @return scaled preview pixbuf or null if not available + */ + public abstract Gdk.Pixbuf? get_preview_pixbuf(); + /** * icon to represent type of clipboard item * diff --git a/libdiodon/clipboard-menu-item.vala b/libdiodon/clipboard-menu-item.vala index 0552137..58a298a 100644 --- a/libdiodon/clipboard-menu-item.vala +++ b/libdiodon/clipboard-menu-item.vala @@ -28,6 +28,10 @@ namespace Diodon class ClipboardMenuItem : Gtk.ImageMenuItem { private string _checksum; + private IClipboardItem _item; + private Gdk.Pixbuf? _cached_preview; + private bool _preview_loaded; + private static Gtk.Window? _preview_window; /** * Clipboard item constructor @@ -37,6 +41,7 @@ namespace Diodon public ClipboardMenuItem(IClipboardItem item) { _checksum = item.get_checksum(); + _item = item; set_label(item.get_label()); // check if image needs to be shown @@ -45,6 +50,84 @@ namespace Diodon set_image(image); set_always_show_image(true); } + + debug("ClipboardMenuItem: connecting select/deselect for %s", _checksum); + select.connect(on_item_select); + deselect.connect(on_item_deselect); + } + + private Gdk.Pixbuf? get_preview() + { + if (!_preview_loaded) { + _cached_preview = _item.get_preview_pixbuf(); + _preview_loaded = true; + } + return _cached_preview; + } + + private void on_item_select() + { + debug("on_item_select: checksum=%s", _checksum); + Gdk.Pixbuf? preview = get_preview(); + if (preview == null) { + debug("on_item_select: no preview available"); + return; + } + debug("on_item_select: preview %dx%d", preview.width, preview.height); + if (_preview_window == null) { + _preview_window = new Gtk.Window(Gtk.WindowType.POPUP); + _preview_window.set_type_hint(Gdk.WindowTypeHint.TOOLTIP); + _preview_window.set_app_paintable(true); + Gdk.Screen screen = _preview_window.get_screen(); + Gdk.Visual? visual = screen.get_rgba_visual(); + if (visual != null) { + _preview_window.set_visual(visual); + } + } + _preview_window.foreach((child) => { _preview_window.remove(child); }); + Gtk.Image preview_image = new Gtk.Image.from_pixbuf(preview); + Gtk.CssProvider css = new Gtk.CssProvider(); + try { + css.load_from_data("frame { border: 1px solid #888888; }", -1); + } catch (Error e) { + warning("Failed to load preview CSS: %s", e.message); + } + Gtk.Frame frame = new Gtk.Frame(null); + frame.set_shadow_type(Gtk.ShadowType.NONE); + frame.get_style_context().add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + frame.add(preview_image); + _preview_window.add(frame); + _preview_window.resize(preview.width, preview.height); + Gdk.Display? display = Gdk.Display.get_default(); + Gdk.Seat? seat = display != null ? display.get_default_seat() : null; + if (seat != null) { + int mouse_x, mouse_y; + seat.get_pointer().get_position(null, out mouse_x, out mouse_y); + int x = mouse_x + 16; + int y = mouse_y + 16; + Gdk.Screen screen = _preview_window.get_screen(); + int screen_height = screen.get_height(); + if (y + preview.height > screen_height) { + y = screen_height - preview.height; + } + if (y < 0) { + y = 0; + } + _preview_window.move(x, y); + } + _preview_window.show_all(); + } + + private static void on_item_deselect() + { + hide_preview(); + } + + public static void hide_preview() + { + if (_preview_window != null) { + _preview_window.hide(); + } } /** diff --git a/libdiodon/clipboard-menu.vala b/libdiodon/clipboard-menu.vala index 4c6bd12..142abcc 100644 --- a/libdiodon/clipboard-menu.vala +++ b/libdiodon/clipboard-menu.vala @@ -89,6 +89,7 @@ namespace Diodon show_all(); this.key_press_event.connect(on_key_pressed); + this.hide.connect(() => { ClipboardMenuItem.hide_preview(); }); } /** diff --git a/libdiodon/file-clipboard-item.vala b/libdiodon/file-clipboard-item.vala index 53f41c8..c4d0b7f 100644 --- a/libdiodon/file-clipboard-item.vala +++ b/libdiodon/file-clipboard-item.vala @@ -153,6 +153,29 @@ namespace Diodon return image; } + /** + * {@inheritDoc} + */ + public Gdk.Pixbuf? get_preview_pixbuf() + { + string[] paths = convert_to_paths(_paths); + if (paths.length == 0) { + return null; + } + string path = paths[0]; + string mime_type = get_mime_type(); + if (!mime_type.has_prefix("image/")) { + return null; + } + try { + Gdk.Pixbuf pixbuf = new Gdk.Pixbuf.from_file(path); + return ImageClipboardItem.create_preview_pixbuf(pixbuf); + } catch (Error e) { + warning("Could not load preview for file %s: %s", path, e.message); + return null; + } + } + /** * {@inheritDoc} */ diff --git a/libdiodon/image-clipboard-item.vala b/libdiodon/image-clipboard-item.vala index 0faccbd..10b8e1d 100644 --- a/libdiodon/image-clipboard-item.vala +++ b/libdiodon/image-clipboard-item.vala @@ -150,6 +150,14 @@ namespace Diodon return new Gtk.Image.from_pixbuf(pixbuf_preview); } + /** + * {@inheritDoc} + */ + public Gdk.Pixbuf? get_preview_pixbuf() + { + return create_preview_pixbuf(_pixbuf); + } + /** * {@inheritDoc} */ @@ -224,6 +232,20 @@ namespace Diodon * * @param pixbuf scaled pixbuf */ + public static Gdk.Pixbuf create_preview_pixbuf(Gdk.Pixbuf pixbuf) + { + int max_size = 500; + int width = pixbuf.width; + int height = pixbuf.height; + if (width <= max_size && height <= max_size) { + return pixbuf; + } + double scale = double.min((double) max_size / width, (double) max_size / height); + int new_width = (int) (width * scale); + int new_height = (int) (height * scale); + return pixbuf.scale_simple(new_width, new_height, Gdk.InterpType.BILINEAR); + } + private static Gdk.Pixbuf create_scaled_pixbuf(Gdk.Pixbuf pixbuf) { // get menu icon size diff --git a/libdiodon/text-clipboard-item.vala b/libdiodon/text-clipboard-item.vala index 800df2b..46694f0 100644 --- a/libdiodon/text-clipboard-item.vala +++ b/libdiodon/text-clipboard-item.vala @@ -117,6 +117,14 @@ namespace Diodon return null; // no image available for text content } + /** + * {@inheritDoc} + */ + public Gdk.Pixbuf? get_preview_pixbuf() + { + return null; + } + /** * {@inheritDoc} */