diff options
Diffstat (limited to 'src/sidebar/Tree.vala')
-rw-r--r-- | src/sidebar/Tree.vala | 1175 |
1 files changed, 1175 insertions, 0 deletions
diff --git a/src/sidebar/Tree.vala b/src/sidebar/Tree.vala new file mode 100644 index 0000000..37da7e0 --- /dev/null +++ b/src/sidebar/Tree.vala @@ -0,0 +1,1175 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * 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) + requires (root_position >= 0) { + base (model, entry, path); + + this.root_position = root_position; + } + } + + private enum Columns { + NAME, + TOOLTIP, + WRAPPER, + PIXBUF, + CLOSED_PIXBUF, + OPEN_PIXBUF, + N_COLUMNS + } + + private Gtk.TreeStore store = new Gtk.TreeStore(Columns.N_COLUMNS, + typeof (string), // NAME + typeof (string?), // TOOLTIP + typeof (EntryWrapper), // WRAPPER + typeof (Gdk.Pixbuf?), // PIXBUF + typeof (Gdk.Pixbuf?), // CLOSED_PIXBUF + typeof (Gdk.Pixbuf?) // OPEN_PIXBUF + ); + + private Gtk.UIManager ui = new Gtk.UIManager(); + private Gtk.IconTheme icon_theme; + private Gtk.CellRendererText text_renderer; + private unowned ExternalDropHandler drop_handler; + private Gtk.Entry? text_entry = null; + private Gee.HashMap<string, Gdk.Pixbuf> icon_cache = new Gee.HashMap<string, Gdk.Pixbuf>(); + private Gee.HashMap<Sidebar.Entry, EntryWrapper> entry_map = + new Gee.HashMap<Sidebar.Entry, EntryWrapper>(); + private Gee.HashMap<Sidebar.Branch, int> branches = new Gee.HashMap<Sidebar.Branch, int>(); + 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 is_internal_drag_in_progress = false; + private Sidebar.Entry? internal_drag_source_entry = 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); + + Gtk.TreeViewColumn text_column = new Gtk.TreeViewColumn(); + text_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED); + Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf(); + text_column.pack_start(icon_renderer, false); + text_column.add_attribute(icon_renderer, "pixbuf", Columns.PIXBUF); + text_column.add_attribute(icon_renderer, "pixbuf_expander_closed", Columns.CLOSED_PIXBUF); + text_column.add_attribute(icon_renderer, "pixbuf_expander_open", Columns.OPEN_PIXBUF); + text_renderer = new Gtk.CellRendererText(); + 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); + + // 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); + + icon_theme = Resources.get_icon_theme_engine(); + icon_theme.changed.connect(on_theme_change); + + 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); + icon_theme.changed.disconnect(on_theme_change); + } + + 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); + + return (wrapper != null) ? get_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; + GLib.List<Gtk.TreePath> rows = get_selection().get_selected_rows(out model); + assert(rows.length() == 0 || rows.length() == 1); + + return rows.length() != 0 ? rows.nth_data(0) : null; + } + + 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); + + selected_wrapper = wrapper; + + if (editing_disabled == 0 && wrapper != null) + text_renderer.editable = wrapper.entry is Sidebar.RenameableEntry; + + if (wrapper != null && !mask_entry_selected_signal) { + Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry; + if (selectable != null) + entry_selected(selectable); + } + + 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); + text_renderer.editable = (wrapper != null && (wrapper.entry is Sidebar.RenameableEntry)); + } + } + + public void toggle_branch_expansion(Gtk.TreePath path, bool expand_all) { + if (is_row_expanded(path)) + collapse_row(path); + else + expand_row(path, expand_all); + } + + public bool expand_to_entry(Sidebar.Entry entry) { + 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) { + 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<Sidebar.Entry>? 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, guarded_markup_escape_text(entry.get_sidebar_name())); + 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.RenameableEntry? renameable = entry as Sidebar.RenameableEntry; + if (renameable != null) + renameable.sidebar_name_changed.connect(on_sidebar_name_changed); + + Sidebar.ExpandableEntry? expandable = entry as Sidebar.ExpandableEntry; + if (expandable != null) + expandable.sidebar_open_closed_icons_changed.connect(on_sidebar_open_closed_icons_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, guarded_markup_escape_text(entry.get_sidebar_name())); + 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<EntryWrapper> children = new Gee.ArrayList<EntryWrapper>(); + + 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.ExpandableEntry? expandable = entry as Sidebar.ExpandableEntry; + if (expandable != null) + expandable.sidebar_open_closed_icons_changed.disconnect(on_sidebar_open_closed_icons_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<Sidebar.Entry>? 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, Icon? icon) { + EntryWrapper? wrapper = get_wrapper(entry); + assert(wrapper != null); + + store.set(wrapper.get_iter(), Columns.PIXBUF, fetch_icon_pixbuf(icon)); + } + + private void on_sidebar_page_created(Sidebar.PageRepresentative entry, Page page) { + page_created(entry, page); + } + + private void on_sidebar_destroying_page(Sidebar.PageRepresentative entry, Page page) { + destroying_page(entry, page); + } + + private void on_sidebar_open_closed_icons_changed(Sidebar.ExpandableEntry entry, Icon? open, + Icon? closed) { + EntryWrapper? wrapper = get_wrapper(entry); + assert(wrapper != null); + + store.set(wrapper.get_iter(), Columns.OPEN_PIXBUF, fetch_icon_pixbuf(open)); + store.set(wrapper.get_iter(), Columns.CLOSED_PIXBUF, fetch_icon_pixbuf(closed)); + } + + private void on_sidebar_name_changed(Sidebar.RenameableEntry entry, string name) { + EntryWrapper? wrapper = get_wrapper(entry); + assert(wrapper != null); + + store.set(wrapper.get_iter(), Columns.NAME, guarded_markup_escape_text(name)); + } + + private Gdk.Pixbuf? fetch_icon_pixbuf(GLib.Icon? gicon) { + if (gicon == null) + return null; + + try { + Gdk.Pixbuf? icon = icon_cache.get(gicon.to_string()); + if (icon != null) + return icon; + + Gtk.IconInfo? info = icon_theme.lookup_by_gicon(gicon, ICON_SIZE, 0); + if (info == null) + return null; + + icon = info.load_icon(); + if (icon == null) + return null; + + icon_cache.set(gicon.to_string(), icon); + + return icon; + } catch (Error err) { + warning("Unable to load icon %s: %s", gicon.to_string(), err.message); + + return null; + } + } + + private void load_entry_icons(Gtk.TreeIter iter) { + EntryWrapper? wrapper = get_wrapper_at_iter(iter); + if (wrapper == null) + return; + + Icon? icon = wrapper.entry.get_sidebar_icon(); + Icon? open = null; + Icon? closed = null; + + Sidebar.ExpandableEntry? expandable = wrapper.entry as Sidebar.ExpandableEntry; + if (expandable != null) { + open = expandable.get_sidebar_open_icon(); + closed = expandable.get_sidebar_closed_icon(); + } + + if (open == null) + open = icon; + + if (closed == null) + closed = icon; + + store.set(iter, Columns.PIXBUF, fetch_icon_pixbuf(icon)); + store.set(iter, Columns.OPEN_PIXBUF, fetch_icon_pixbuf(open)); + store.set(iter, Columns.CLOSED_PIXBUF, fetch_icon_pixbuf(closed)); + } + + 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 void on_theme_change() { + Gtk.TreeIter iter; + if (store.get_iter_first(out iter)) { + do { + load_branch_icons(iter); + } while (store.iter_next(ref 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<Gtk.TreePath> 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<Gtk.TreePath> 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 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.2BUTTON_PRESS) { + // double left click + if (path != null) { + toggle_branch_expansion(path, false); + + if (can_rename_path(path)) + return false; + } + } else if (event.button == 1 && event.type == Gdk.EventType.BUTTON_PRESS) { + // Is this a click on an already-highlighted tree item? + Gtk.TreePath? cursor_path = null; + get_cursor(out cursor_path, null); + if ((cursor_path != null) && (cursor_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 (path != null && get_wrapper_at_path(path).entry is Sidebar.RenameableEntry) { + text_renderer.editable = true; + } + } + + 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(cursor_path, cursor_column, 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<MediaSource>? 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; + + 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()); + } +} + |