summaryrefslogtreecommitdiff
path: root/trunk/dimbola/plugins/tagtree_plugin.py
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/dimbola/plugins/tagtree_plugin.py')
-rw-r--r--trunk/dimbola/plugins/tagtree_plugin.py318
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
+