diff options
Diffstat (limited to 'trunk/dimbola/plugins/tagtree_plugin.py')
-rw-r--r-- | trunk/dimbola/plugins/tagtree_plugin.py | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/trunk/dimbola/plugins/tagtree_plugin.py b/trunk/dimbola/plugins/tagtree_plugin.py new file mode 100644 index 0000000..494e272 --- /dev/null +++ b/trunk/dimbola/plugins/tagtree_plugin.py @@ -0,0 +1,318 @@ +# Copyright (C) 2009 Lars Wirzenius <liw@liw.fi> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +# This is necessary for running under Python 2.5, which we need to +# do on Debian, for now. +from __future__ import with_statement + + +import gobject +import gtk + +import dimbola + + +class TaglistEditor(dimbola.Plugin): + + '''Show/edit selected list of all tags.''' + + ID_COL = 0 + NAME_COL = 1 + + def __init__(self, mwc): + self.mwc = mwc + mwc.connect('setup-widgets', self.setup_widgets) + self.db = None + self.model = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING) + self.dragged_tagids = None + + mwc.new_hook('tagtree-changed', gobject.TYPE_NONE, []) + + def tagtree_changed(self): + self.mwc.emit('tagtree-changed') + + def enable(self): + self.enable_signal(self.mwc, 'db-changed', self.remember_db) + self.mwc.add_to_sidebar('right_sidebar', 'tag_tree_expander', + expand=True, fill=True, weight=0) + + def disable(self): + self.disable_signals() + self.mwc.remove_from_sidebar('right_sidebar', 'tag_tree_expander') + + def setup_widgets(self, mwc): + self.remove_dialog = mwc.widgets['remove_tag_dialog'] + self.remove_dialog.set_transient_for(mwc.widgets['window']) + self.remove_label = mwc.widgets['remove_tag_label'] + self.popup = mwc.widgets['tagtree_menu'] + + cr = gtk.CellRendererText() + self.tagname_cr = cr + cr.connect('edited', self.tag_name_was_edited) + col = gtk.TreeViewColumn() + col.pack_start(cr) + col.add_attribute(cr, 'text', self.NAME_COL) + self.tagname_col = col + self.treeview = mwc.widgets['tagtree'] + self.treeview.append_column(col) + self.treeview.set_model(self.model) + + src_targets = [(dimbola.TAGIDS_TYPE, gtk.TARGET_SAME_APP, 0)] + self.treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, + src_targets, + gtk.gdk.ACTION_COPY) + + dest_targets = [(dimbola.TAGIDS_TYPE, gtk.TARGET_SAME_WIDGET, 0)] + self.treeview.enable_model_drag_dest(dest_targets, gtk.gdk.ACTION_COPY) + + self.treeview.get_selection().connect('changed', + lambda *a: mwc.set_sensitive()) + + def columns(self, id_value, name_value): + values = [(self.ID_COL, id_value), (self.NAME_COL, name_value)] + values.sort() + return [value for colno, value in values] + + def remember_db(self, mwc): + self.db = mwc.db + with self.db: + tb = dimbola.TreeBuilder() + for tagid, tagname, parentid in self.db.get_tagnames(): + tb.add(tagid, tagname, tagname.lower(), parentid) + tb.done() + self.model.clear() + self.populate_treemodel(tb.tree) + self.sort_model(None) + + def populate_treemodel(self, nodes, parent_iter=None): + for node in nodes: + tagid, tagname, children = node + print tagid, tagname + it = self.model.append(parent_iter, self.columns(tagid, tagname)) + self.populate_treemodel(children, parent_iter=it) + + def sort_model(self, parent_iter): + '''Sort self.model by tagname at node parent_iter.''' + items = [] + it = self.model.iter_children(parent_iter) + i = 0 + while it: + name = self.model.get_value(it, self.NAME_COL) + items.append((name.lower(), i)) + self.sort_model(it) + it = self.model.iter_next(it) + i += 1 + if items: + items.sort() + reordered = [i for name, i in items] + self.model.reorder(parent_iter, reordered) + + def remove_from_model(self, tagids, parent_iter=None): + '''Remove the given tagids from the model. + + If they have child tags, they are removed too. + + ''' + + it = self.model.iter_children(parent_iter) + while it: + next = self.model.iter_next(it) + tagid = self.model.get_value(it, self.ID_COL) + if tagid in tagids: + self.model.remove(it) + tagids.remove(tagid) + else: + self.remove_from_model(tagids, it) + it = next + + @property + def selected_tags(self): + '''Return list of iters, ids of currently selected tags.''' + selection = self.treeview.get_selection() + model, paths = selection.get_selected_rows() + iters = [model.get_iter(path) for path in paths] + return [(it, model.get_value(it, self.ID_COL)) for it in iters] + + @property + def selected_tag_iter(self): + '''Return iter for currently selected tag, or None. + + If there are more than one, return None. + + ''' + + selection = self.treeview.get_selection() + model, paths = selection.get_selected_rows() + if len(paths) == 1: + return model.get_iter(paths[0]) + else: + return None + + @property + def selected_tagid(self): + '''Return id for currently selected tag, or None. + + If there are more than one, return None. + + ''' + + selection = self.treeview.get_selection() + model, paths = selection.get_selected_rows() + if len(paths) == 1: + it = model.get_iter(paths[0]) + return model.get_value(it, self.ID_COL) + else: + return None + + def on_add_tag_button_clicked(self, *args): + tagname = 'new tag' + with self.db: + tagid = self.db.add_tagname(unicode(tagname)) + self.db.set_tagparent(tagid, self.selected_tagid) + it = self.model.append(self.selected_tag_iter, + self.columns(tagid, tagname)) + self.sort_model(self.selected_tag_iter) + path = self.model.get_path(it) + self.treeview.expand_to_path(path) + self.tagname_cr.set_property('editable', True) + self.treeview.set_cursor(path, focus_column=self.tagname_col, + start_editing=True) + + on_tagtreemenu_add_menuitem_activate = on_add_tag_button_clicked + + def remove_tag_button_is_sensitive(self): + return self.selected_tags + + def on_remove_tag_button_clicked(self, *args): + selected = self.selected_tags + tagnames = [self.model.get_value(it, self.NAME_COL) + for it, tagid in selected] + + self.remove_label.set_markup('Really remove tag <b>%s</b>?' % + ', '.join(tagnames)) + self.remove_dialog.show() + response = self.remove_dialog.run() + self.remove_dialog.hide() + if response == gtk.RESPONSE_OK: + tagids = [tagid for it, tagid in selected] + with self.db: + for tagid in tagids: + self.remove_tag_with_children(tagid) + self.remove_from_model(tagids) + self.tagtree_changed() + + def remove_tag_with_children(self, tagid): + for childid in self.db.get_tagchildren(tagid): + self.remove_tag_with_children(childid) + self.db.remove_tagname(tagid) + + def tag_name_was_edited(self, cr, path, new_tagname): + it = self.model.get_iter(path) + tagid = self.model.get_value(it, self.ID_COL) + with self.db: + self.db.change_tagname(tagid, new_tagname) + self.model.set_value(it, self.NAME_COL, new_tagname) + self.sort_model(self.model.iter_parent(it)) + self.tagtree_changed() + self.tagname_cr.set_property('editable', False) + + def on_tagtree_button_press_event(self, widget, event): + if event.button == 3: + self.popup.popup(None, None, None, event.button, event.time) + return True + + def on_tagtreemenu_rename_menuitem_activate(self, *args): + it, tagid = self.selected_tags[0] + path = self.model.get_path(it) + self.tagname_cr.set_property('editable', True) + self.treeview.set_cursor(path, focus_column=self.tagname_col, + start_editing=True) + + def tagtreemenu_rename_menuitem_is_sensitive(self): + return len(self.selected_tags) == 1 + + # Drag handlers for when we're the drag source. + + def on_tagtree_drag_begin(self, w, dc): + '''Dragging from us begins. + + We find the currently selected tags and remember their tagids + in the dragged_tagids attribute. + + ''' + + self.dragged_tagids = [tagid for it, tagid in self.selected_tags] + + def on_tagtree_drag_data_get(self, w, dc, seldata, info, ts): + '''Send the dragged data to the other end.''' + seldata.set(dimbola.TAGIDS_TYPE, 8, + dimbola.encode_dnd_tagids(self.dragged_tagids)) + + def on_tagtree_drag_end(self, w, dc): + '''Drag operation is finished. + + We forget the tagids that were being dragged. + + ''' + self.dragged_tagids = None + + def on_tagtree_drag_failed(self, w, dc, result): + '''Dragging from us failed: deal with it.''' + self.dragged_tagids = None + + # Drag handler for when we're the drag target. + + def on_tagtree_drag_data_received(self, w, dc, x, y, seldata, info, ts): + '''Receive data from other end.''' + assert seldata.type == dimbola.TAGIDS_TYPE + tagids = dimbola.decode_dnd_tagids(seldata.data) + t = self.treeview.get_dest_row_at_pos(x, y) + if t is None: + parent = None + parentid = None + else: + path, drop = t + parent = self.model.get_iter(path) + if drop in [gtk.TREE_VIEW_DROP_BEFORE, gtk.TREE_VIEW_DROP_AFTER]: + parent = self.model.iter_parent(parent) + if parent: + parentid = self.model.get_value(parent, self.ID_COL) + else: + parentid = None + with self.mwc.db: + for tagid in tagids: + tb = self.build_tree_for_tag(tagid) + self.populate_treemodel(tb, parent_iter=parent) + self.sort_model(parent) + self.mwc.db.set_tagparent(tagid, parentid) + dc.finish(True, True, ts) + + def build_tree_for_tag(self, tagid): + '''Make a dimbola.TreeBuilder.tree for the tree rooted at tagid.''' + + def helper(tb, tagid, parentid): + # We assume we're within a transaction! + tagname = self.mwc.db.get_tagname(tagid) + tb.add(tagid, tagname, tagname.lower(), parentid) + childids = self.mwc.db.get_tagchildren(tagid) + for childid in childids: + helper(tb, childid, tagid) + + tb = dimbola.TreeBuilder() + helper(tb, tagid, None) + tb.done() + return tb.tree + |