diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
commit | 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch) | |
tree | d2e54377d14d604356c86862a326f64ae64dadd6 /src/sidebar |
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/sidebar')
-rw-r--r-- | src/sidebar/Branch.vala | 450 | ||||
-rw-r--r-- | src/sidebar/Entry.vala | 70 | ||||
-rw-r--r-- | src/sidebar/Sidebar.vala | 16 | ||||
-rw-r--r-- | src/sidebar/Tree.vala | 1175 | ||||
-rw-r--r-- | src/sidebar/common.vala | 114 | ||||
-rw-r--r-- | src/sidebar/mk/sidebar.mk | 31 |
6 files changed, 1856 insertions, 0 deletions
diff --git a/src/sidebar/Branch.vala b/src/sidebar/Branch.vala new file mode 100644 index 0000000..23badda --- /dev/null +++ b/src/sidebar/Branch.vala @@ -0,0 +1,450 @@ +/* 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 delegate bool Locator<G>(G item); + +public class Sidebar.Branch : Object { + [Flags] + public enum Options { + NONE = 0, + HIDE_IF_EMPTY, + AUTO_OPEN_ON_NEW_CHILD, + STARTUP_EXPAND_TO_FIRST_CHILD, + STARTUP_OPEN_GROUPING; + + public bool is_hide_if_empty() { + return (this & HIDE_IF_EMPTY) != 0; + } + + public bool is_auto_open_on_new_child() { + return (this & AUTO_OPEN_ON_NEW_CHILD) != 0; + } + + public bool is_startup_expand_to_first_child() { + return (this & STARTUP_EXPAND_TO_FIRST_CHILD) != 0; + } + + public bool is_startup_open_grouping() { + return (this & STARTUP_OPEN_GROUPING) != 0; + } + } + + private class Node { + public delegate void PruneCallback(Node node); + + public delegate void ChildrenReorderedCallback(Node node); + + public Sidebar.Entry entry; + public weak Node? parent; + public CompareDataFunc<Sidebar.Entry> comparator; + public Gee.SortedSet<Node>? children = null; + + public Node(Sidebar.Entry entry, Node? parent, + owned CompareDataFunc<Sidebar.Entry> comparator) { + this.entry = entry; + this.parent = parent; + this.comparator = (owned) comparator; + } + + private static int comparator_wrapper(Node? a, Node? b) { + if (a == b) + return 0; + + assert(a.parent == b.parent); + + return a.parent.comparator(a.entry, b.entry); + } + + public bool has_children() { + return (children != null && children.size > 0); + } + + public void add_child(Node child) { + child.parent = this; + + if (children == null) + children = new Gee.TreeSet<Node>(comparator_wrapper); + + bool added = children.add(child); + assert(added); + } + + public void remove_child(Node child) { + assert(children != null); + + Gee.SortedSet<Node> new_children = new Gee.TreeSet<Node>(comparator_wrapper); + + // For similar reasons as in reorder_child(), can't rely on Gee.TreeSet to locate this + // node because we need reference equality. + bool found = false; + foreach (Node c in children) { + if (c != child) + new_children.add(c); + else + found = true; + } + + assert(found); + + if (new_children.size != 0) + children = new_children; + else + children = null; + + child.parent = null; + } + + public void prune_children(PruneCallback cb) { + if (children == null) + return; + + foreach (Node child in children) + child.prune_children(cb); + + Gee.SortedSet<Node> old_children = children; + children = null; + + // Although this could've been done in the prior loop, it means notifying that + // a child has been removed prior to it being removed; this can cause problem + // if a signal handler calls back into the Tree to examine/add/remove nodes. + foreach (Node child in old_children) + cb(child); + } + + // This returns the index of the Node purely by reference equality, making it useful if + // the criteria the Node is sorted upon has changed. + public int index_of_by_reference(Node child) { + if (children == null) + return -1; + + int index = 0; + foreach (Node c in children) { + if (child == c) + return index; + + index++; + } + + return -1; + } + + // Returns true if child moved when reordered. + public bool reorder_child(Node child) { + assert(children != null); + + int old_index = index_of_by_reference(child); + assert(old_index >= 0); + + // Because Gee.SortedSet uses the comparator for equality, if the Node's entry state + // has changed in such a way that the item is no longer sorted properly, the SortedSet's + // search and remove methods are useless. Makes no difference if children.remove() is + // called or the set is manually iterated over and removed via the Iterator -- a + // tree search is performed and the child will not be found. Only easy solution is + // to rebuild a new SortedSet and see if the child has moved. + Gee.SortedSet<Node> new_children = new Gee.TreeSet<Node>(comparator_wrapper); + bool added = new_children.add_all(children); + assert(added); + + children = new_children; + + int new_index = index_of_by_reference(child); + assert(new_index >= 0); + + return (old_index != new_index); + } + + public void reorder_children(bool recursive, ChildrenReorderedCallback cb) { + if (children == null) + return; + + Gee.SortedSet<Node> reordered = new Gee.TreeSet<Node>(comparator_wrapper); + reordered.add_all(children); + children = reordered; + + if (recursive) { + foreach (Node child in children) + child.reorder_children(true, cb); + } + + cb(this); + } + + public void change_comparator(owned CompareDataFunc<Sidebar.Entry> comparator, bool recursive, + ChildrenReorderedCallback cb) { + this.comparator = (owned) comparator; + + // reorder children, but need to do manual recursion to set comparator + reorder_children(false, cb); + + if (recursive) { + foreach (Node child in children) + child.change_comparator((owned) comparator, true, cb); + } + } + } + + private Node root; + private Options options; + private bool shown = true; + private CompareDataFunc<Sidebar.Entry> default_comparator; + private Gee.HashMap<Sidebar.Entry, Node> map = new Gee.HashMap<Sidebar.Entry, Node>(); + + public signal void entry_added(Sidebar.Entry entry); + + public signal void entry_removed(Sidebar.Entry entry); + + public signal void entry_moved(Sidebar.Entry entry); + + public signal void entry_reparented(Sidebar.Entry entry, Sidebar.Entry old_parent); + + public signal void children_reordered(Sidebar.Entry entry); + + public signal void show_branch(bool show); + + public Branch(Sidebar.Entry root, Options options, + owned CompareDataFunc<Sidebar.Entry> default_comparator, + owned CompareDataFunc<Sidebar.Entry>? root_comparator = null) { + this.default_comparator = (owned) default_comparator; + + CompareDataFunc<Sidebar.Entry>? broken_ternary_workaround; + + if (root_comparator != null) + broken_ternary_workaround = (owned) root_comparator; + else + broken_ternary_workaround = (owned) default_comparator; + + this.root = new Node(root, null, (owned) broken_ternary_workaround); + this.options = options; + + map.set(root, this.root); + + if (options.is_hide_if_empty()) + set_show_branch(false); + } + + public Sidebar.Entry get_root() { + return root.entry; + } + + public void set_show_branch(bool shown) { + if (this.shown == shown) + return; + + this.shown = shown; + show_branch(shown); + } + + public bool get_show_branch() { + return shown; + } + + public bool is_auto_open_on_new_child() { + return options.is_auto_open_on_new_child(); + } + + public bool is_startup_expand_to_first_child() { + return options.is_startup_expand_to_first_child(); + } + + public bool is_startup_open_grouping() { + return options.is_startup_open_grouping(); + } + + public void graft(Sidebar.Entry parent, Sidebar.Entry entry, + owned CompareDataFunc<Sidebar.Entry>? comparator = null) { + assert(map.has_key(parent)); + assert(!map.has_key(entry)); + + if (options.is_hide_if_empty()) + set_show_branch(true); + + Node parent_node = map.get(parent); + + CompareDataFunc<Sidebar.Entry>? broken_ternary_workaround; + + if (comparator != null) + broken_ternary_workaround = (owned) comparator; + else + broken_ternary_workaround = (owned) default_comparator; + + Node entry_node = new Node(entry, parent_node, (owned) broken_ternary_workaround); + + parent_node.add_child(entry_node); + map.set(entry, entry_node); + + entry_added(entry); + } + + // Cannot prune the root. The Branch should simply be removed from the Tree. + public void prune(Sidebar.Entry entry) { + assert(entry != root.entry); + assert(map.has_key(entry)); + + Node entry_node = map.get(entry); + + entry_node.prune_children(prune_callback); + + assert(entry_node.parent != null); + entry_node.parent.remove_child(entry_node); + + bool removed = map.unset(entry); + assert(removed); + + entry_removed(entry); + + if (options.is_hide_if_empty() && !root.has_children()) + set_show_branch(false); + } + + // Cannot reparent the root. + public void reparent(Sidebar.Entry new_parent, Sidebar.Entry entry) { + assert(entry != root.entry); + assert(map.has_key(entry)); + assert(map.has_key(new_parent)); + + Node entry_node = map.get(entry); + Node new_parent_node = map.get(new_parent); + + assert(entry_node.parent != null); + Sidebar.Entry old_parent = entry_node.parent.entry; + + entry_node.parent.remove_child(entry_node); + new_parent_node.add_child(entry_node); + + entry_reparented(entry, old_parent); + } + + public bool has_entry(Sidebar.Entry entry) { + return (root.entry == entry || map.has_key(entry)); + } + + // Call when a value related to the comparison of this entry has changed. The root cannot be + // reordered. + public void reorder(Sidebar.Entry entry) { + assert(entry != root.entry); + + Node? entry_node = map.get(entry); + assert(entry_node != null); + + assert(entry_node.parent != null); + if (entry_node.parent.reorder_child(entry_node)) + entry_moved(entry); + } + + // Call when the entire tree needs to be reordered. + public void reorder_all() { + root.reorder_children(true, children_reordered_callback); + } + + // Call when the children of the entry need to be reordered. + public void reorder_children(Sidebar.Entry entry, bool recursive) { + Node? entry_node = map.get(entry); + assert(entry_node != null); + + entry_node.reorder_children(recursive, children_reordered_callback); + } + + public void change_all_comparators(owned CompareDataFunc<Sidebar.Entry>? comparator) { + root.change_comparator((owned) comparator, true, children_reordered_callback); + } + + public void change_comparator(Sidebar.Entry entry, bool recursive, + owned CompareDataFunc<Sidebar.Entry>? comparator) { + Node? entry_node = map.get(entry); + assert(entry_node != null); + + entry_node.change_comparator((owned) comparator, recursive, children_reordered_callback); + } + + public int get_child_count(Sidebar.Entry parent) { + Node? parent_node = map.get(parent); + assert(parent_node != null); + + return (parent_node.children != null) ? parent_node.children.size : 0; + } + + // Gets a snapshot of the children of the entry; this list will not be changed as the + // branch is updated. + public Gee.List<Sidebar.Entry>? get_children(Sidebar.Entry parent) { + assert(map.has_key(parent)); + + Node parent_node = map.get(parent); + if (parent_node.children == null) + return null; + + Gee.List<Sidebar.Entry> child_entries = new Gee.ArrayList<Sidebar.Entry>(); + foreach (Node child in parent_node.children) + child_entries.add(child.entry); + + return child_entries; + } + + public Sidebar.Entry? find_first_child(Sidebar.Entry parent, Locator<Sidebar.Entry> locator) { + Node? parent_node = map.get(parent); + assert(parent_node != null); + + if (parent_node.children == null) + return null; + + foreach (Node child in parent_node.children) { + if (locator(child.entry)) + return child.entry; + } + + return null; + } + + // Returns null if entry is root; + public Sidebar.Entry? get_parent(Sidebar.Entry entry) { + if (entry == root.entry) + return null; + + Node? entry_node = map.get(entry); + assert(entry_node != null); + assert(entry_node.parent != null); + + return entry_node.parent.entry; + } + + // Returns null if entry is root; + public Sidebar.Entry? get_previous_sibling(Sidebar.Entry entry) { + if (entry == root.entry) + return null; + + Node? entry_node = map.get(entry); + assert(entry_node != null); + assert(entry_node.parent != null); + assert(entry_node.parent.children != null); + + Node? sibling = entry_node.parent.children.lower(entry_node); + + return (sibling != null) ? sibling.entry : null; + } + + // Returns null if entry is root; + public Sidebar.Entry? get_next_sibling(Sidebar.Entry entry) { + if (entry == root.entry) + return null; + + Node? entry_node = map.get(entry); + assert(entry_node != null); + assert(entry_node.parent != null); + assert(entry_node.parent.children != null); + + Node? sibling = entry_node.parent.children.higher(entry_node); + + return (sibling != null) ? sibling.entry : null; + } + + private void prune_callback(Node node) { + entry_removed(node.entry); + } + + private void children_reordered_callback(Node node) { + children_reordered(node.entry); + } +} + diff --git a/src/sidebar/Entry.vala b/src/sidebar/Entry.vala new file mode 100644 index 0000000..4162f21 --- /dev/null +++ b/src/sidebar/Entry.vala @@ -0,0 +1,70 @@ +/* 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 interface Sidebar.Entry : Object { + public signal void sidebar_tooltip_changed(string? tooltip); + + public signal void sidebar_icon_changed(Icon? icon); + + public abstract string get_sidebar_name(); + + public abstract string? get_sidebar_tooltip(); + + public abstract Icon? get_sidebar_icon(); + + public abstract string to_string(); + + internal virtual void grafted(Sidebar.Tree tree) { + } + + internal virtual void pruned(Sidebar.Tree tree) { + } +} + +public interface Sidebar.ExpandableEntry : Sidebar.Entry { + public signal void sidebar_open_closed_icons_changed(Icon? open, Icon? closed); + + public abstract Icon? get_sidebar_open_icon(); + + public abstract Icon? get_sidebar_closed_icon(); + + public abstract bool expand_on_select(); +} + +public interface Sidebar.SelectableEntry : Sidebar.Entry { +} + +public interface Sidebar.PageRepresentative : Sidebar.Entry, Sidebar.SelectableEntry { + // Fired after the page has been created + public signal void page_created(Page page); + + // Fired before the page is destroyed. + public signal void destroying_page(Page page); + + public abstract bool has_page(); + + public abstract Page get_page(); +} + +public interface Sidebar.RenameableEntry : Sidebar.Entry { + public signal void sidebar_name_changed(string name); + + public abstract void rename(string new_name); +} + +public interface Sidebar.DestroyableEntry : Sidebar.Entry { + public abstract void destroy_source(); +} + +public interface Sidebar.InternalDropTargetEntry : Sidebar.Entry { + // Returns true if drop was successful + public abstract bool internal_drop_received(Gee.List<MediaSource> sources); + public abstract bool internal_drop_received_arbitrary(Gtk.SelectionData data); +} + +public interface Sidebar.InternalDragSourceEntry : Sidebar.Entry { + public abstract void prepare_selection_data(Gtk.SelectionData data); +} diff --git a/src/sidebar/Sidebar.vala b/src/sidebar/Sidebar.vala new file mode 100644 index 0000000..8f6904b --- /dev/null +++ b/src/sidebar/Sidebar.vala @@ -0,0 +1,16 @@ +/* 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. + */ + +namespace Sidebar { + +public void init() throws Error { +} + +public void terminate() { +} + +} + 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()); + } +} + diff --git a/src/sidebar/common.vala b/src/sidebar/common.vala new file mode 100644 index 0000000..36adfff --- /dev/null +++ b/src/sidebar/common.vala @@ -0,0 +1,114 @@ +/* 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. + */ + +// A simple grouping Entry that is only expandable +public class Sidebar.Grouping : Object, Sidebar.Entry, Sidebar.ExpandableEntry { + private string name; + private Icon? open_icon; + private Icon? closed_icon; + + public Grouping(string name, Icon? open_icon, Icon? closed_icon = null) { + this.name = name; + this.open_icon = open_icon; + this.closed_icon = closed_icon ?? open_icon; + } + + public string get_sidebar_name() { + return name; + } + + public string? get_sidebar_tooltip() { + return name; + } + + public Icon? get_sidebar_icon() { + return null; + } + + public Icon? get_sidebar_open_icon() { + return open_icon; + } + + public Icon? get_sidebar_closed_icon() { + return closed_icon; + } + + public string to_string() { + return name; + } + + public bool expand_on_select() { + return true; + } +} + +// An end-node on the sidebar that represents a Page with its page context menu. Additional +// interfaces can be added if additional functionality is required (such as a drop target). +// This class also handles the bookwork of creating the Page on-demand and maintaining it in memory. +public abstract class Sidebar.SimplePageEntry : Object, Sidebar.Entry, Sidebar.SelectableEntry, + Sidebar.PageRepresentative, Sidebar.Contextable { + private Page? page = null; + + public SimplePageEntry() { + } + + public abstract string get_sidebar_name(); + + public virtual string? get_sidebar_tooltip() { + return get_sidebar_name(); + } + + public abstract Icon? get_sidebar_icon(); + + public virtual string to_string() { + return get_sidebar_name(); + } + + protected abstract Page create_page(); + + public bool has_page() { + return page != null; + } + + protected Page get_page() { + if (page == null) { + page = create_page(); + page_created(page); + } + + return page; + } + + internal void pruned(Sidebar.Tree tree) { + if (page == null) + return; + + destroying_page(page); + page.destroy(); + page = null; + } + + public Gtk.Menu? get_sidebar_context_menu(Gdk.EventButton? event) { + return get_page().get_page_context_menu(); + } +} + +// A simple Sidebar.Branch where the root node is the branch in entirety. +public class Sidebar.RootOnlyBranch : Sidebar.Branch { + public RootOnlyBranch(Sidebar.Entry root) { + base (root, Sidebar.Branch.Options.NONE, null_comparator); + } + + private static int null_comparator(Sidebar.Entry a, Sidebar.Entry b) { + return (a != b) ? -1 : 0; + } +} + +public interface Sidebar.Contextable : Object { + // Return null if the context menu should not be invoked for this event + public abstract Gtk.Menu? get_sidebar_context_menu(Gdk.EventButton? event); +} + diff --git a/src/sidebar/mk/sidebar.mk b/src/sidebar/mk/sidebar.mk new file mode 100644 index 0000000..2a6c67d --- /dev/null +++ b/src/sidebar/mk/sidebar.mk @@ -0,0 +1,31 @@ + +# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with +# a init() and terminate() function declared in the namespace. +UNIT_NAME := Sidebar + +# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all +# lowercase. The name of this file should be UNIT_DIR.mk. +UNIT_DIR := sidebar + +# All Vala files in the unit should be listed here with no subdirectory prefix. +# +# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala. +UNIT_FILES := \ + Branch.vala \ + Entry.vala \ + Tree.vala \ + common.vala + +# Any unit this unit relies upon (and should be initialized before it's initialized) should +# be listed here using its Vala namespace. +# +# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here. +UNIT_USES := + +# List any additional files that are used in the build process as a part of this unit that should +# be packaged in the tarball. File names should be relative to the unit's home directory. +UNIT_RC := + +# unitize.mk must be called at the end of each UNIT_DIR.mk file. +include unitize.mk + |