geary/src/client/sidebar/sidebar-tree.vala
2015-02-06 12:43:33 -08:00

1173 lines
42 KiB
Vala

/* Copyright 2011-2015 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) {
base (model, entry, path);
this.root_position = root_position;
}
}
private enum Columns {
NAME,
TOOLTIP,
WRAPPER,
ICON,
COUNTER,
N_COLUMNS
}
private Gtk.TreeStore store = new Gtk.TreeStore(Columns.N_COLUMNS,
typeof (string), // NAME
typeof (string?), // TOOLTIP
typeof (EntryWrapper), // WRAPPER
typeof (string?), // ICON
typeof (int) // COUNTER
);
private Gtk.IconTheme? icon_theme;
private Gtk.CellRendererText text_renderer;
private unowned ExternalDropHandler drop_handler;
private Gtk.Entry? text_entry = null;
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 expander_called_manually = false;
private int expander_special_count = 0;
private bool is_internal_drag_in_progress = false;
private Sidebar.Entry? internal_drag_source_entry = null;
private Gtk.TreeRowReference? old_path_ref = null;
public signal void entry_selected(Sidebar.SelectableEntry selectable);
public signal void selected_entry_removed(Sidebar.SelectableEntry removed);
public signal void branch_added(Sidebar.Branch branch);
public signal void branch_removed(Sidebar.Branch branch);
public signal void branch_shown(Sidebar.Branch branch, bool shown);
public Tree(Gtk.TargetEntry[] target_entries, Gdk.DragAction actions,
ExternalDropHandler drop_handler, Gtk.IconTheme? theme = null) {
set_model(store);
icon_theme = theme;
get_style_context().add_class("sidebar");
Gtk.TreeViewColumn text_column = new Gtk.TreeViewColumn();
text_column.set_expand(true);
Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf();
icon_renderer.follow_state = true;
text_column.pack_start(icon_renderer, false);
text_column.add_attribute(icon_renderer, "icon_name", Columns.ICON);
text_column.set_cell_data_func(icon_renderer, icon_renderer_function);
text_renderer = new Gtk.CellRendererText();
text_renderer.ellipsize = Pango.EllipsizeMode.END;
text_renderer.editing_canceled.connect(on_editing_canceled);
text_renderer.editing_started.connect(on_editing_started);
text_column.pack_start(text_renderer, true);
text_column.add_attribute(text_renderer, "markup", Columns.NAME);
append_column(text_column);
// Count column.
Gtk.TreeViewColumn count_column = new Gtk.TreeViewColumn();
SidebarCountCellRenderer unread_renderer = new SidebarCountCellRenderer();
count_column.pack_start(unread_renderer, false);
count_column.add_attribute(unread_renderer, "counter", Columns.COUNTER);
append_column(count_column);
set_headers_visible(false);
set_enable_search(false);
set_search_column(-1);
set_rules_hint(false);
set_show_expanders(true);
set_reorderable(false);
set_enable_tree_lines(false);
set_grid_lines(Gtk.TreeViewGridLines.NONE);
set_tooltip_column(Columns.TOOLTIP);
Gtk.TreeSelection selection = get_selection();
selection.set_mode(Gtk.SelectionMode.BROWSE);
selection.set_select_function(on_selection);
test_expand_row.connect(on_toggle_row);
test_collapse_row.connect(on_toggle_row);
// It Would Be Nice if the target entries and actions were gleaned by querying each
// Sidebar.Entry as it was added, but that's a tad too complicated for our needs
// currently
enable_model_drag_dest(target_entries, actions);
// Drag source removed as per http://redmine.yorba.org/issues/4701
//
// Reason: this isn't working correctly (Sidebar.InternalDragSourceEntry should be
// the trigger for enable dragging, but that doesn't work anymore). It looks like
// the entire mechanism shifted somehow under GTK 3; see Gtk.TreeDragSource and
// Gtk.TreeDragDest for more information on how Sidebar should implement this
// properly
this.drop_handler = drop_handler;
popup_menu.connect(on_context_menu_keypress);
drag_begin.connect(on_drag_begin);
drag_end.connect(on_drag_end);
drag_motion.connect(on_drag_motion);
}
~Tree() {
text_renderer.editing_canceled.disconnect(on_editing_canceled);
text_renderer.editing_started.disconnect(on_editing_started);
}
public void icon_renderer_function(Gtk.CellLayout layout, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) {
EntryWrapper? wrapper = get_wrapper_at_iter(iter);
if (wrapper == null) {
return;
}
renderer.visible = !(wrapper.entry is Sidebar.Header);
}
public void counter_renderer_function(Gtk.CellLayout layout, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) {
EntryWrapper? wrapper = get_wrapper_at_iter(iter);
if (wrapper == null) {
return;
}
renderer.visible = !(wrapper.entry is Sidebar.Header);
}
private void on_drag_begin(Gdk.DragContext ctx) {
is_internal_drag_in_progress = true;
}
private void on_drag_end(Gdk.DragContext ctx) {
is_internal_drag_in_progress = false;
internal_drag_source_entry = null;
}
private bool on_drag_motion (Gdk.DragContext context, int x, int y, uint time_) {
if (is_internal_drag_in_progress && internal_drag_source_entry == null) {
Gtk.TreePath? path;
Gtk.TreeViewDropPosition position;
get_dest_row_at_pos(x, y, out path, out position);
if (path != null) {
EntryWrapper wrapper = get_wrapper_at_path(path);
if (wrapper != null)
internal_drag_source_entry = wrapper.entry;
}
}
return false;
}
private 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);
}
public void set_default_context_menu(Gtk.Menu context_menu) {
default_context_menu = context_menu;
}
// Note that this method will result in the "entry-selected" signal to fire if mask_signal
// is set to false.
public bool place_cursor(Sidebar.Entry entry, bool mask_signal) {
if (!expand_to_entry(entry))
return false;
EntryWrapper? wrapper = get_wrapper(entry);
if (wrapper == null)
return false;
get_selection().select_path(wrapper.get_path());
mask_entry_selected_signal = mask_signal;
set_cursor(wrapper.get_path(), null, false);
mask_entry_selected_signal = false;
return scroll_to_entry(entry);
}
public bool is_selected(Sidebar.Entry entry) {
EntryWrapper? wrapper = get_wrapper(entry);
// Even though get_selection() does not report its return type as nullable, it can be null
// if the window has been destroyed.
Gtk.TreeSelection selection = get_selection();
if (selection == null)
return false;
return (wrapper != null) ? selection.path_is_selected(wrapper.get_path()) : false;
}
public bool is_any_selected() {
return get_selection().count_selected_rows() != 0;
}
private Gtk.TreePath? get_selected_path() {
Gtk.TreeModel model;
Gtk.TreeSelection? selection = get_selection();
if (selection == null){
return null;
}
GLib.List<Gtk.TreePath> rows = selection.get_selected_rows(out model);
assert(rows.length() == 0 || rows.length() == 1);
return rows.length() != 0 ? rows.nth_data(0) : null;
}
private string get_name_for_entry(Sidebar.Entry entry) {
string name = Geary.HTML.escape_markup(entry.get_sidebar_name());
Sidebar.EmphasizableEntry? emphasizable_entry = entry as Sidebar.EmphasizableEntry;
if (emphasizable_entry != null && emphasizable_entry.is_emphasized())
name = "<b>%s</b>".printf(name);
return name;
}
public virtual bool accept_cursor_changed() {
return true;
}
public override void cursor_changed() {
Gtk.TreePath? path = get_selected_path();
if (path == null) {
if (base.cursor_changed != null)
base.cursor_changed();
return;
}
EntryWrapper? wrapper = get_wrapper_at_path(path);
if (selected_wrapper != wrapper) {
EntryWrapper old_wrapper = selected_wrapper;
selected_wrapper = wrapper;
if (editing_disabled == 0 && wrapper != null && wrapper.entry is Sidebar.RenameableEntry)
text_renderer.editable = ((Sidebar.RenameableEntry) wrapper.entry).is_user_renameable();
if (wrapper != null && !mask_entry_selected_signal) {
Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry;
if (selectable != null) {
if (accept_cursor_changed()) {
entry_selected(selectable);
} else {
place_cursor(old_wrapper.entry, true);
}
}
}
}
if (base.cursor_changed != null)
base.cursor_changed();
}
public void disable_editing() {
if (editing_disabled++ == 0)
text_renderer.editable = false;
}
public void enable_editing() {
Gtk.TreePath? path = get_selected_path();
if (path != null && editing_disabled > 0 && --editing_disabled == 0) {
EntryWrapper? wrapper = get_wrapper_at_path(path);
if (wrapper != null && (wrapper.entry is Sidebar.RenameableEntry))
text_renderer.editable = ((Sidebar.RenameableEntry) wrapper.entry).
is_user_renameable();
}
}
public void toggle_branch_expansion(Gtk.TreePath path, bool expand_all) {
expander_called_manually = true;
if (is_row_expanded(path))
collapse_row(path);
else
expand_row(path, expand_all);
}
public bool expand_to_entry(Sidebar.Entry entry) {
expander_called_manually = true;
EntryWrapper? wrapper = get_wrapper(entry);
if (wrapper == null)
return false;
expand_to_path(wrapper.get_path());
return true;
}
public void expand_to_first_child(Sidebar.Entry entry) {
expander_called_manually = true;
EntryWrapper? wrapper = get_wrapper(entry);
if (wrapper == null)
return;
Gtk.TreePath path = wrapper.get_path();
Gtk.TreeIter iter;
while (store.get_iter(out iter, path)) {
if (!store.iter_has_child(iter))
break;
path.down();
}
expand_to_path(path);
}
public bool has_branch(Sidebar.Branch branch) {
return branches.has_key(branch);
}
public void graft(Sidebar.Branch branch, int position) {
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);
}
public int get_position_for_branch(Sidebar.Branch branch) {
if (branches.has_key(branch))
return branches.get(branch);
return int.MIN;
}
// 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, get_name_for_entry(entry));
store.set(assoc_iter, Columns.TOOLTIP, entry.get_sidebar_tooltip() != null ?
Geary.HTML.escape_markup(entry.get_sidebar_tooltip()) : null);
store.set(assoc_iter, Columns.WRAPPER, wrapper);
store.set(assoc_iter, Columns.COUNTER, entry.get_count());
load_entry_icons(assoc_iter);
entry.sidebar_tooltip_changed.connect(on_sidebar_tooltip_changed);
entry.sidebar_name_changed.connect(on_sidebar_name_changed);
entry.sidebar_count_changed.connect(on_sidebar_count_changed);
Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry;
if (emphasizable != null)
emphasizable.is_emphasized_changed.connect(on_is_emphasized_changed);
entry.grafted(this);
}
private EntryWrapper reparent_wrapper(Gtk.TreeIter new_iter, EntryWrapper current_wrapper) {
Sidebar.Entry entry = current_wrapper.entry;
bool removed = entry_map.unset(entry);
assert(removed);
EntryWrapper new_wrapper = new EntryWrapper(store, entry, store.get_path(new_iter));
entry_map.set(entry, new_wrapper);
store.set(new_iter, Columns.NAME, get_name_for_entry(entry));
store.set(new_iter, Columns.TOOLTIP, Geary.HTML.escape_markup(entry.get_sidebar_tooltip()));
store.set(new_iter, Columns.COUNTER, entry.get_count());
store.set(new_iter, Columns.WRAPPER, new_wrapper);
load_entry_icons(new_iter);
return new_wrapper;
}
protected void prune_all() {
while (branches.keys.size > 0) {
Gee.Iterator<Sidebar.Branch> iterator = branches.keys.iterator();
if (!iterator.next())
break;
prune(iterator.get());
}
}
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_name_changed.disconnect(on_sidebar_name_changed);
entry.sidebar_count_changed.disconnect(on_sidebar_count_changed);
Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry;
if (emphasizable != null)
emphasizable.is_emphasized_changed.disconnect(on_is_emphasized_changed);
bool removed = entry_map.unset(entry);
assert(removed);
}
private void on_branch_entry_added(Sidebar.Branch branch, Sidebar.Entry entry) {
Sidebar.Entry? parent = branch.get_parent(entry);
assert(parent != null);
EntryWrapper? parent_wrapper = get_wrapper(parent);
assert(parent_wrapper != null);
Gtk.TreeIter insertion_iter;
Sidebar.Entry? next = branch.get_next_sibling(entry);
if (next != null) {
EntryWrapper next_wrapper = get_wrapper(next);
// insert before the next sibling in this branch level
store.insert_before(out insertion_iter, parent_wrapper.get_iter(), next_wrapper.get_iter());
} else {
// append to the bottom of this branch level
store.append(out insertion_iter, parent_wrapper.get_iter());
}
associate_entry(insertion_iter, entry);
associate_children(branch, entry, insertion_iter);
if (branch.is_auto_open_on_new_child())
expand_to_entry(entry);
}
private void on_branch_entry_removed(Sidebar.Branch branch, Sidebar.Entry entry) {
EntryWrapper? wrapper = get_wrapper(entry);
assert(wrapper != null);
assert(!(wrapper is RootWrapper));
disassociate_wrapper_and_signal(wrapper, false);
}
private void on_branch_entry_moved(Sidebar.Branch branch, Sidebar.Entry entry) {
EntryWrapper? wrapper = get_wrapper(entry);
assert(wrapper != null);
assert(!(wrapper is RootWrapper));
// null means entry is now at the top of the sibling list
Gtk.TreeIter? prev_iter = null;
Sidebar.Entry? prev = branch.get_previous_sibling(entry);
if (prev != null) {
EntryWrapper? prev_wrapper = get_wrapper(prev);
assert(prev_wrapper != null);
prev_iter = prev_wrapper.get_iter();
}
Gtk.TreeIter entry_iter = wrapper.get_iter();
store.move_after(ref entry_iter, prev_iter);
}
private void on_branch_entry_reparented(Sidebar.Branch branch, Sidebar.Entry entry,
Sidebar.Entry old_parent) {
EntryWrapper? wrapper = get_wrapper(entry);
assert(wrapper != null);
assert(!(wrapper is RootWrapper));
bool selected = (get_current_path().compare(wrapper.get_path()) == 0);
// remove from current position in tree
Gtk.TreeIter iter = wrapper.get_iter();
store.remove(ref iter);
Sidebar.Entry? parent = branch.get_parent(entry);
assert(parent != null);
EntryWrapper? parent_wrapper = get_wrapper(parent);
assert(parent_wrapper != null);
// null means entry is now at the top of the sibling list
Gtk.TreeIter? prev_iter = null;
Sidebar.Entry? prev = branch.get_previous_sibling(entry);
if (prev != null) {
EntryWrapper? prev_wrapper = get_wrapper(prev);
assert(prev_wrapper != null);
prev_iter = prev_wrapper.get_iter();
}
Gtk.TreeIter new_iter;
store.insert_after(out new_iter, parent_wrapper.get_iter(), prev_iter);
EntryWrapper new_wrapper = reparent_wrapper(new_iter, wrapper);
if (selected) {
expand_to_entry(new_wrapper.entry);
place_cursor(new_wrapper.entry, false);
}
}
private void on_branch_children_reordered(Sidebar.Branch branch, Sidebar.Entry entry) {
Gee.List<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, tooltip != null ?
Geary.HTML.escape_markup(tooltip) : null);
}
private void rename_entry(Sidebar.Entry entry) {
EntryWrapper? wrapper = get_wrapper(entry);
assert(wrapper != null);
store.set(wrapper.get_iter(), Columns.NAME, get_name_for_entry(entry));
}
private void on_sidebar_name_changed(Sidebar.Entry entry, string name) {
rename_entry(entry);
}
private void on_is_emphasized_changed(Sidebar.EmphasizableEntry entry, bool is_emphasized) {
rename_entry(entry);
}
private void on_sidebar_count_changed(Sidebar.Entry entry, int coun) {
EntryWrapper? wrapper = get_wrapper(entry);
assert(wrapper != null);
store.set(wrapper.get_iter(), Columns.COUNTER, entry.get_count());
}
private void load_entry_icons(Gtk.TreeIter iter) {
EntryWrapper? wrapper = get_wrapper_at_iter(iter);
if (wrapper == null)
return;
string? icon = wrapper.entry.get_sidebar_icon();
store.set(iter, Columns.ICON, icon);
}
private void load_branch_icons(Gtk.TreeIter iter) {
load_entry_icons(iter);
Gtk.TreeIter child_iter;
if (store.iter_children(out child_iter, iter)) {
do {
load_branch_icons(child_iter);
} while (store.iter_next(ref child_iter));
}
}
private bool on_selection(Gtk.TreeSelection selection, Gtk.TreeModel model, Gtk.TreePath path,
bool path_currently_selected) {
// only allow selection if a page is selectable
EntryWrapper? wrapper = get_wrapper_at_path(path);
return (wrapper != null) ? (wrapper.entry is Sidebar.SelectableEntry) : false;
}
private Gtk.TreePath? get_path_from_event(Gdk.EventButton event) {
int x, y;
Gdk.ModifierType mask;
event.window.get_device_position(Gdk.Display.get_default().get_device_manager()
.get_client_pointer(), out x, out y, out mask);
int cell_x, cell_y;
Gtk.TreePath path;
return get_path_at_pos(x, y, out path, null, out cell_x, out cell_y) ? path : null;
}
private Gtk.TreePath? get_current_path() {
Gtk.TreeModel model;
GLib.List<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;
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) {
if (default_context_menu != null)
default_context_menu.popup(null, null, null, event.button, event.time);
return true;
}
public bool on_toggle_row(Gtk.TreeIter iter, Gtk.TreePath path) {
// Determine whether to allow the row to toggle
EntryWrapper? wrapper = get_wrapper_at_iter(iter);
if (wrapper == null) {
return false; // don't affect things
}
// Most of the time, only allow manual toggles
bool should_allow_toggle = expander_called_manually;
// Cancel out the manual flag
expander_called_manually = false;
// If we are an expanded parent entry with content
if (is_row_expanded(path) && store.iter_has_child(iter) && wrapper.entry is Sidebar.SelectableEntry) {
// We are taking a special action
expander_special_count++;
if (expander_special_count == 1) {
// Workaround that prevents arrows from double-toggling
return true;
} else {
// Toggle only if non-manual, as opposed to the usual behavior
should_allow_toggle = !should_allow_toggle;
}
} else {
// Reset the special behavior count
expander_special_count = 0;
}
if (should_allow_toggle) {
return false;
}
// Prevent branch expansion toggle
return true;
}
public override bool button_press_event(Gdk.EventButton event) {
Gtk.TreePath? path = get_path_from_event(event);
if (event.button == 3 && event.type == Gdk.EventType.BUTTON_PRESS) {
// single right click
if (path != null)
popup_context_menu(path, event);
else
popup_default_context_menu(event);
} else if (event.button == 1 && event.type == Gdk.EventType.BUTTON_PRESS) {
if (path == null) {
old_path_ref = null;
return base.button_press_event(event);
}
EntryWrapper? wrapper = get_wrapper_at_path(path);
if (wrapper == null) {
old_path_ref = null;
return base.button_press_event(event);
}
// Enable single click to toggle tree entries (bug 4985)
if (wrapper.entry is Sidebar.ExpandableEntry
|| wrapper.entry is Sidebar.InternalDropTargetEntry) {
// all labels are InternalDropTargetEntries
toggle_branch_expansion(path, false);
}
// Is this a click on an already-highlighted tree item?
if ((old_path_ref != null) && (old_path_ref.get_path() != null)
&& (old_path_ref.get_path().compare(path) == 0)) {
// yes, don't allow single-click editing, but
// pass the event on for dragging.
text_renderer.editable = false;
return base.button_press_event(event);
}
// Got click on different tree item, make sure it is editable
// if it needs to be.
if (wrapper.entry is Sidebar.RenameableEntry &&
((Sidebar.RenameableEntry) wrapper.entry).is_user_renameable()) {
text_renderer.editable = true;
}
// Remember what tree item is highlighted for next time.
old_path_ref = new Gtk.TreeRowReference(store, path);
}
return base.button_press_event(event);
}
public bool is_keypress_interpreted(Gdk.EventKey event) {
switch (Gdk.keyval_name(event.keyval)) {
case "F2":
case "Delete":
case "Return":
case "KP_Enter":
return true;
default:
return false;
}
}
public override bool key_press_event(Gdk.EventKey event) {
switch (Gdk.keyval_name(event.keyval)) {
case "Return":
case "KP_Enter":
Gtk.TreePath? path = get_current_path();
if (path != null)
toggle_branch_expansion(path, false);
return true;
case "F2":
return rename_in_place();
case "Delete":
Gtk.TreePath? path = get_current_path();
return (path != null) ? destroy_path(path) : false;
}
return base.key_press_event(event);
}
public bool rename_entry_in_place(Sidebar.Entry entry) {
if (!expand_to_entry(entry))
return false;
if (!place_cursor(entry, false))
return false;
return rename_in_place();
}
private bool rename_in_place() {
Gtk.TreePath? cursor_path;
Gtk.TreeViewColumn? cursor_column;
get_cursor(out cursor_path, out cursor_column);
if (can_rename_path(cursor_path)) {
set_cursor(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 = targetable.internal_drop_received(context, selection_data);
Gtk.drag_finish(context, success, false, time);
}
public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) {
// call the base signal to get rows with children to spring open
base.drag_motion(context, x, y, time);
Gtk.TreePath path;
Gtk.TreeViewDropPosition pos;
bool has_dest = get_dest_row_at_pos(x, y, out path, out pos);
// we don't want to insert between rows, only select the rows themselves
if (!has_dest || pos == Gtk.TreeViewDropPosition.BEFORE)
set_drag_dest_row(path, Gtk.TreeViewDropPosition.INTO_OR_BEFORE);
else if (pos == Gtk.TreeViewDropPosition.AFTER)
set_drag_dest_row(path, Gtk.TreeViewDropPosition.INTO_OR_AFTER);
Gdk.drag_status(context, context.get_suggested_action(), time);
return has_dest;
}
// Returns true if path is renameable, and selects the path as well.
private bool can_rename_path(Gtk.TreePath path) {
if (editing_disabled > 0)
return false;
EntryWrapper? wrapper = get_wrapper_at_path(path);
if (wrapper == null)
return false;
Sidebar.RenameableEntry? renameable = wrapper.entry as Sidebar.RenameableEntry;
if (renameable == null)
return false;
if (wrapper.entry is Sidebar.Grouping)
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;
}
}