# Copyright (C) 2009 Lars Wirzenius # # 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 . # 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 %s?' % ', '.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