From 03e4ee3bd70cd30e6e93c043a29633cfadb9bcc2 Mon Sep 17 00:00:00 2001 From: Eric Blanquer Date: Fri, 13 Mar 2026 19:43:17 +0100 Subject: [PATCH 1/2] 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} */ From 7e6759306d2f43f339f15b9b1fa2cd95338cc42f Mon Sep 17 00:00:00 2001 From: Eric Blanquer Date: Sat, 14 Mar 2026 14:49:34 +0100 Subject: [PATCH 2/2] Add pinned clipboard items support Items can be pinned to always appear at the top of the clipboard menu. Pinned items are preserved when clearing history. Features: - Right-click on menu item to pin/unpin - Press 'p' key to toggle pin on selected item - Pinned items section separated by divider in menu - Preferences tab to manage pinned items with drag-and-drop reordering, up/down buttons, add text, and image preview on hover - Menu reopens at same position after pin/unpin - Pinned checksums stored in GSettings --- data/net.launchpad.Diodon.gschema.xml | 5 + libdiodon/clipboard-menu-item.vala | 14 ++ libdiodon/clipboard-menu.vala | 79 +++++++++- libdiodon/controller.vala | 100 +++++++++++- libdiodon/preferences-view.vala | 213 +++++++++++++++++++++++++- 5 files changed, 395 insertions(+), 16 deletions(-) diff --git a/data/net.launchpad.Diodon.gschema.xml b/data/net.launchpad.Diodon.gschema.xml index da18f25..62b79d9 100644 --- a/data/net.launchpad.Diodon.gschema.xml +++ b/data/net.launchpad.Diodon.gschema.xml @@ -35,6 +35,11 @@ Number of recent clipboard items Number of recent clipboard items shown in clipboard menu. + + [] + Pinned clipboard items + Checksums of clipboard items that are pinned to the top of the menu and preserved on clear. + '^\\s+$' Clipboard content filter pattern diff --git a/libdiodon/clipboard-menu-item.vala b/libdiodon/clipboard-menu-item.vala index 58a298a..b0f5fa0 100644 --- a/libdiodon/clipboard-menu-item.vala +++ b/libdiodon/clipboard-menu-item.vala @@ -74,6 +74,20 @@ namespace Diodon return; } debug("on_item_select: preview %dx%d", preview.width, preview.height); + show_preview_pixbuf(preview); + } + + public static void show_preview_for(IClipboardItem item) + { + Gdk.Pixbuf? preview = item.get_preview_pixbuf(); + if (preview == null) { + return; + } + show_preview_pixbuf(preview); + } + + private static void show_preview_pixbuf(Gdk.Pixbuf preview) + { if (_preview_window == null) { _preview_window = new Gtk.Window(Gtk.WindowType.POPUP); _preview_window.set_type_hint(Gdk.WindowTypeHint.TOOLTIP); diff --git a/libdiodon/clipboard-menu.vala b/libdiodon/clipboard-menu.vala index 142abcc..be1645b 100644 --- a/libdiodon/clipboard-menu.vala +++ b/libdiodon/clipboard-menu.vala @@ -28,16 +28,19 @@ namespace Diodon { private Controller controller; private unowned List static_menu_items; + private int _saved_menu_x = -1; + private int _saved_menu_y = -1; /** * Create clipboard menu * * @param controller reference to controller + * @param pinned_items pinned clipboard items shown at the top * @param items clipboard items to be shown * @param menu_items additional menu items to be added after separator * @param privacy_mode check whether privacy mode is enabled */ - public ClipboardMenu(Controller controller, List items, List? static_menu_items, bool privace_mode, string? error = null) + public ClipboardMenu(Controller controller, List pinned_items, List items, List? static_menu_items, bool privace_mode, string? error = null) { this.controller = controller; this.static_menu_items = static_menu_items; @@ -46,7 +49,7 @@ namespace Diodon Gtk.MenuItem error_item = new Gtk.MenuItem.with_label(wrap_label(error)); error_item.set_sensitive(false); append(error_item); - } else if(items.length() <= 0) { + } else if(pinned_items.length() <= 0 && items.length() <= 0) { Gtk.MenuItem empty_item = new Gtk.MenuItem.with_label(_("")); empty_item.set_sensitive(false); append(empty_item); @@ -60,9 +63,17 @@ namespace Diodon append(privacy_item); } + foreach(IClipboardItem item in pinned_items) { + append_clipboard_item(item, true); + } + + if (pinned_items.length() > 0 && items.length() > 0) { + Gtk.SeparatorMenuItem pin_sep = new Gtk.SeparatorMenuItem(); + append(pin_sep); + } foreach(IClipboardItem item in items) { - append_clipboard_item(item); + append_clipboard_item(item, false); } Gtk.SeparatorMenuItem sep_item = new Gtk.SeparatorMenuItem(); @@ -90,21 +101,67 @@ namespace Diodon this.key_press_event.connect(on_key_pressed); this.hide.connect(() => { ClipboardMenuItem.hide_preview(); }); + this.map.connect(() => { + if (get_window() != null) { + int x, y; + get_window().get_origin(out x, out y); + _saved_menu_x = x; + _saved_menu_y = y; + } + }); } /** * Append given clipboard item to menu. * - * @param entry entry to be added + * @param item clipboard item to add + * @param pinned whether the item is pinned */ - public void append_clipboard_item(IClipboardItem item) + public void append_clipboard_item(IClipboardItem item, bool pinned = false) { ClipboardMenuItem menu_item = new ClipboardMenuItem(item); menu_item.activate.connect(on_clicked_item); + menu_item.button_press_event.connect((event) => { + if (event.button == 3) { + show_pin_context_menu(menu_item, pinned, event); + return true; + } + return false; + }); menu_item.show(); append(menu_item); } + private void show_pin_context_menu(ClipboardMenuItem menu_item, bool pinned, Gdk.EventButton event) + { + Gtk.Menu ctx = new Gtk.Menu(); + string label = pinned ? _("Unpin") : _("Pin"); + Gtk.MenuItem pin_item = new Gtk.MenuItem.with_label(label); + int reopen_x = _saved_menu_x; + int reopen_y = _saved_menu_y; + pin_item.activate.connect(() => { + controller.toggle_pin_item.begin(menu_item.get_item_checksum(), false); + }); + ctx.append(pin_item); + ctx.show_all(); + ctx.deactivate.connect(() => { + controller.rebuild_recent_menu.begin((obj, res) => { + controller.rebuild_recent_menu.end(res); + if (reopen_x >= 0 && reopen_y >= 0) { + Gtk.Menu new_menu = controller.get_recent_menu() as Gtk.Menu; + if (new_menu != null) { + new_menu.popup(null, null, (menu, ref x, ref y, out push_in) => { + x = reopen_x; + y = reopen_y; + push_in = false; + }, 0, Gtk.get_current_event_time()); + } + } + }); + }); + ctx.popup_at_pointer(event); + } + public void show_menu() { // timer is needed to workaround race condition between X11 and Gdk event @@ -182,12 +239,14 @@ namespace Diodon } /** - * Allow moving of cursor with vi-style j and k keys + * Allow moving of cursor with vi-style j and k keys, + * and p to toggle pin on selected item. */ private bool on_key_pressed(Gdk.EventKey event) { uint down_keyval = Gdk.keyval_from_name("j"); uint up_keyval = Gdk.keyval_from_name("k"); + uint pin_keyval = Gdk.keyval_from_name("p"); uint pressed_keyval = Gdk.keyval_to_lower(event.keyval); if(pressed_keyval == down_keyval) { @@ -205,6 +264,14 @@ namespace Diodon move_selected(-1); return true; } + if(pressed_keyval == pin_keyval) { + Gtk.MenuItem? selected = get_selected_item() as Gtk.MenuItem; + if (selected != null && selected is ClipboardMenuItem) { + ClipboardMenuItem clip_item = (ClipboardMenuItem) selected; + controller.toggle_pin_item.begin(clip_item.get_item_checksum()); + } + return true; + } return false; } diff --git a/libdiodon/controller.vala b/libdiodon/controller.vala index 5e7fec7..af65bdd 100644 --- a/libdiodon/controller.vala +++ b/libdiodon/controller.vala @@ -66,6 +66,8 @@ namespace Diodon */ public signal void on_recent_menu_changed(Gtk.Menu recent_menu); + public signal void on_pinned_items_changed(); + public delegate void ActionCallback(string[] args); public Controller() @@ -531,11 +533,30 @@ namespace Diodon items = new List(); } + List pinned = yield get_pinned_items(); + + // remove pinned items from the regular list to avoid duplicates + string[] pinned_checksums = settings_clipboard.get_strv("pinned-items"); + List filtered_items = new List(); + foreach (IClipboardItem item in items) { + bool is_pinned = false; + foreach (string pc in pinned_checksums) { + if (item.get_checksum() == pc) { + is_pinned = true; + break; + } + } + if (!is_pinned) { + filtered_items.append(item); + } + } + if(recent_menu != null) { recent_menu.destroy_menu(); } - recent_menu = new ClipboardMenu(this, items, static_recent_menu_items, + recent_menu = new ClipboardMenu(this, pinned, filtered_items, + static_recent_menu_items, storage.is_privacy_mode_enabled(), error); on_recent_menu_changed(recent_menu); } @@ -643,7 +664,73 @@ namespace Diodon */ public void show_preferences() { - preferences_view.show(configuration); + preferences_view.show(configuration, this); + } + + public void set_pinned_checksums(string[] checksums) + { + settings_clipboard.set_strv("pinned-items", checksums); + rebuild_recent_menu.begin(); + } + + public bool is_item_pinned(string checksum) + { + string[] pinned = settings_clipboard.get_strv("pinned-items"); + foreach (string c in pinned) { + if (c == checksum) { + return true; + } + } + return false; + } + + public async void toggle_pin_item(string checksum, bool rebuild = true) + { + string[] pinned = settings_clipboard.get_strv("pinned-items"); + string[] new_pinned = {}; + bool found = false; + foreach (string c in pinned) { + if (c == checksum) { + found = true; + } + else { + new_pinned += c; + } + } + if (!found) { + new_pinned += checksum; + } + settings_clipboard.set_strv("pinned-items", new_pinned); + on_pinned_items_changed(); + if (rebuild) { + yield rebuild_recent_menu(); + } + } + + public async void add_and_pin_text(string text) + { + IClipboardItem item = new TextClipboardItem(ClipboardType.CLIPBOARD, text, null, new DateTime.now_utc()); + yield storage.add_item(item); + string checksum = item.get_checksum(); + if (!is_item_pinned(checksum)) { + yield toggle_pin_item(checksum); + } + else { + yield rebuild_recent_menu(); + } + } + + public async List get_pinned_items() + { + string[] pinned = settings_clipboard.get_strv("pinned-items"); + List items = new List(); + foreach (string checksum in pinned) { + IClipboardItem? item = yield storage.get_item_by_checksum(checksum); + if (item != null) { + items.append(item); + } + } + return items; } /** @@ -651,12 +738,13 @@ namespace Diodon */ public async void clear() { + List pinned = yield get_pinned_items(); yield storage.clear(); on_clear(); - - // Bug #1383013: - // in some rare circumstances doesn't the recent menu get refreshed - // when clear is executed; therefore forcing it here as a workaround + foreach (IClipboardItem item in pinned) { + yield storage.add_item(item); + } + // Bug #1383013: force menu refresh as workaround yield rebuild_recent_menu(); } diff --git a/libdiodon/preferences-view.vala b/libdiodon/preferences-view.vala index 0d635fb..2649eb7 100644 --- a/libdiodon/preferences-view.vala +++ b/libdiodon/preferences-view.vala @@ -27,7 +27,6 @@ namespace Diodon class PreferencesView : GLib.Object { private Gtk.Dialog preferences; - public PreferencesView() { } @@ -35,9 +34,10 @@ namespace Diodon /** * Show preferences view * - * @param configuraiton configuration to initialize dialog + * @param configuration configuration to initialize dialog + * @param controller controller for pinned items management */ - public void show(ClipboardConfiguration configuration) + public void show(ClipboardConfiguration configuration, Controller? controller = null) { // check if preferences window is already open if(preferences == null) { @@ -110,6 +110,12 @@ namespace Diodon Gtk.Box plugins_box = builder.get_object("plugins_box") as Gtk.Box; plugins_box.pack_start(manager); + // pinned items tab + if (controller != null) { + Gtk.Notebook notebook = builder.get_object("notebook_preferences") as Gtk.Notebook; + build_pinned_tab(notebook, controller); + } + // close Gtk.Button close = builder.get_object("button_close") as Gtk.Button; close.clicked.connect(hide); @@ -125,6 +131,203 @@ namespace Diodon } } + private bool _inhibit_save = false; + private int _select_after_repopulate = -1; + private Gtk.TreeView? _pinned_tree_view = null; + + private void build_pinned_tab(Gtk.Notebook notebook, Controller controller) + { + Gtk.Box pinned_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); + pinned_box.border_width = 12; + + Gtk.ListStore store = new Gtk.ListStore(3, typeof(Gdk.Pixbuf), typeof(string), typeof(string)); + Gtk.TreeView tree_view = new Gtk.TreeView.with_model(store); + _pinned_tree_view = tree_view; + tree_view.headers_visible = false; + tree_view.reorderable = true; + + Gtk.CellRendererPixbuf icon_cell = new Gtk.CellRendererPixbuf(); + tree_view.insert_column_with_attributes(-1, "Icon", icon_cell, "pixbuf", 0); + + Gtk.CellRendererText cell = new Gtk.CellRendererText(); + cell.ellipsize = Pango.EllipsizeMode.END; + tree_view.insert_column_with_attributes(-1, "Label", cell, "text", 1); + + populate_pinned_list(store, controller); + + store.row_deleted.connect(() => { + if (!_inhibit_save) { + save_pinned_order(store, controller); + } + }); + + string _hover_checksum = ""; + tree_view.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK); + tree_view.motion_notify_event.connect((event) => { + Gtk.TreePath? path; + if (tree_view.get_path_at_pos((int) event.x, (int) event.y, out path, null, null, null)) { + Gtk.TreeIter iter; + if (store.get_iter(out iter, path)) { + string checksum; + store.get(iter, 2, out checksum); + if (checksum == _hover_checksum) { + return false; + } + ClipboardMenuItem.hide_preview(); + _hover_checksum = checksum; + controller.get_item_by_checksum.begin(checksum, null, (obj, res) => { + IClipboardItem? item = controller.get_item_by_checksum.end(res); + if (item != null) { + ClipboardMenuItem.show_preview_for(item); + } + }); + } + } + else { + _hover_checksum = ""; + ClipboardMenuItem.hide_preview(); + } + return false; + }); + tree_view.leave_notify_event.connect(() => { + _hover_checksum = ""; + ClipboardMenuItem.hide_preview(); + return false; + }); + + Gtk.Box list_and_buttons = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); + + Gtk.ScrolledWindow scroll = new Gtk.ScrolledWindow(null, null); + scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); + scroll.add(tree_view); + list_and_buttons.pack_start(scroll, true, true, 0); + + Gtk.Box side_buttons = new Gtk.Box(Gtk.Orientation.VERTICAL, 4); + Gtk.Button up_button = new Gtk.Button.with_label("\xe2\x96\xb2"); + Gtk.Button down_button = new Gtk.Button.with_label("\xe2\x96\xbc"); + Gtk.Button remove_button = new Gtk.Button.with_label(_("Unpin")); + + up_button.clicked.connect(() => { + Gtk.TreeModel model; + Gtk.TreeIter iter; + if (tree_view.get_selection().get_selected(out model, out iter)) { + Gtk.TreeIter prev = iter; + if (store.iter_previous(ref prev)) { + store.swap(iter, prev); + save_pinned_order(store, controller); + } + } + }); + + down_button.clicked.connect(() => { + Gtk.TreeModel model; + Gtk.TreeIter iter; + if (tree_view.get_selection().get_selected(out model, out iter)) { + Gtk.TreeIter next = iter; + if (store.iter_next(ref next)) { + store.swap(iter, next); + save_pinned_order(store, controller); + } + } + }); + + remove_button.clicked.connect(() => { + Gtk.TreeModel model; + Gtk.TreeIter iter; + if (tree_view.get_selection().get_selected(out model, out iter)) { + string checksum; + model.get(iter, 2, out checksum); + ClipboardMenuItem.hide_preview(); + Gtk.TreePath? path = store.get_path(iter); + int removed_index = path != null ? path.get_indices()[0] : -1; + _inhibit_save = true; + store.remove(ref iter); + _inhibit_save = false; + _select_after_repopulate = removed_index; + controller.toggle_pin_item.begin(checksum); + } + }); + + side_buttons.pack_start(up_button, false, false, 0); + side_buttons.pack_start(down_button, false, false, 0); + side_buttons.pack_start(remove_button, false, false, 0); + list_and_buttons.pack_start(side_buttons, false, false, 0); + + pinned_box.pack_start(list_and_buttons, true, true, 0); + + controller.on_pinned_items_changed.connect(() => { + ClipboardMenuItem.hide_preview(); + populate_pinned_list(store, controller); + }); + + Gtk.Entry add_entry = new Gtk.Entry(); + add_entry.placeholder_text = _("Text to pin..."); + Gtk.Button add_button = new Gtk.Button.with_label(_("Add")); + add_button.clicked.connect(() => { + string text = add_entry.get_text().strip(); + if (text.length > 0) { + controller.add_and_pin_text.begin(text); + add_entry.set_text(""); + } + }); + add_entry.activate.connect(() => { + add_button.clicked(); + }); + + Gtk.Box add_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); + add_box.pack_start(add_entry, true, true, 0); + add_box.pack_start(add_button, false, false, 0); + pinned_box.pack_start(add_box, false, false, 0); + + Gtk.Label tab_label = new Gtk.Label(_("Pinned")); + notebook.append_page(pinned_box, tab_label); + } + + private void save_pinned_order(Gtk.ListStore store, Controller controller) + { + string[] checksums = {}; + Gtk.TreeIter iter; + if (store.get_iter_first(out iter)) { + do { + string checksum; + store.get(iter, 2, out checksum); + checksums += checksum; + } while (store.iter_next(ref iter)); + } + controller.set_pinned_checksums(checksums); + } + + private void populate_pinned_list(Gtk.ListStore store, Controller controller) + { + _inhibit_save = true; + store.clear(); + _inhibit_save = false; + controller.get_pinned_items.begin((obj, res) => { + List items = controller.get_pinned_items.end(res); + int count = 0; + foreach (IClipboardItem item in items) { + Gtk.TreeIter iter; + store.append(out iter); + Gtk.Image? image = item.get_image(); + Gdk.Pixbuf? pixbuf = null; + if (image != null) { + pixbuf = image.get_pixbuf(); + } + store.set(iter, 0, pixbuf, 1, item.get_label(), 2, item.get_checksum()); + count++; + } + if (_select_after_repopulate >= 0 && _pinned_tree_view != null && count > 0) { + int idx = _select_after_repopulate; + if (idx >= count) { + idx = count - 1; + } + Gtk.TreePath path = new Gtk.TreePath.from_indices(idx); + _pinned_tree_view.get_selection().select_path(path); + _select_after_repopulate = -1; + } + }); + } + /** * Hide preferences view */ @@ -138,8 +341,10 @@ namespace Diodon */ public void reset() { + ClipboardMenuItem.hide_preview(); + _pinned_tree_view = null; + _select_after_repopulate = -1; preferences = null; } } } -