summaryrefslogtreecommitdiff
path: root/src/sidebar
diff options
context:
space:
mode:
Diffstat (limited to 'src/sidebar')
-rw-r--r--src/sidebar/Branch.vala450
-rw-r--r--src/sidebar/Entry.vala70
-rw-r--r--src/sidebar/Sidebar.vala16
-rw-r--r--src/sidebar/Tree.vala1175
-rw-r--r--src/sidebar/common.vala114
-rw-r--r--src/sidebar/mk/sidebar.mk31
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
+