/* Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ public class Sidebar.Tree : Gtk.TreeView { public const int ICON_SIZE = 16; // Only one ExternalDropHandler can be registered with the Tree; it's responsible for completing // the "drag-data-received" signal properly. public delegate void ExternalDropHandler(Gdk.DragContext context, Sidebar.Entry? entry, Gtk.SelectionData data, uint info, uint time); private class EntryWrapper : Object { public Sidebar.Entry entry; public Gtk.TreeRowReference row; public EntryWrapper(Gtk.TreeModel model, Sidebar.Entry entry, Gtk.TreePath path) { this.entry = entry; this.row = new Gtk.TreeRowReference(model, path); } public Gtk.TreePath get_path() { return row.get_path(); } public Gtk.TreeIter get_iter() { Gtk.TreeIter iter; bool valid = row.get_model().get_iter(out iter, get_path()); assert(valid); return iter; } } private class RootWrapper : EntryWrapper { public int root_position; public RootWrapper(Gtk.TreeModel model, Sidebar.Entry entry, Gtk.TreePath path, int root_position) { base (model, entry, path); this.root_position = root_position; } } private enum Columns { NAME, TOOLTIP, WRAPPER, ICON, N_COLUMNS } private Gtk.TreeStore store = new Gtk.TreeStore(Columns.N_COLUMNS, typeof (string), // NAME typeof (string?), // TOOLTIP typeof (EntryWrapper), // WRAPPER typeof (string?) // ICON ); private Gtk.UIManager ui = new Gtk.UIManager(); private Gtk.CellRendererText text_renderer; private unowned ExternalDropHandler drop_handler; private Gtk.Entry? text_entry = null; private Gee.HashMap entry_map = new Gee.HashMap(); private Gee.HashMap branches = new Gee.HashMap(); private int editing_disabled = 0; private bool mask_entry_selected_signal = false; private weak EntryWrapper? selected_wrapper = null; private Gtk.Menu? default_context_menu = null; private bool expander_called_manually = false; private int expander_special_count = 0; private bool is_internal_drag_in_progress = false; private Sidebar.Entry? internal_drag_source_entry = null; private Gtk.TreeRowReference? old_path_ref = null; public signal void entry_selected(Sidebar.SelectableEntry selectable); public signal void selected_entry_removed(Sidebar.SelectableEntry removed); public signal void branch_added(Sidebar.Branch branch); public signal void branch_removed(Sidebar.Branch branch); public signal void branch_shown(Sidebar.Branch branch, bool shown); public signal void page_created(Sidebar.PageRepresentative entry, Page page); public signal void destroying_page(Sidebar.PageRepresentative entry, Page page); public Tree(Gtk.TargetEntry[] target_entries, Gdk.DragAction actions, ExternalDropHandler drop_handler) { set_model(store); get_style_context().add_class("sidebar"); Gtk.TreeViewColumn text_column = new Gtk.TreeViewColumn(); text_column.set_expand(true); Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf(); icon_renderer.follow_state = true; text_column.pack_start(icon_renderer, false); text_column.add_attribute(icon_renderer, "icon_name", Columns.ICON); text_column.set_cell_data_func(icon_renderer, icon_renderer_function); text_renderer = new Gtk.CellRendererText(); text_renderer.ellipsize = Pango.EllipsizeMode.END; text_renderer.editing_canceled.connect(on_editing_canceled); text_renderer.editing_started.connect(on_editing_started); text_column.pack_start(text_renderer, true); text_column.add_attribute(text_renderer, "markup", Columns.NAME); append_column(text_column); Gtk.CellRendererText invisitext = new Gtk.CellRendererText(); Gtk.TreeViewColumn page_holder = new Gtk.TreeViewColumn(); page_holder.pack_start(invisitext, true); page_holder.visible = false; append_column(page_holder); set_headers_visible(false); set_enable_search(false); set_rules_hint(false); set_show_expanders(true); set_reorderable(false); set_enable_tree_lines(false); set_grid_lines(Gtk.TreeViewGridLines.NONE); set_tooltip_column(Columns.TOOLTIP); Gtk.TreeSelection selection = get_selection(); selection.set_mode(Gtk.SelectionMode.BROWSE); selection.set_select_function(on_selection); test_expand_row.connect(on_toggle_row); test_collapse_row.connect(on_toggle_row); // It Would Be Nice if the target entries and actions were gleaned by querying each // Sidebar.Entry as it was added, but that's a tad too complicated for our needs // currently enable_model_drag_dest(target_entries, actions); Gtk.TargetEntry[] source_entries = new Gtk.TargetEntry[0]; source_entries += target_entries[LibraryWindow.TargetType.TAG_PATH]; enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, source_entries, Gdk.DragAction.COPY); this.drop_handler = drop_handler; popup_menu.connect(on_context_menu_keypress); setup_default_context_menu(); drag_begin.connect(on_drag_begin); drag_end.connect(on_drag_end); drag_motion.connect(on_drag_motion); } ~Tree() { text_renderer.editing_canceled.disconnect(on_editing_canceled); text_renderer.editing_started.disconnect(on_editing_started); } public void icon_renderer_function(Gtk.CellLayout layout, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) { EntryWrapper? wrapper = get_wrapper_at_iter(iter); if (wrapper == null) { return; } renderer.visible = !(wrapper.entry is Sidebar.Header); } private void on_drag_begin(Gdk.DragContext ctx) { is_internal_drag_in_progress = true; } private void on_drag_end(Gdk.DragContext ctx) { is_internal_drag_in_progress = false; internal_drag_source_entry = null; } private bool on_drag_motion (Gdk.DragContext context, int x, int y, uint time_) { if (is_internal_drag_in_progress && internal_drag_source_entry == null) { Gtk.TreePath? path; Gtk.TreeViewDropPosition position; get_dest_row_at_pos(x, y, out path, out position); if (path != null) { EntryWrapper wrapper = get_wrapper_at_path(path); if (wrapper != null) internal_drag_source_entry = wrapper.entry; } } return false; } private void setup_default_context_menu() { Gtk.ActionGroup group = new Gtk.ActionGroup("SidebarDefault"); Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0]; Gtk.ActionEntry new_search = { "CommonNewSearch", null, TRANSLATABLE, null, null, on_new_search }; new_search.label = _("Ne_w Saved Search..."); actions += new_search; Gtk.ActionEntry new_tag = { "CommonNewTag", null, TRANSLATABLE, null, null, on_new_tag }; new_tag.label = _("New _Tag..."); actions += new_tag; group.add_actions(actions, this); ui.insert_action_group(group, 0); File ui_file = Resources.get_ui("sidebar_default_context.ui"); try { ui.add_ui_from_file(ui_file.get_path()); } catch (Error err) { AppWindow.error_message("Error loading UI file %s: %s".printf( ui_file.get_path(), err.message)); Application.get_instance().panic(); } default_context_menu = (Gtk.Menu) ui.get_widget("/SidebarDefaultContextMenu"); ui.ensure_update(); } private bool has_wrapper(Sidebar.Entry entry) { return entry_map.has_key(entry); } private EntryWrapper? get_wrapper(Sidebar.Entry entry) { EntryWrapper? wrapper = entry_map.get(entry); if (wrapper == null) warning("Entry %s not found in sidebar", entry.to_string()); return wrapper; } private EntryWrapper? get_wrapper_at_iter(Gtk.TreeIter iter) { Value val; store.get_value(iter, Columns.WRAPPER, out val); EntryWrapper? wrapper = (EntryWrapper?) val; if (wrapper == null) message("No entry found in sidebar at %s", store.get_path(iter).to_string()); return wrapper; } private EntryWrapper? get_wrapper_at_path(Gtk.TreePath path) { Gtk.TreeIter iter; if (!store.get_iter(out iter, path)) { message("No entry found in sidebar at %s", path.to_string()); return null; } return get_wrapper_at_iter(iter); } // Note that this method will result in the "entry-selected" signal to fire if mask_signal // is set to false. public bool place_cursor(Sidebar.Entry entry, bool mask_signal) { if (!expand_to_entry(entry)) return false; EntryWrapper? wrapper = get_wrapper(entry); if (wrapper == null) return false; get_selection().select_path(wrapper.get_path()); mask_entry_selected_signal = mask_signal; set_cursor(wrapper.get_path(), null, false); mask_entry_selected_signal = false; return scroll_to_entry(entry); } public bool is_selected(Sidebar.Entry entry) { EntryWrapper? wrapper = get_wrapper(entry); // Even though get_selection() does not report its return type as nullable, it can be null // if the window has been destroyed. Gtk.TreeSelection selection = get_selection(); if (selection == null) return false; return (wrapper != null) ? selection.path_is_selected(wrapper.get_path()) : false; } public bool is_any_selected() { return get_selection().count_selected_rows() != 0; } private Gtk.TreePath? get_selected_path() { Gtk.TreeModel model; Gtk.TreeSelection? selection = get_selection(); if (selection == null){ return null; } GLib.List rows = selection.get_selected_rows(out model); assert(rows.length() == 0 || rows.length() == 1); return rows.length() != 0 ? rows.nth_data(0) : null; } private string get_name_for_entry(Sidebar.Entry entry) { string name = guarded_markup_escape_text(entry.get_sidebar_name()); Sidebar.EmphasizableEntry? emphasizable_entry = entry as Sidebar.EmphasizableEntry; if (emphasizable_entry != null && emphasizable_entry.is_emphasized()) name = "%s".printf(name); return name; } public virtual bool accept_cursor_changed() { return true; } public override void cursor_changed() { Gtk.TreePath? path = get_selected_path(); if (path == null) { if (base.cursor_changed != null) base.cursor_changed(); return; } EntryWrapper? wrapper = get_wrapper_at_path(path); if (selected_wrapper != wrapper) { EntryWrapper old_wrapper = selected_wrapper; selected_wrapper = wrapper; if (editing_disabled == 0 && wrapper != null && wrapper.entry is Sidebar.RenameableEntry) text_renderer.editable = ((Sidebar.RenameableEntry) wrapper.entry).is_user_renameable(); if (wrapper != null && !mask_entry_selected_signal) { Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry; if (selectable != null) { if (accept_cursor_changed()) { entry_selected(selectable); } else { place_cursor(old_wrapper.entry, true); } } } } if (base.cursor_changed != null) base.cursor_changed(); } public void disable_editing() { if (editing_disabled++ == 0) text_renderer.editable = false; } public void enable_editing() { Gtk.TreePath? path = get_selected_path(); if (path != null && editing_disabled > 0 && --editing_disabled == 0) { EntryWrapper? wrapper = get_wrapper_at_path(path); if (wrapper != null && (wrapper.entry is Sidebar.RenameableEntry)) text_renderer.editable = ((Sidebar.RenameableEntry) wrapper.entry). is_user_renameable(); } } public void toggle_branch_expansion(Gtk.TreePath path, bool expand_all) { expander_called_manually = true; if (is_row_expanded(path)) collapse_row(path); else expand_row(path, expand_all); } public bool expand_to_entry(Sidebar.Entry entry) { expander_called_manually = true; EntryWrapper? wrapper = get_wrapper(entry); if (wrapper == null) return false; expand_to_path(wrapper.get_path()); return true; } public void expand_to_first_child(Sidebar.Entry entry) { expander_called_manually = true; EntryWrapper? wrapper = get_wrapper(entry); if (wrapper == null) return; Gtk.TreePath path = wrapper.get_path(); Gtk.TreeIter iter; while (store.get_iter(out iter, path)) { if (!store.iter_has_child(iter)) break; path.down(); } expand_to_path(path); } public void graft(Sidebar.Branch branch, int position) requires (position >= 0) { assert(!branches.has_key(branch)); branches.set(branch, position); if (branch.get_show_branch()) { associate_branch(branch); if (branch.is_startup_expand_to_first_child()) expand_to_first_child(branch.get_root()); if (branch.is_startup_open_grouping()) expand_to_entry(branch.get_root()); } branch.entry_added.connect(on_branch_entry_added); branch.entry_removed.connect(on_branch_entry_removed); branch.entry_moved.connect(on_branch_entry_moved); branch.entry_reparented.connect(on_branch_entry_reparented); branch.children_reordered.connect(on_branch_children_reordered); branch.show_branch.connect(on_show_branch); branch_added(branch); } // This is used to associate a known branch with the TreeView. private void associate_branch(Sidebar.Branch branch) { assert(branches.has_key(branch)); int position = branches.get(branch); Gtk.TreeIter? insertion_iter = null; // search current roots for insertion point Gtk.TreeIter iter; bool found = store.get_iter_first(out iter); while (found) { RootWrapper? root_wrapper = get_wrapper_at_iter(iter) as RootWrapper; assert(root_wrapper != null); if (position < root_wrapper.root_position) { store.insert_before(out insertion_iter, null, iter); break; } found = store.iter_next(ref iter); } // if not found, append if (insertion_iter == null) store.append(out insertion_iter, null); associate_wrapper(insertion_iter, new RootWrapper(store, branch.get_root(), store.get_path(insertion_iter), position)); // mirror the branch's initial contents from below the root down, let the signals handle // future work associate_children(branch, branch.get_root(), insertion_iter); } private void associate_children(Sidebar.Branch branch, Sidebar.Entry parent, Gtk.TreeIter parent_iter) { Gee.List? children = branch.get_children(parent); if (children == null) return; foreach (Sidebar.Entry child in children) { Gtk.TreeIter append_iter; store.append(out append_iter, parent_iter); associate_entry(append_iter, child); associate_children(branch, child, append_iter); } } private void associate_entry(Gtk.TreeIter assoc_iter, Sidebar.Entry entry) { associate_wrapper(assoc_iter, new EntryWrapper(store, entry, store.get_path(assoc_iter))); } private void associate_wrapper(Gtk.TreeIter assoc_iter, EntryWrapper wrapper) { Sidebar.Entry entry = wrapper.entry; assert(!entry_map.has_key(entry)); entry_map.set(entry, wrapper); store.set(assoc_iter, Columns.NAME, get_name_for_entry(entry)); store.set(assoc_iter, Columns.TOOLTIP, guarded_markup_escape_text(entry.get_sidebar_tooltip())); store.set(assoc_iter, Columns.WRAPPER, wrapper); load_entry_icons(assoc_iter); entry.sidebar_tooltip_changed.connect(on_sidebar_tooltip_changed); entry.sidebar_icon_changed.connect(on_sidebar_icon_changed); Sidebar.PageRepresentative? pageable = entry as Sidebar.PageRepresentative; if (pageable != null) { pageable.page_created.connect(on_sidebar_page_created); pageable.destroying_page.connect(on_sidebar_destroying_page); } Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry; if (emphasizable != null) emphasizable.is_emphasized_changed.connect(on_is_emphasized_changed); Sidebar.RenameableEntry? renameable = entry as Sidebar.RenameableEntry; if (renameable != null) renameable.sidebar_name_changed.connect(on_sidebar_name_changed); entry.grafted(this); } private EntryWrapper reparent_wrapper(Gtk.TreeIter new_iter, EntryWrapper current_wrapper) { Sidebar.Entry entry = current_wrapper.entry; bool removed = entry_map.unset(entry); assert(removed); EntryWrapper new_wrapper = new EntryWrapper(store, entry, store.get_path(new_iter)); entry_map.set(entry, new_wrapper); store.set(new_iter, Columns.NAME, get_name_for_entry(entry)); store.set(new_iter, Columns.TOOLTIP, guarded_markup_escape_text(entry.get_sidebar_tooltip())); store.set(new_iter, Columns.WRAPPER, new_wrapper); load_entry_icons(new_iter); return new_wrapper; } public void prune(Sidebar.Branch branch) { assert(branches.has_key(branch)); if (has_wrapper(branch.get_root())) disassociate_branch(branch); branch.entry_added.disconnect(on_branch_entry_added); branch.entry_removed.disconnect(on_branch_entry_removed); branch.entry_moved.disconnect(on_branch_entry_moved); branch.entry_reparented.disconnect(on_branch_entry_reparented); branch.children_reordered.disconnect(on_branch_children_reordered); branch.show_branch.disconnect(on_show_branch); bool removed = branches.unset(branch); assert(removed); branch_removed(branch); } private void disassociate_branch(Sidebar.Branch branch) { RootWrapper? root_wrapper = get_wrapper(branch.get_root()) as RootWrapper; assert(root_wrapper != null); disassociate_wrapper_and_signal(root_wrapper, false); } // A wrapper for disassociate_wrapper() (?!?) that fires the "selected-entry-removed" signal if // condition exists private void disassociate_wrapper_and_signal(EntryWrapper wrapper, bool only_children) { bool selected = is_selected(wrapper.entry); disassociate_wrapper(wrapper, only_children); if (selected) { Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry; assert(selectable != null); selected_entry_removed(selectable); } } private void disassociate_wrapper(EntryWrapper wrapper, bool only_children) { Gee.ArrayList children = new Gee.ArrayList(); Gtk.TreeIter child_iter; bool found = store.iter_children(out child_iter, wrapper.get_iter()); while (found) { EntryWrapper? child_wrapper = get_wrapper_at_iter(child_iter); assert(child_wrapper != null); children.add(child_wrapper); found = store.iter_next(ref child_iter); } foreach (EntryWrapper child_wrapper in children) disassociate_wrapper(child_wrapper, false); if (only_children) return; Gtk.TreeIter iter = wrapper.get_iter(); store.remove(ref iter); if (selected_wrapper == wrapper) selected_wrapper = null; Sidebar.Entry entry = wrapper.entry; entry.pruned(this); entry.sidebar_tooltip_changed.disconnect(on_sidebar_tooltip_changed); entry.sidebar_icon_changed.disconnect(on_sidebar_icon_changed); Sidebar.PageRepresentative? pageable = entry as Sidebar.PageRepresentative; if (pageable != null) { pageable.page_created.disconnect(on_sidebar_page_created); pageable.destroying_page.disconnect(on_sidebar_destroying_page); } Sidebar.RenameableEntry? renameable = entry as Sidebar.RenameableEntry; if (renameable != null) renameable.sidebar_name_changed.disconnect(on_sidebar_name_changed); Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry; if (emphasizable != null) emphasizable.is_emphasized_changed.disconnect(on_is_emphasized_changed); bool removed = entry_map.unset(entry); assert(removed); } private void on_branch_entry_added(Sidebar.Branch branch, Sidebar.Entry entry) { Sidebar.Entry? parent = branch.get_parent(entry); assert(parent != null); EntryWrapper? parent_wrapper = get_wrapper(parent); assert(parent_wrapper != null); Gtk.TreeIter insertion_iter; Sidebar.Entry? next = branch.get_next_sibling(entry); if (next != null) { EntryWrapper next_wrapper = get_wrapper(next); // insert before the next sibling in this branch level store.insert_before(out insertion_iter, parent_wrapper.get_iter(), next_wrapper.get_iter()); } else { // append to the bottom of this branch level store.append(out insertion_iter, parent_wrapper.get_iter()); } associate_entry(insertion_iter, entry); associate_children(branch, entry, insertion_iter); if (branch.is_auto_open_on_new_child()) expand_to_entry(entry); } private void on_branch_entry_removed(Sidebar.Branch branch, Sidebar.Entry entry) { EntryWrapper? wrapper = get_wrapper(entry); assert(wrapper != null); assert(!(wrapper is RootWrapper)); disassociate_wrapper_and_signal(wrapper, false); } private void on_branch_entry_moved(Sidebar.Branch branch, Sidebar.Entry entry) { EntryWrapper? wrapper = get_wrapper(entry); assert(wrapper != null); assert(!(wrapper is RootWrapper)); // null means entry is now at the top of the sibling list Gtk.TreeIter? prev_iter = null; Sidebar.Entry? prev = branch.get_previous_sibling(entry); if (prev != null) { EntryWrapper? prev_wrapper = get_wrapper(prev); assert(prev_wrapper != null); prev_iter = prev_wrapper.get_iter(); } Gtk.TreeIter entry_iter = wrapper.get_iter(); store.move_after(ref entry_iter, prev_iter); } private void on_branch_entry_reparented(Sidebar.Branch branch, Sidebar.Entry entry, Sidebar.Entry old_parent) { EntryWrapper? wrapper = get_wrapper(entry); assert(wrapper != null); assert(!(wrapper is RootWrapper)); bool selected = (get_current_path().compare(wrapper.get_path()) == 0); // remove from current position in tree Gtk.TreeIter iter = wrapper.get_iter(); store.remove(ref iter); Sidebar.Entry? parent = branch.get_parent(entry); assert(parent != null); EntryWrapper? parent_wrapper = get_wrapper(parent); assert(parent_wrapper != null); // null means entry is now at the top of the sibling list Gtk.TreeIter? prev_iter = null; Sidebar.Entry? prev = branch.get_previous_sibling(entry); if (prev != null) { EntryWrapper? prev_wrapper = get_wrapper(prev); assert(prev_wrapper != null); prev_iter = prev_wrapper.get_iter(); } Gtk.TreeIter new_iter; store.insert_after(out new_iter, parent_wrapper.get_iter(), prev_iter); EntryWrapper new_wrapper = reparent_wrapper(new_iter, wrapper); if (selected) { expand_to_entry(new_wrapper.entry); place_cursor(new_wrapper.entry, false); } } private void on_branch_children_reordered(Sidebar.Branch branch, Sidebar.Entry entry) { Gee.List? children = branch.get_children(entry); if (children == null) return; // This works by moving the entries to the bottom of the tree's list in the order they // are presented in the Sidebar.Branch list. foreach (Sidebar.Entry child in children) { EntryWrapper? child_wrapper = get_wrapper(child); assert(child_wrapper != null); Gtk.TreeIter child_iter = child_wrapper.get_iter(); store.move_before(ref child_iter, null); } } private void on_show_branch(Sidebar.Branch branch, bool shown) { if (shown) associate_branch(branch); else disassociate_branch(branch); branch_shown(branch, shown); } private void on_sidebar_tooltip_changed(Sidebar.Entry entry, string? tooltip) { EntryWrapper? wrapper = get_wrapper(entry); assert(wrapper != null); store.set(wrapper.get_iter(), Columns.TOOLTIP, guarded_markup_escape_text(tooltip)); } private void on_sidebar_icon_changed(Sidebar.Entry entry, string? icon) { EntryWrapper? wrapper = get_wrapper(entry); assert(wrapper != null); store.set(wrapper.get_iter(), Columns.ICON, icon); } private void rename_entry(Sidebar.Entry entry) { EntryWrapper? wrapper = get_wrapper(entry); assert(wrapper != null); store.set(wrapper.get_iter(), Columns.NAME, get_name_for_entry(entry)); } private void on_sidebar_name_changed(Sidebar.Entry entry, string name) { rename_entry(entry); } private void on_sidebar_page_created(Sidebar.PageRepresentative entry, Page page) { page_created(entry, page); } private void on_is_emphasized_changed(Sidebar.EmphasizableEntry entry, bool is_emphasized) { rename_entry(entry); } private void on_sidebar_destroying_page(Sidebar.PageRepresentative entry, Page page) { destroying_page(entry, page); } private void load_entry_icons(Gtk.TreeIter iter) { EntryWrapper? wrapper = get_wrapper_at_iter(iter); if (wrapper == null) return; string? icon = wrapper.entry.get_sidebar_icon(); store.set(iter, Columns.ICON, icon); } private void load_branch_icons(Gtk.TreeIter iter) { load_entry_icons(iter); Gtk.TreeIter child_iter; if (store.iter_children(out child_iter, iter)) { do { load_branch_icons(child_iter); } while (store.iter_next(ref child_iter)); } } private bool on_selection(Gtk.TreeSelection selection, Gtk.TreeModel model, Gtk.TreePath path, bool path_currently_selected) { // only allow selection if a page is selectable EntryWrapper? wrapper = get_wrapper_at_path(path); return (wrapper != null) ? (wrapper.entry is Sidebar.SelectableEntry) : false; } private Gtk.TreePath? get_path_from_event(Gdk.EventButton event) { int x, y; Gdk.ModifierType mask; event.window.get_device_position(Gdk.Display.get_default().get_device_manager(). get_client_pointer(), out x, out y, out mask); int cell_x, cell_y; Gtk.TreePath path; return get_path_at_pos(x, y, out path, null, out cell_x, out cell_y) ? path : null; } private Gtk.TreePath? get_current_path() { Gtk.TreeModel model; GLib.List rows = get_selection().get_selected_rows(out model); assert(rows.length() == 0 || rows.length() == 1); return rows.length() != 0 ? rows.nth_data(0) : null; } private bool on_context_menu_keypress() { GLib.List rows = get_selection().get_selected_rows(null); if (rows == null) return false; Gtk.TreePath? path = rows.data; if (path == null) return false; scroll_to_cell(path, null, false, 0, 0); return popup_context_menu(path); } private bool popup_context_menu(Gtk.TreePath path, Gdk.EventButton? event = null) { EntryWrapper? wrapper = get_wrapper_at_path(path); if (wrapper == null) return false; Sidebar.Contextable? contextable = wrapper.entry as Sidebar.Contextable; if (contextable == null) return false; // First select the sidebar item so that its context menu will be available. Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry; if (selectable != null) entry_selected(selectable); Gtk.Menu? context_menu = contextable.get_sidebar_context_menu(event); if (context_menu == null) return false; if (event != null) context_menu.popup(null, null, null, event.button, event.time); else context_menu.popup(null, null, null, 0, Gtk.get_current_event_time()); return true; } private bool popup_default_context_menu(Gdk.EventButton event) { default_context_menu.popup(null, null, null, event.button, event.time); return true; } public bool on_toggle_row(Gtk.TreeIter iter, Gtk.TreePath path) { // Determine whether to allow the row to toggle EntryWrapper? wrapper = get_wrapper_at_iter(iter); if (wrapper == null) { return false; // don't affect things } // Most of the time, only allow manual toggles bool should_allow_toggle = expander_called_manually; // Cancel out the manual flag expander_called_manually = false; // If we are an expanded parent entry with content if (is_row_expanded(path) && store.iter_has_child(iter) && wrapper.entry is Sidebar.SelectableEntry) { // We are taking a special action expander_special_count++; if (expander_special_count == 1) { // Workaround that prevents arrows from double-toggling return true; } else { // Toggle only if non-manual, as opposed to the usual behavior should_allow_toggle = !should_allow_toggle; } } else { // Reset the special behavior count expander_special_count = 0; } if (should_allow_toggle) { return false; } // Prevent branch expansion toggle return true; } public override bool button_press_event(Gdk.EventButton event) { Gtk.TreePath? path = get_path_from_event(event); // user clicked on empty area, but isn't trying to spawn a context menu? if ((path == null) && (event.button != 3)) { return true; } if (event.button == 3 && event.type == Gdk.EventType.BUTTON_PRESS) { // single right click if (path != null) popup_context_menu(path, event); else popup_default_context_menu(event); } else if (event.button == 1 && event.type == Gdk.EventType.BUTTON_PRESS) { if (path == null) { old_path_ref = null; return base.button_press_event(event); } EntryWrapper? wrapper = get_wrapper_at_path(path); if (wrapper == null) { old_path_ref = null; return base.button_press_event(event); } // Enable single click to toggle tree entries (bug 4985) if (wrapper.entry is Sidebar.ExpandableEntry || wrapper.entry is Sidebar.InternalDropTargetEntry) { // all labels are InternalDropTargetEntries toggle_branch_expansion(path, false); } // Is this a click on an already-highlighted tree item? if ((old_path_ref != null) && (old_path_ref.get_path() != null) && (old_path_ref.get_path().compare(path) == 0)) { // yes, don't allow single-click editing, but // pass the event on for dragging. text_renderer.editable = false; return base.button_press_event(event); } // Got click on different tree item, make sure it is editable // if it needs to be. if (wrapper.entry is Sidebar.RenameableEntry && ((Sidebar.RenameableEntry) wrapper.entry).is_user_renameable()) { text_renderer.editable = true; } // Remember what tree item is highlighted for next time. old_path_ref = new Gtk.TreeRowReference(store, path); } return base.button_press_event(event); } public bool is_keypress_interpreted(Gdk.EventKey event) { switch (Gdk.keyval_name(event.keyval)) { case "F2": case "Delete": case "Return": case "KP_Enter": return true; default: return false; } } public override bool key_press_event(Gdk.EventKey event) { switch (Gdk.keyval_name(event.keyval)) { case "Return": case "KP_Enter": Gtk.TreePath? path = get_current_path(); if (path != null) toggle_branch_expansion(path, false); return true; case "F2": return rename_in_place(); case "Delete": Gtk.TreePath? path = get_current_path(); return (path != null) ? destroy_path(path) : false; } return base.key_press_event(event); } public bool rename_entry_in_place(Sidebar.Entry entry) { if (!expand_to_entry(entry)) return false; if (!place_cursor(entry, false)) return false; return rename_in_place(); } private bool rename_in_place() { Gtk.TreePath? cursor_path; Gtk.TreeViewColumn? cursor_column; get_cursor(out cursor_path, out cursor_column); if (can_rename_path(cursor_path)) { set_cursor_on_cell (cursor_path, cursor_column, text_renderer, true); return true; } return false; } public bool scroll_to_entry(Sidebar.Entry entry) { EntryWrapper? wrapper = get_wrapper(entry); if (wrapper == null) return false; scroll_to_cell(wrapper.get_path(), null, false, 0, 0); return true; } public override void drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data, uint info, uint time) { InternalDragSourceEntry? drag_source = null; if (internal_drag_source_entry != null) { Sidebar.SelectableEntry selectable = internal_drag_source_entry as Sidebar.SelectableEntry; if (selectable == null) { drag_source = internal_drag_source_entry as InternalDragSourceEntry; } } if (drag_source == null) { Gtk.TreePath? selected_path = get_selected_path(); if (selected_path == null) return; EntryWrapper? wrapper = get_wrapper_at_path(selected_path); if (wrapper == null) return; drag_source = wrapper.entry as InternalDragSourceEntry; if (drag_source == null) return; } drag_source.prepare_selection_data(selection_data); } public override void drag_data_received(Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time) { Gtk.TreePath path; Gtk.TreeViewDropPosition pos; if (!get_dest_row_at_pos(x, y, out path, out pos)) { // If an external drop, hand it off to the handler if (Gtk.drag_get_source_widget(context) == null) drop_handler(context, null, selection_data, info, time); else Gtk.drag_finish(context, false, false, time); return; } // Note that a drop outside a sidebar entry is legal if an external drop. EntryWrapper? wrapper = get_wrapper_at_path(path); // If an external drop, hand it off to the handler if (Gtk.drag_get_source_widget(context) == null) { drop_handler(context, (wrapper != null) ? wrapper.entry : null, selection_data, info, time); return; } // An internal drop only applies to DropTargetEntry's if (wrapper == null) { Gtk.drag_finish(context, false, false, time); return; } Sidebar.InternalDropTargetEntry? targetable = wrapper.entry as Sidebar.InternalDropTargetEntry; if (targetable == null) { Gtk.drag_finish(context, false, false, time); return; } bool success = false; if (selection_data.get_data_type().name() == LibraryWindow.TAG_PATH_MIME_TYPE) { success = targetable.internal_drop_received_arbitrary(selection_data); } else { Gee.List? media = unserialize_media_sources(selection_data.get_data(), selection_data.get_length()); if (media != null && media.size > 0) success = targetable.internal_drop_received(media); } Gtk.drag_finish(context, success, false, time); } public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) { // call the base signal to get rows with children to spring open base.drag_motion(context, x, y, time); Gtk.TreePath path; Gtk.TreeViewDropPosition pos; bool has_dest = get_dest_row_at_pos(x, y, out path, out pos); // we don't want to insert between rows, only select the rows themselves if (!has_dest || pos == Gtk.TreeViewDropPosition.BEFORE) set_drag_dest_row(path, Gtk.TreeViewDropPosition.INTO_OR_BEFORE); else if (pos == Gtk.TreeViewDropPosition.AFTER) set_drag_dest_row(path, Gtk.TreeViewDropPosition.INTO_OR_AFTER); Gdk.drag_status(context, context.get_suggested_action(), time); return has_dest; } // Returns true if path is renameable, and selects the path as well. private bool can_rename_path(Gtk.TreePath path) { if (editing_disabled > 0) return false; EntryWrapper? wrapper = get_wrapper_at_path(path); if (wrapper == null) return false; Sidebar.RenameableEntry? renameable = wrapper.entry as Sidebar.RenameableEntry; if (renameable == null) return false; if (wrapper.entry is Sidebar.Header) return false; get_selection().select_path(path); return true; } private bool destroy_path(Gtk.TreePath path) { EntryWrapper? wrapper = get_wrapper_at_path(path); if (wrapper == null) return false; Sidebar.DestroyableEntry? destroyable = wrapper.entry as Sidebar.DestroyableEntry; if (destroyable == null) return false; destroyable.destroy_source(); return true; } private void on_editing_started(Gtk.CellEditable editable, string path) { if (editable is Gtk.Entry) { text_entry = (Gtk.Entry) editable; text_entry.editing_done.connect(on_editing_done); text_entry.focus_out_event.connect(on_editing_focus_out); text_entry.editable = true; } } private void on_editing_canceled() { text_entry.editable = false; text_entry.editing_done.disconnect(on_editing_done); text_entry.focus_out_event.disconnect(on_editing_focus_out); } private void on_editing_done() { text_entry.editable = false; EntryWrapper? wrapper = get_wrapper_at_path(get_current_path()); if (wrapper != null) { Sidebar.RenameableEntry? renameable = wrapper.entry as Sidebar.RenameableEntry; if (renameable != null) renameable.rename(text_entry.get_text()); } text_entry.editing_done.disconnect(on_editing_done); text_entry.focus_out_event.disconnect(on_editing_focus_out); } private bool on_editing_focus_out(Gdk.EventFocus event) { // We'll return false here, in case other parts of the app // want to know if the button press event that caused // us to lose focus have been fully handled. return false; } private void on_new_search() { (new SavedSearchDialog()).show(); } private void on_new_tag() { NewRootTagCommand creation_command = new NewRootTagCommand(); AppWindow.get_command_manager().execute(creation_command); LibraryWindow.get_app().rename_tag_in_sidebar(creation_command.get_created_tag()); } }