summaryrefslogtreecommitdiff
path: root/trunk/dimbola/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/dimbola/plugins')
-rw-r--r--trunk/dimbola/plugins/checksum_plugin.py78
-rw-r--r--trunk/dimbola/plugins/export.ui105
-rw-r--r--trunk/dimbola/plugins/export_plugin.py101
-rw-r--r--trunk/dimbola/plugins/folderlist.ui31
-rw-r--r--trunk/dimbola/plugins/folderlist_plugin.py129
-rw-r--r--trunk/dimbola/plugins/gimp_plugin.py76
-rw-r--r--trunk/dimbola/plugins/import.ui66
-rw-r--r--trunk/dimbola/plugins/import_plugin.py213
-rw-r--r--trunk/dimbola/plugins/news_plugin.py47
-rw-r--r--trunk/dimbola/plugins/photoinfo.ui286
-rw-r--r--trunk/dimbola/plugins/photoinfo_plugin.py126
-rw-r--r--trunk/dimbola/plugins/phototags.ui45
-rw-r--r--trunk/dimbola/plugins/phototags_plugin.py107
-rw-r--r--trunk/dimbola/plugins/photoviewer.ui112
-rw-r--r--trunk/dimbola/plugins/photoviewer_plugin.py214
-rw-r--r--trunk/dimbola/plugins/rate_plugin.py101
-rw-r--r--trunk/dimbola/plugins/remove_photos.ui158
-rw-r--r--trunk/dimbola/plugins/remove_photos_plugin.py71
-rw-r--r--trunk/dimbola/plugins/rotate_plugin.py81
-rw-r--r--trunk/dimbola/plugins/rotate_plugin_tests.py53
-rw-r--r--trunk/dimbola/plugins/search.ui214
-rw-r--r--trunk/dimbola/plugins/search_plugin.py181
-rw-r--r--trunk/dimbola/plugins/tagtree.ui166
-rw-r--r--trunk/dimbola/plugins/tagtree_plugin.py318
24 files changed, 3079 insertions, 0 deletions
diff --git a/trunk/dimbola/plugins/checksum_plugin.py b/trunk/dimbola/plugins/checksum_plugin.py
new file mode 100644
index 0000000..e58abf1
--- /dev/null
+++ b/trunk/dimbola/plugins/checksum_plugin.py
@@ -0,0 +1,78 @@
+# 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 datetime
+import hashlib
+import os
+import subprocess
+
+import gtk
+import pyexiv2
+
+import dimbola
+
+
+class ChecksumResult(dimbola.BackgroundStatus):
+
+ def __init__(self, action, description, photoid, sha1):
+ dimbola.BackgroundStatus.__init__(self, action, description)
+ self.photoid = photoid
+ self.sha1 = sha1
+
+ def process_result(self, mwc):
+ with mwc.db:
+ mwc.db.set_sha1(self.photoid, self.sha1)
+
+
+class ComputeChecksumJob(dimbola.BackgroundJob):
+
+ '''Compute checksum for a specific photo.'''
+
+ def __init__(self, photoid, pathname):
+ self.photoid = photoid
+ self.pathname = pathname
+
+ def run(self):
+ return ChecksumResult('stop', 'Checksum for %s' % self.pathname,
+ self.photoid, dimbola.sha1(self.pathname))
+
+
+class ComputeChecksums(dimbola.Plugin):
+
+ '''Compute checksums for original photo files.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+
+ def enable(self):
+ self.mwc.add_to_menu('file_menu', 'missing_checksums_menuitem',
+ 'Compute missing checksums')
+
+ def disable(self):
+ self.mwc.remove_from_menu('file_menu', 'missing_checksums_menuitem')
+
+ def on_missing_checksums_menuitem_activate(self, *args):
+ with self.mwc.db:
+ photoids = self.mwc.db.find_photos_without_checksum()
+ for photoid in photoids:
+ pathname = self.mwc.db.get_photo_pathname(photoid)
+ self.mwc.add_bgjob(ComputeChecksumJob(photoid, pathname))
+
diff --git a/trunk/dimbola/plugins/export.ui b/trunk/dimbola/plugins/export.ui
new file mode 100644
index 0000000..4d0e1df
--- /dev/null
+++ b/trunk/dimbola/plugins/export.ui
@@ -0,0 +1,105 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkFileChooserDialog" id="export_filechooser">
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Export photos</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">normal</property>
+ <property name="has_separator">False</property>
+ <property name="action">select-folder</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="dialog-vbox4">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox3">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkRadioButton" id="export_original_radiobutton">
+ <property name="label" translatable="yes">Original file</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="export_jpeg_radiobutton">
+ <property name="label" translatable="yes">Convert JPEG</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">export_original_radiobutton</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="dialog-action_area4">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button6">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button5">
+ <property name="label" translatable="yes">Export</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button6</action-widget>
+ <action-widget response="-5">button5</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/export_plugin.py b/trunk/dimbola/plugins/export_plugin.py
new file mode 100644
index 0000000..d76187c
--- /dev/null
+++ b/trunk/dimbola/plugins/export_plugin.py
@@ -0,0 +1,101 @@
+# 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 os
+import shutil
+
+import gtk
+
+import dimbola
+
+
+class ExportOriginal(dimbola.BackgroundJob):
+
+ def __init__(self, mwc, photoid, dirname):
+ self.dirname = dirname
+ with mwc.db:
+ folderid, basename, c, d = mwc.db.get_basic_photo_metadata(photoid)
+ self.foldername = mwc.db.get_folder_name(folderid)
+ self.basename = basename
+
+ def run(self):
+ status = dimbola.BackgroundStatus('start',
+ 'Exporting %s' % self.basename)
+ self.send_status(status)
+ shutil.copy(os.path.join(self.foldername, self.basename),
+ os.path.join(self.dirname, self.basename))
+ return dimbola.BackgroundStatus('stop', 'Exported %s' % self.basename)
+
+
+class ExportJpeg(dimbola.BackgroundJob):
+
+ def __init__(self, mwc, photoid, dirname):
+ self.dirname = dirname
+ self.basename = 'foo'
+ with mwc.db:
+ self.preview = str(mwc.db.get_preview(photoid))
+ a, basename, c, rotate = mwc.db.get_basic_photo_metadata(photoid)
+ self.basename = basename
+ self.rotate = rotate
+
+ def run(self):
+ pixbuf = dimbola.image_data_to_pixbuf(self.preview)
+ pixbuf = dimbola.rotate_pixbuf(pixbuf, self.rotate)
+ basename, ext = os.path.splitext(self.basename)
+ new_basename = basename + '.jpg'
+ pixbuf.save(os.path.join(self.dirname, new_basename), "jpeg")
+ return dimbola.BackgroundStatus('stop', 'Exported %s' % new_basename)
+
+
+class ExportFiles(dimbola.Plugin):
+
+ '''Export selected files using background jobs.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+
+ def enable(self):
+ self.mwc.add_to_menu('file_menu', 'export_menuitem',
+ 'Export selected photos')
+
+ def disable(self):
+ self.mwc.remove_from_menu('file_menu', 'export_menuitem')
+
+ def on_export_menuitem_activate(self, menuitem):
+ chooser = self.mwc.widgets['export_filechooser']
+ chooser.set_transient_for(self.mwc.widgets['window'])
+ chooser.show()
+ response = chooser.run()
+ chooser.hide()
+ if response == gtk.RESPONSE_OK:
+ dirname = chooser.get_filenames()[0]
+ origs = self.mwc.widgets['export_original_radiobutton'].get_active()
+ selected = self.mwc.grid.model.selected
+ for photoid in selected:
+ if origs:
+ job = ExportOriginal(self.mwc, photoid, dirname)
+ else:
+ job = ExportJpeg(self.mwc, photoid, dirname)
+ self.mwc.add_bgjob(job)
+
+ def export_menuitem_is_sensitive(self):
+ return self.mwc.grid.model.selected
+
diff --git a/trunk/dimbola/plugins/folderlist.ui b/trunk/dimbola/plugins/folderlist.ui
new file mode 100644
index 0000000..ddea255
--- /dev/null
+++ b/trunk/dimbola/plugins/folderlist.ui
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkExpander" id="folderlist_expander">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkTreeView" id="folders_treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label19">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Folders with photos</property>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/folderlist_plugin.py b/trunk/dimbola/plugins/folderlist_plugin.py
new file mode 100644
index 0000000..1f08de3
--- /dev/null
+++ b/trunk/dimbola/plugins/folderlist_plugin.py
@@ -0,0 +1,129 @@
+# 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 os
+
+import gobject
+import gtk
+
+import dimbola
+
+
+class FolderList(dimbola.Plugin):
+
+ '''Show list of folders with imported photos in left sidebar.'''
+
+ ID_COL = 0
+ NAME_COL = 1
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+ mwc.connect('setup-widgets', self.setup_widgets)
+ self.tagids = set()
+ self.store = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING)
+
+ mwc.new_hook('folder-selection-changed', gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT))
+
+ def enable(self):
+ self.mwc.add_to_sidebar('left_sidebar', 'folderlist_expander',
+ weight=dimbola.MIN_WEIGHT,
+ expand=True, fill=True)
+ self.enable_signal(self.mwc, 'db-changed', self.db_changed)
+
+ def disable(self):
+ self.mwc.remove_from_sidebar('left_sidebar', 'folderlist_expander')
+ self.disable_signals()
+
+ def setup_widgets(self, mwc):
+ cr = gtk.CellRendererText()
+ col = gtk.TreeViewColumn()
+ col.pack_start(cr)
+ col.add_attribute(cr, 'text', self.NAME_COL)
+ self.treeview = self.mwc.widgets['folders_treeview']
+ self.treeview.append_column(col)
+ self.treeview.set_model(self.store)
+
+ sel = self.treeview.get_selection()
+ sel.set_mode(gtk.SELECTION_MULTIPLE)
+ sel.connect('changed', self.selection_changed)
+
+ def db_changed(self, mwc):
+ self.refresh_store()
+
+ def refresh_store(self):
+ tb = dimbola.TreeBuilder()
+ fakes = dict()
+ with self.mwc.db:
+ photoids = self.mwc.db.find_photoids()
+ for photoid in photoids:
+ folderid, b, c, d = \
+ self.mwc.db.get_basic_photo_metadata(photoid)
+ foldername = self.mwc.db.get_folder_name(folderid)
+ parentname = os.path.dirname(foldername)
+ parentid = self.mwc.db.find_folder(parentname)
+ if parentid is None:
+ parentid = self.make_fake_parent(fakes, parentname)
+ basename = os.path.basename(foldername)
+ tb.add(folderid, basename, basename, parentid)
+ for fakename in fakes:
+ parentname = os.path.dirname(fakename)
+ parentid = fakes[parentname]
+ fakeid = fakes[fakename]
+ if parentid == fakeid:
+ parentid = None
+ basename = os.path.basename(fakename) or os.sep
+ tb.add(fakeid, basename, basename, parentid)
+ tb.done()
+ self.store.clear()
+ self.populate_treemodel(tb.tree)
+ self.treeview.expand_all()
+
+ def make_fake_parent(self, fakes, parentname):
+ if parentname not in fakes:
+ fakes[parentname] = -len(fakes) - 1
+ self.make_fake_parent(fakes, os.path.dirname(parentname))
+ return fakes[parentname]
+
+ def populate_treemodel(self, nodes, parent_iter=None):
+ for node in nodes:
+ folderid, foldername, children = node
+ it = self.store.append(parent_iter, (folderid, foldername))
+ self.populate_treemodel(children, parent_iter=it)
+
+ def photos_in_selected_folders(self):
+ '''Return photoids for all currently selected folders.'''
+ sel = self.treeview.get_selection()
+ model, paths = sel.get_selected_rows()
+ folderids = list()
+ photoids = list()
+ with self.mwc.db:
+ for path in paths:
+ it = self.store.get_iter(path)
+ folderid = self.store.get_value(it, self.ID_COL)
+ photoids += self.mwc.db.find_photoids_in_folder(folderid)
+ folderids.append(folderid)
+ return folderids, photoids
+
+ def selection_changed(self, *args):
+ folderids, photoids = self.photos_in_selected_folders()
+ self.mwc.emit('folder-selection-changed', folderids, photoids)
+
diff --git a/trunk/dimbola/plugins/gimp_plugin.py b/trunk/dimbola/plugins/gimp_plugin.py
new file mode 100644
index 0000000..8653f98
--- /dev/null
+++ b/trunk/dimbola/plugins/gimp_plugin.py
@@ -0,0 +1,76 @@
+# 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 os
+import subprocess
+import tempfile
+
+import dimbola
+
+
+class ConvertToPNGAndGimpIt(dimbola.BackgroundJob):
+
+ def __init__(self, mwc, photoid):
+ with mwc.db:
+ folderid, basename, c, d = mwc.db.get_basic_photo_metadata(photoid)
+ foldername = mwc.db.get_folder_name(folderid)
+
+ self.pathname = os.path.join(foldername, basename)
+ self.pngname = self.pathname + '.png'
+
+ def run(self):
+ if not os.path.exists(self.pngname):
+ p = subprocess.Popen(['dcraw', '-c', self.pathname],
+ stdout=subprocess.PIPE)
+ ppm, stderr = p.communicate('')
+ if p.returncode:
+ raise Exception('dcraw failed: exit code %s:\n%s' %
+ (p.returncode, stderr))
+
+ pixbuf = dimbola.image_data_to_pixbuf(ppm)
+ pixbuf.save(self.pngname, 'png')
+
+ os.spawnlp(os.P_NOWAIT, "gimp", "gimp", self.pngname)
+
+
+class Gimp(dimbola.Plugin):
+
+ '''Edit selected photos using the GIMP.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+
+ def enable(self):
+ self.mwc.add_to_menu('photo_menu', 'gimp_menuitem',
+ 'Edit with the GIMP')
+
+ def disable(self):
+ self.mwc.remove_from_menu('photo_menu', 'gimp_menuitem')
+
+ def on_gimp_menuitem_activate(self, *args):
+ selected = self.mwc.grid.model.selected
+ for photoid in selected:
+ job = ConvertToPNGAndGimpIt(self.mwc, photoid)
+ self.mwc.add_bgjob(job)
+
+ def gimp_menuitem_is_sensitive(self):
+ return self.mwc.grid.model.selected
+
diff --git a/trunk/dimbola/plugins/import.ui b/trunk/dimbola/plugins/import.ui
new file mode 100644
index 0000000..55c8aaa
--- /dev/null
+++ b/trunk/dimbola/plugins/import.ui
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkFileChooserDialog" id="import_filechooserdialog">
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Import photos</property>
+ <property name="type_hint">normal</property>
+ <property name="has_separator">False</property>
+ <property name="select_multiple">True</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="dialog-vbox6">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="dialog-action_area6">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button7">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button8">
+ <property name="label" translatable="yes">Import</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button7</action-widget>
+ <action-widget response="-5">button8</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/import_plugin.py b/trunk/dimbola/plugins/import_plugin.py
new file mode 100644
index 0000000..e66da88
--- /dev/null
+++ b/trunk/dimbola/plugins/import_plugin.py
@@ -0,0 +1,213 @@
+# 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 datetime
+import hashlib
+import os
+import subprocess
+
+import gtk
+import pyexiv2
+
+import dimbola
+
+
+class ImportResult(dimbola.BackgroundStatus):
+
+ def __init__(self, action, description, foldername, basename,
+ thumbnail, preview, exifs, sha1):
+ dimbola.BackgroundStatus.__init__(self, action, description)
+ self.foldername = foldername
+ self.basename = basename
+ self.thumbnail = thumbnail
+ self.preview = preview
+ self.exifs = exifs
+ self.sha1 = sha1
+
+ def process_result(self, mwc):
+ db = mwc.db
+ with db:
+ folderid = db.find_folder(self.foldername)
+ if not folderid:
+ folderid = db.add_folder(self.foldername)
+
+ photoid = db.add_photo(folderid, self.basename, 0, 0)
+ db.set_sha1(photoid, self.sha1)
+ for exifname, exifvalue in self.exifs.iteritems():
+ assert type(exifvalue) == str
+ exifid = db.find_exifname(exifname)
+ if not exifid:
+ exifid = db.add_exifname(exifname)
+ db.add_exif(photoid, exifid, exifvalue)
+
+ db.add_thumbnail(photoid, self.thumbnail)
+ db.add_preview(photoid, self.preview)
+
+ mwc.emit('db-changed')
+
+
+class PreviewMaker(object):
+
+ '''Create a JPEG preview of an image file.'''
+
+ def __init__(self):
+ self.dtc = dimbola.DcrawTypeCache()
+
+ def make_preview(self, filename):
+ if self.dtc.supported(filename):
+ return self.from_raw(filename)
+ else:
+ return self.from_other(filename)
+
+ def from_raw(self, filename):
+ p = subprocess.Popen(['dcraw', '-c', filename], stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ ppm, stderr = p.communicate('')
+ if p.returncode:
+ raise Exception('dcraw failed: exit code %s:\n%s' %
+ (p.returncode, stderr))
+ return dimbola.image_data_to_image_data(ppm, 'jpeg',
+ { 'quality': '50' })
+
+ def from_other(self, filename):
+ pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
+ return dimbola.pixbuf_to_image_data(pixbuf, 'jpeg',
+ { 'quality': '50' })
+
+
+class ThumbnailMaker(object):
+
+ '''Create a thumbnail from a preview.'''
+
+ def make_thumbnail(self, preview_jpeg):
+ pixbuf = dimbola.image_data_to_pixbuf(preview_jpeg)
+ pixbuf = dimbola.scale_pixbuf(pixbuf, 200, 200)
+ return dimbola.pixbuf_to_image_data(pixbuf, 'jpeg', {'quality':'50'})
+
+
+class ImportPhoto(dimbola.BackgroundJob):
+
+ exifs = ['Exif.Image.Make',
+ 'Exif.Image.Model',
+ 'Exif.Image.Orientation',
+ 'Exif.Image.DateTime',
+ 'Exif.Photo.ExposureTime',
+ 'Exif.Photo.FNumber',
+ 'Exif.Photo.ISOSpeedRatings',
+ 'Exif.Photo.ShutterSpeedValue',
+ 'Exif.Photo.ApertureValue',
+ 'Exif.Photo.ExposureBiasValue',
+ 'Exif.Photo.FocalLength',
+ 'Exif.Photo.WhiteBalance']
+
+ def __init__(self, pathname):
+ self.pathname = pathname
+ self.description = 'Importing %s' % pathname
+
+ def run(self):
+ self.send_status(dimbola.BackgroundStatus('start',
+ 'Importing %s' % os.path.basename(self.pathname)))
+
+ preview_maker = PreviewMaker()
+ preview = preview_maker.make_preview(self.pathname)
+
+ thumbnail_maker = ThumbnailMaker()
+ thumbnail = thumbnail_maker.make_thumbnail(preview)
+
+ image = pyexiv2.Image(self.pathname)
+ image.readMetadata()
+
+ foldername = os.path.dirname(os.path.abspath(self.pathname))
+ basename = os.path.basename(self.pathname)
+
+ exifs = dict()
+ for exifname in image.exifKeys():
+ if exifname not in self.exifs:
+ continue
+ exifs[exifname] = self.get_encoded(image, exifname)
+ assert type(exifs[exifname]) == str
+
+ return ImportResult('stop', 'Imported %s' % basename,
+ foldername, basename, thumbnail, preview, exifs,
+ dimbola.sha1(self.pathname))
+
+ def get_encoded(self, image, key):
+ s = image.interpretedExifValue(key)
+ assert type(s) == str
+ return s
+
+
+class ImportFiles(dimbola.Plugin):
+
+ '''Import into database using background jobs.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+ self.mwc.connect('setup-widgets', self.setup_widgets)
+
+ def setup_widgets(self, mwc):
+ self.chooser = mwc.widgets['import_filechooserdialog']
+ self.chooser.set_transient_for(mwc.widgets['window'])
+ self.dtc = dimbola.DcrawTypeCache()
+
+ photo_filter = gtk.FileFilter()
+ photo_filter.set_name('All images')
+ photo_filter.add_pixbuf_formats()
+ photo_filter.add_custom(gtk.FILE_FILTER_FILENAME |
+ gtk.FILE_FILTER_MIME_TYPE,
+ self.filter_dcraw_known,
+ None)
+ self.chooser.add_filter(photo_filter)
+
+ raw_filter = gtk.FileFilter()
+ raw_filter.set_name('RAW photos')
+ raw_filter.add_custom(gtk.FILE_FILTER_FILENAME |
+ gtk.FILE_FILTER_MIME_TYPE,
+ self.filter_dcraw_known,
+ None)
+ self.chooser.add_filter(raw_filter)
+
+ all_filter = gtk.FileFilter()
+ all_filter.set_name('All files')
+ all_filter.add_pattern('*')
+ self.chooser.add_filter(all_filter)
+
+
+ def enable(self):
+ self.mwc.add_to_menu('file_menu', 'import_menuitem',
+ 'Import photos')
+
+ def disable(self):
+ self.mwc.remove_from_menu('file_menu', 'import_menuitem')
+
+ def on_import_menuitem_activate(self, *args):
+ self.chooser.show()
+ response = self.chooser.run()
+ self.chooser.hide()
+ if response == gtk.RESPONSE_OK:
+ for pathname in self.chooser.get_filenames():
+ job = ImportPhoto(pathname)
+ self.mwc.add_bgjob(job)
+
+ def filter_dcraw_known(self, filter_info, data):
+ pathname, uri, display_name, mime_type = filter_info
+ return self.dtc.supported(pathname)
+
diff --git a/trunk/dimbola/plugins/news_plugin.py b/trunk/dimbola/plugins/news_plugin.py
new file mode 100644
index 0000000..c76aa3e
--- /dev/null
+++ b/trunk/dimbola/plugins/news_plugin.py
@@ -0,0 +1,47 @@
+# 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/>.
+
+
+import os
+import time
+
+import gtk
+
+import dimbola
+
+
+class News(dimbola.Plugin):
+
+ '''Show the user the NEWS file.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+ path = '/%s/NEWS.html' % os.path.dirname(dimbola.__file__)
+ self.uri = 'file://%s' % path
+ self.exists = os.path.exists(path)
+
+ def enable(self):
+ self.mwc.add_to_menu('help_menu', 'news_menuitem',
+ 'News')
+
+ def disable(self):
+ self.mwc.remove_from_menu('help_menu', 'news_menuitem')
+
+ def on_news_menuitem_activate(self, *args):
+ gtk.show_uri(screen=None, uri=self.uri, timestamp=int(time.time()))
+
+ def news_menuitem_is_sensitive(self):
+ return self.exists
+
diff --git a/trunk/dimbola/plugins/photoinfo.ui b/trunk/dimbola/plugins/photoinfo.ui
new file mode 100644
index 0000000..3be939a
--- /dev/null
+++ b/trunk/dimbola/plugins/photoinfo.ui
@@ -0,0 +1,286 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkExpander" id="basic_info_expander">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="n_rows">10</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">6</property>
+ <property name="row_spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Rating</property>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Filename</property>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Folder</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Photo</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_photoid">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_folder">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="max_width_chars">20</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_filename">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="max_width_chars">20</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Camera</property>
+ </object>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_camera">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Date/time</property>
+ </object>
+ <packing>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_datetime">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="labelxx">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Shutter</property>
+ </object>
+ <packing>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Aperture</property>
+ </object>
+ <packing>
+ <property name="top_attach">7</property>
+ <property name="bottom_attach">8</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">ISO</property>
+ </object>
+ <packing>
+ <property name="top_attach">8</property>
+ <property name="bottom_attach">9</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label15">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Focal length</property>
+ </object>
+ <packing>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ <property name="x_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_shutter">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_aperture">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">7</property>
+ <property name="bottom_attach">8</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_iso">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">8</property>
+ <property name="bottom_attach">9</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="photo_info_focal_length">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">9</property>
+ <property name="bottom_attach">10</property>
+ <property name="x_options">GTK_EXPAND | GTK_SHRINK | GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkDrawingArea" id="photo_info_rating">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Basic info</property>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/photoinfo_plugin.py b/trunk/dimbola/plugins/photoinfo_plugin.py
new file mode 100644
index 0000000..e5bb4fb
--- /dev/null
+++ b/trunk/dimbola/plugins/photoinfo_plugin.py
@@ -0,0 +1,126 @@
+# 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 os
+
+import gtk
+import pyexiv2
+
+import dimbola
+
+
+class PhotoInfoViewer(dimbola.Plugin):
+
+ '''Show information about the selected photo in the right sidebar.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+ self.photoid = None
+
+ def enable(self):
+ self.enable_signal(self.mwc.grid.model, 'selection-changed',
+ self.remember_photo)
+ self.enable_signal(self.mwc, 'photo-meta-changed',
+ self.remember_photo)
+ self.mwc.add_to_sidebar('right_sidebar', 'basic_info_expander')
+
+ def disable(self):
+ self.disable_signals()
+ self.mwc.remove_from_sidebar('right_sidebar', 'basic_info_expander')
+
+ def exifvalue(self, exifname):
+ if self.photoid is None:
+ return ''
+ else:
+ return self.mwc.db.get_exif(self.photoid, exifname) or ''
+
+ @property
+ def foldername(self):
+ if self.photoid is None:
+ return ''
+ else:
+ folderid, basename, rating, rotate = \
+ self.mwc.db.get_basic_photo_metadata(self.photoid)
+ foldername = self.mwc.db.get_folder_name(folderid)
+ if foldername:
+ foldername = os.path.basename(foldername)
+ return foldername or ''
+
+ @property
+ def filename(self):
+ if self.photoid is None:
+ return ''
+ else:
+ folderid, basename, rating, rotate = \
+ self.mwc.db.get_basic_photo_metadata(self.photoid)
+ return basename
+
+ @property
+ def rating(self):
+ if self.photoid is None:
+ return ''
+ else:
+ folderid, basename, rating, rotate = \
+ self.mwc.db.get_basic_photo_metadata(self.photoid)
+ return '*' * rating
+
+ def draw_rating_stars(self):
+ w = self.mwc.widgets['photo_info_rating']
+ gc = w.get_style().fg_gc[gtk.STATE_NORMAL]
+ x = 0
+ y = 0
+ width, dim = w.window.get_size()
+ with self.mwc.db:
+ dimbola.draw_stars(len(self.rating), w.window, gc, x, y, dim)
+
+ def on_photo_info_rating_expose_event(self, w, event):
+ self.draw_rating_stars()
+
+ def remember_photo(self, *args):
+ if self.mwc.db is None:
+ return
+
+ if len(self.mwc.grid.model.selected) == 1:
+ self.photoid = self.mwc.grid.model.selected[0]
+ else:
+ self.photoid = None
+ with self.mwc.db:
+ exiftable = {
+ 'photo_info_camera': 'Exif.Image.Model',
+ 'photo_info_datetime': 'Exif.Image.DateTime',
+ 'photo_info_shutter': 'Exif.Photo.ExposureTime',
+ 'photo_info_aperture': 'Exif.Photo.ApertureValue',
+ 'photo_info_iso': 'Exif.Photo.ISOSpeedRatings',
+ 'photo_info_focal_length': 'Exif.Photo.FocalLength',
+ }
+ for widget_name, exifname in exiftable.iteritems():
+ value = self.exifvalue(exifname)
+ self.mwc.widgets[widget_name].set_text(value)
+
+ misctable = {
+ 'photo_info_photoid': '%s' % (self.photoid or ''),
+ 'photo_info_folder': self.foldername,
+ 'photo_info_filename': self.filename,
+ }
+ for widget_name, value in misctable.iteritems():
+ self.mwc.widgets[widget_name].set_text(value)
+ self.draw_rating_stars()
+
diff --git a/trunk/dimbola/plugins/phototags.ui b/trunk/dimbola/plugins/phototags.ui
new file mode 100644
index 0000000..794e23a
--- /dev/null
+++ b/trunk/dimbola/plugins/phototags.ui
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkExpander" id="photo_tags_expander">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkTextView" id="tags_textview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="border_width">1</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">word</property>
+ <property name="cursor_visible">False</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label18">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Photo's tags</property>
+ </object>
+ </child>
+ </object>
+ <object class="GtkMenu" id="tags_textview_popup">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImageMenuItem" id="remove_photo_tag_menuitem">
+ <property name="label">gtk-remove</property>
+ <property name="visible">True</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/phototags_plugin.py b/trunk/dimbola/plugins/phototags_plugin.py
new file mode 100644
index 0000000..e2fc1d2
--- /dev/null
+++ b/trunk/dimbola/plugins/phototags_plugin.py
@@ -0,0 +1,107 @@
+# 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 gtk
+
+import dimbola
+
+
+class PhotoTags(dimbola.Plugin):
+
+ '''Show/edit selected photo's tags.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+ mwc.connect('setup-widgets', self.setup_widgets)
+
+ def setup_widgets(self, mwc):
+ self.textview = mwc.widgets['tags_textview']
+ self.popup = mwc.widgets['tags_textview_popup']
+ self.remove_menuitem = mwc.widgets['remove_photo_tag_menuitem']
+ self.taglist = dimbola.Taglist(self.textview, self.popup,
+ self.prepare_popup)
+ self.photoid = None
+
+ def enable(self):
+ self.enable_signal(self.mwc.grid.model, 'selection-changed',
+ self.selection_changed)
+ self.enable_signal(self.mwc, 'tagtree-changed',
+ lambda *args: self.refresh_tags())
+ self.mwc.add_to_sidebar('right_sidebar', 'photo_tags_expander')
+
+ def disable(self):
+ self.disable_signals()
+ self.mwc.remove_from_sidebar('right_sidebar', 'photo_tags_expander')
+
+ def refresh_tags(self):
+ with self.mwc.db:
+ tags = []
+ for tagid in self.mwc.db.get_tagids(self.photoid):
+ tagname = self.mwc.db.get_tagname(tagid)
+ if tagname is not None:
+ tags.append((tagid, tagname))
+ tags.sort()
+ self.taglist.set_tags(self.photoid, tags)
+
+ def selection_changed(self, model):
+ if self.mwc.db:
+ if len(model.selected) == 1:
+ self.photoid = model.selected[0]
+ self.refresh_tags()
+ else:
+ self.taglist.clear()
+ self.photoid = None
+ else:
+ self.photoid = None
+
+ def tags_textview_is_sensitive(self):
+ return len(self.mwc.grid.model.selected) == 1
+
+ def prepare_popup(self):
+ self.remove_menuitem.set_sensitive(
+ self.taglist.buf.get_has_selection())
+
+ def on_tags_textview_button_press_event(self, widget, event):
+ return self.taglist.button_press_event(widget, event)
+
+ def on_remove_photo_tag_menuitem_activate(self, *args):
+ photoid, tagid = self.taglist.selected_tag()
+ if photoid is not None:
+ with self.mwc.db:
+ self.mwc.db.remove_tagid(photoid, tagid)
+ self.refresh_tags()
+
+ def on_tags_textview_drag_motion(self, *args):
+ return self.taglist.drag_motion(*args)
+
+ def on_tags_textview_drag_leave(self, *args):
+ return self.taglist.drag_leave(*args)
+
+ def on_tags_textview_drag_data_received(self, *args):
+ if self.photoid is None:
+ return
+
+ tagids = self.taglist.drag_data_received(*args)
+ with self.mwc.db:
+ for tagid in tagids:
+ self.mwc.db.add_tagid(self.photoid, tagid)
+ self.refresh_tags()
+
diff --git a/trunk/dimbola/plugins/photoviewer.ui b/trunk/dimbola/plugins/photoviewer.ui
new file mode 100644
index 0000000..ed36f28
--- /dev/null
+++ b/trunk/dimbola/plugins/photoviewer.ui
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkWindow" id="photo_window">
+ <property name="title" translatable="yes">Dimbola Photo Viewer</property>
+ <property name="default_width">700</property>
+ <property name="default_height">500</property>
+ <property name="destroy_with_parent">True</property>
+ <child>
+ <object class="GtkVBox" id="vbox6">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkDrawingArea" id="photowin_drawingarea">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="events">GDK_BUTTON_PRESS_MASK | GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK</property>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkHBox" id="hbox4">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="fullscreen_button">
+ <property name="label">gtk-fullscreen</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="unfullscreen_button">
+ <property name="label">gtk-leave-fullscreen</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox2">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="photowin_previous_button">
+ <property name="label">gtk-media-previous</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="photowin_next_button">
+ <property name="label">gtk-media-next</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/photoviewer_plugin.py b/trunk/dimbola/plugins/photoviewer_plugin.py
new file mode 100644
index 0000000..45ce81c
--- /dev/null
+++ b/trunk/dimbola/plugins/photoviewer_plugin.py
@@ -0,0 +1,214 @@
+# 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 os
+
+import gtk
+
+import dimbola
+
+
+class PhotoViewerBase(object):
+
+ '''Display the currently selected photo in full size.
+
+ This is a base class, from which will be derived two other classes,
+ one for showing photos in a tab, the other in a separate toplevel
+ window.
+
+ '''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+
+ self.photoid = None
+
+ mwc.connect('setup-widgets', self.setup_widgets)
+ mwc.connect('photo-meta-changed', lambda *args: self.draw_photo())
+
+ def remember_photo(self, model):
+ if len(model.selected) == 1:
+ self.photoid = model.selected[0]
+ else:
+ self.photoid = None
+ self.draw_photo()
+
+ def draw_photo(self):
+ w = self.widget.window
+ if w is None:
+ return
+ w.clear()
+ width, height = w.get_size()
+ style = self.widget.get_style()
+ if self.widget.flags() & gtk.HAS_FOCUS:
+ self.widget.get_style().paint_focus(w,
+ self.widget.state,
+ None,
+ None,
+ None,
+ 0, 0,
+ width, height)
+ fg = style.fg_gc[gtk.STATE_NORMAL]
+ with self.mwc.db:
+ preview = self.mwc.db.get_preview(self.photoid)
+ a, b, c, rotate = \
+ self.mwc.db.get_basic_photo_metadata(self.photoid)
+ if preview is not None:
+ pixbuf = dimbola.image_data_to_pixbuf(preview)
+ pixbuf = dimbola.rotate_pixbuf(pixbuf, rotate)
+ pixbuf = dimbola.scale_pixbuf(pixbuf, width, height)
+ x = (width - pixbuf.get_width()) / 2
+ y = (height - pixbuf.get_height()) / 2
+ w.draw_pixbuf(fg, pixbuf, 0, 0, x, y)
+
+ def request_rating(self, stars):
+ self.mwc.emit('photo-rating-requested', stars)
+
+ def key_press_event(self, event):
+ if event.type == gtk.gdk.KEY_PRESS:
+ bindings = {
+ '0': lambda *args: self.request_rating(0),
+ '1': lambda *args: self.request_rating(1),
+ '2': lambda *args: self.request_rating(2),
+ '3': lambda *args: self.request_rating(3),
+ '4': lambda *args: self.request_rating(4),
+ '5': lambda *args: self.request_rating(5),
+ }
+ if event.keyval in bindings:
+ bindings[event.keyval]()
+ return True
+ elif event.string in bindings:
+ bindings[event.string]()
+ return True
+ return False
+
+
+class PhotoViewerNotebook(PhotoViewerBase, dimbola.Plugin):
+
+ '''Display the currently selected photo in full size, in notebook.'''
+
+ def enable(self):
+ pass
+
+ def disable(self):
+ pass
+
+ def setup_widgets(self, mwc):
+ self.widget = mwc.widgets['photo_drawingarea']
+ self.box = mwc.widgets['photo_vbox']
+ mwc.grid.model.connect('selection-changed', self.remember_photo)
+
+ def on_photo_drawingarea_expose_event(self, *args):
+ self.draw_photo()
+
+ def on_photo_previous_button_clicked(self, *args):
+ self.mwc.grid.model.select_previous()
+
+ def on_photo_next_button_clicked(self, *args):
+ self.mwc.grid.model.select_next()
+
+ def on_view_photo_menuitem_activate(self, radio):
+ if radio.get_active():
+ self.box.show()
+ self.widget.grab_focus()
+ else:
+ self.box.hide()
+
+ def on_photo_drawingarea_key_press_event(self, widget, event):
+ return self.key_press_event(event)
+
+ def on_photo_drawingarea_button_press_event(self, widget, event):
+ widget.grab_focus()
+ return False
+
+
+class PhotoViewerWindow(PhotoViewerBase, dimbola.Plugin):
+
+ '''Like PhotoViewerNotebook, but use a separate window.'''
+
+ def enable(self):
+ self.mwc.add_to_menu('photo_menu', 'photowin_menuitem',
+ 'View photo in separate window',
+ check=True)
+ self.menuitem = self.mwc.widgets['photowin_menuitem']
+
+ def disable(self):
+ self.mwc.remove_from_menu('photo_menu', 'photowin_menuitem')
+ self.menuitem = None
+
+ def setup_widgets(self, mwc):
+ self.widget = mwc.widgets['photowin_drawingarea']
+ self.window = mwc.widgets['photo_window']
+ self.fullscreen_button = mwc.widgets['fullscreen_button']
+ self.unfullscreen_button = mwc.widgets['unfullscreen_button']
+ self.widget = mwc.widgets['photowin_drawingarea']
+ mwc.grid.model.connect('selection-changed', self.remember_photo)
+
+ def on_photowin_menuitem_activate(self, menuitem):
+ if menuitem.get_active():
+ self.window.show()
+ else:
+ self.window.hide()
+
+ def on_photowin_drawingarea_expose_event(self, *args):
+ self.draw_photo()
+
+ def on_photo_window_delete_event(self, *args):
+ self.menuitem.set_active(False)
+ self.window.hide()
+ return True
+
+ def on_fullscreen_button_clicked(self, *args):
+ self.window.fullscreen()
+ self.fullscreen_button.hide()
+ self.unfullscreen_button.show()
+
+ def on_unfullscreen_button_clicked(self, *args):
+ self.window.unfullscreen()
+ self.unfullscreen_button.hide()
+ self.fullscreen_button.show()
+
+ def on_photowin_previous_button_clicked(self, *args):
+ self.mwc.grid.model.select_previous()
+
+ def on_photowin_next_button_clicked(self, *args):
+ self.mwc.grid.model.select_next()
+
+ def on_photowin_drawingarea_key_press_event(self, widget, event):
+
+ bindings = {
+ gtk.keysyms.Escape: self.on_unfullscreen_button_clicked,
+ }
+
+ if event.type == gtk.gdk.KEY_PRESS:
+ if event.keyval == gtk.keysyms.Escape:
+ self.on_unfullscreen_button_clicked()
+ return True
+ elif (event.keyval == gtk.keysyms.w and
+ event.state & gtk.gdk.CONTROL_MASK):
+ self.on_photo_window_delete_event()
+ return True
+ return self.key_press_event(event)
+
+ def on_photowin_drawingarea_button_press_event(self, widget, event):
+ widget.grab_focus()
+ return False
+
diff --git a/trunk/dimbola/plugins/rate_plugin.py b/trunk/dimbola/plugins/rate_plugin.py
new file mode 100644
index 0000000..435d01f
--- /dev/null
+++ b/trunk/dimbola/plugins/rate_plugin.py
@@ -0,0 +1,101 @@
+# 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 os
+import shutil
+
+import gobject
+import gtk
+
+import dimbola
+
+
+class RatePhotosPlugin(dimbola.Plugin):
+
+ '''Allow user to use a 0-5 star rating for photos.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+ mwc.new_hook('photo-rating-requested', gobject.TYPE_NONE,
+ (gobject.TYPE_INT,))
+
+ def enable(self):
+ self.mwc.add_to_menu('photo_menu', 'rate_as_0_stars',
+ 'Rate as 0 stars')
+ self.mwc.add_to_menu('photo_menu', 'rate_as_1_star',
+ 'Rate as 1 stars')
+ self.mwc.add_to_menu('photo_menu', 'rate_as_2_stars',
+ 'Rate as 2 stars')
+ self.mwc.add_to_menu('photo_menu', 'rate_as_3_stars',
+ 'Rate as 3 stars')
+ self.mwc.add_to_menu('photo_menu', 'rate_as_4_stars',
+ 'Rate as 4 stars')
+ self.mwc.add_to_menu('photo_menu', 'rate_as_5_stars',
+ 'Rate as 5 stars')
+ self.enable_signal(self.mwc, 'photo-rating-requested', self.rate_cb)
+
+ def disable(self):
+ self.mwc.remove_from_menu('photo_menu', 'rate_as_0_stars')
+ self.mwc.remove_from_menu('photo_menu', 'rate_as_1_star')
+ self.mwc.remove_from_menu('photo_menu', 'rate_as_2_stars')
+ self.mwc.remove_from_menu('photo_menu', 'rate_as_3_stars')
+ self.mwc.remove_from_menu('photo_menu', 'rate_as_4_stars')
+ self.mwc.remove_from_menu('photo_menu', 'rate_as_5_stars')
+
+ def rate(self, stars):
+ '''Rate selected photos with stars.'''
+ with self.mwc.db:
+ for photoid in self.mwc.grid.model.selected:
+ self.mwc.db.set_rating(photoid, stars)
+ for photoid in self.mwc.grid.model.selected:
+ self.mwc.photo_meta_changed(photoid)
+
+ def rate_cb(self, mwc, stars):
+ self.rate(stars)
+
+ def on_rate_as_0_stars_activate(self, menuitem):
+ self.rate(0)
+
+ def on_rate_as_1_star_activate(self, menuitem):
+ self.rate(1)
+
+ def on_rate_as_2_stars_activate(self, menuitem):
+ self.rate(2)
+
+ def on_rate_as_3_stars_activate(self, menuitem):
+ self.rate(3)
+
+ def on_rate_as_4_stars_activate(self, menuitem):
+ self.rate(4)
+
+ def on_rate_as_5_stars_activate(self, menuitem):
+ self.rate(5)
+
+ def menuitem_is_sensitive(self):
+ return self.mwc.grid.model.selected
+
+ rate_as_0_stars_is_sensitive = menuitem_is_sensitive
+ rate_as_1_star_is_sensitive = menuitem_is_sensitive
+ rate_as_2_stars_is_sensitive = menuitem_is_sensitive
+ rate_as_3_stars_is_sensitive = menuitem_is_sensitive
+ rate_as_4_stars_is_sensitive = menuitem_is_sensitive
+ rate_as_5_stars_is_sensitive = menuitem_is_sensitive
+
diff --git a/trunk/dimbola/plugins/remove_photos.ui b/trunk/dimbola/plugins/remove_photos.ui
new file mode 100644
index 0000000..edb9f8a
--- /dev/null
+++ b/trunk/dimbola/plugins/remove_photos.ui
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkDialog" id="remove_photos_dialog">
+ <property name="border_width">5</property>
+ <property name="resizable">False</property>
+ <property name="type_hint">normal</property>
+ <property name="has_separator">False</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="xpad">12</property>
+ <property name="ypad">12</property>
+ <property name="stock">gtk-dialog-warning</property>
+ <property name="icon-size">6</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="padding">6</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox3">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">&lt;b&gt;Remove photos?&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="padding">6</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">The photos will be removed from the database. This operation cannot be undone.
+
+If you choose to also remove files from disk, the photos will be entirely, totally, completely lost, and only black magic and large amounts of bribes to sysadmins will bring them back, and even then it might be impossible. Please don't claim you were not warned.</property>
+ <property name="use_markup">True</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="remove_photos_from_disk_checkbutton">
+ <property name="label" translatable="yes">Remove files from disk also? (They will be completely lost.)</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="padding">12</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button2">
+ <property name="label">gtk-remove</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button1">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-5">button2</action-widget>
+ <action-widget response="-6">button1</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/remove_photos_plugin.py b/trunk/dimbola/plugins/remove_photos_plugin.py
new file mode 100644
index 0000000..36f46ee
--- /dev/null
+++ b/trunk/dimbola/plugins/remove_photos_plugin.py
@@ -0,0 +1,71 @@
+# 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 os
+
+import gtk
+
+import dimbola
+
+
+class RemovePhotos(dimbola.Plugin):
+
+ '''Remove selected files from database, optional also disk.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+
+ def enable(self):
+ self.mwc.add_to_menu('photo_menu', 'remove_photo_menuitem',
+ 'Remove selected photos')
+
+ def disable(self):
+ self.mwc.remove_from_menu('photo_menu', 'remove_photo_menuitem')
+
+ def remove_photo_menuitem_is_sensitive(self):
+ return self.mwc.grid.model.selected
+
+ def on_remove_photo_menuitem_activate(self, menuitem):
+ photoids = self.mwc.grid.model.selected
+ if not photoids:
+ return
+ dialog = self.mwc.widgets['remove_photos_dialog']
+ dialog.set_transient_for(self.mwc.widgets['window'])
+ button = self.mwc.widgets['remove_photos_from_disk_checkbutton']
+ button.set_active(False)
+ dialog.show()
+ response = dialog.run()
+ dialog.hide()
+ if response == gtk.RESPONSE_OK:
+ from_disk = button.get_active()
+ with self.mwc.db:
+ for photoid in photoids:
+ if from_disk:
+ (folderid, basename,
+ c, d) = self.mwc.db.get_basic_photo_metadata(photoid)
+ foldername = self.mwc.db.get_folder_name(folderid)
+ pathname = os.path.join(foldername, basename)
+ os.remove(pathname)
+ self.mwc.db.remove_photo(photoid)
+ old = self.mwc.grid.model.photoids
+ self.mwc.grid.model.photoids = [x for x in old if x not in photoids]
+ self.mwc.load_thumbnails_from_database()
+
diff --git a/trunk/dimbola/plugins/rotate_plugin.py b/trunk/dimbola/plugins/rotate_plugin.py
new file mode 100644
index 0000000..5c63656
--- /dev/null
+++ b/trunk/dimbola/plugins/rotate_plugin.py
@@ -0,0 +1,81 @@
+# 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 os
+import shutil
+
+import gtk
+
+import dimbola
+
+
+class RotatePhotos(dimbola.Plugin):
+
+ '''Rotate selected photos in 90 degree steps.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+
+ def enable(self): # pragma: no cover
+ self.mwc.add_to_menu('photo_menu', 'rotate_90_left_menuitem',
+ 'Rotate photos left')
+ self.mwc.add_to_menu('photo_menu', 'rotate_90_right_menuitem',
+ 'Rotate photos right')
+
+ def disable(self): # pragma: no cover
+ self.mwc.remove_from_menu('file_menu', 'rotate_90_left_menuitem')
+ self.mwc.remove_from_menu('file_menu', 'rotate_90_right_menuitem')
+
+ def new_angle(self, old_angle, step):
+ '''Compute angle given old angle and number of steps.
+
+ Positive steps are clockwise, negative steps are counter-clockwise.
+
+ '''
+ angles = [0, 90, 180, 270]
+ try:
+ i = angles.index(old_angle)
+ except ValueError:
+ return 0
+ else:
+ return (angles + angles)[i - step]
+
+ def rotate(self, step): # pragma: no cover
+ with self.mwc.db:
+ for photoid in self.mwc.grid.model.selected:
+ a, b, c, angle = self.mwc.db.get_basic_photo_metadata(photoid)
+ self.mwc.db.set_rotate(photoid, self.new_angle(angle, step))
+ new_value = self.new_angle(angle, step)
+ for photoid in self.mwc.grid.model.selected:
+ self.mwc.photo_meta_changed(photoid)
+
+ def on_rotate_90_left_menuitem_activate(self, menuitem): #pragma: no cover
+ self.rotate(-1)
+
+ def on_rotate_90_right_menuitem_activate(self, menuitem): #pragma: no cover
+ self.rotate(1)
+
+ def menuitem_is_sensitive(self): # pragma: no cover
+ return self.mwc.grid.model.selected
+
+ rotate_90_left_menuitem_is_sensitive = menuitem_is_sensitive
+ rotate_90_right_menuitem_is_sensitive = menuitem_is_sensitive
+
diff --git a/trunk/dimbola/plugins/rotate_plugin_tests.py b/trunk/dimbola/plugins/rotate_plugin_tests.py
new file mode 100644
index 0000000..f5d790e
--- /dev/null
+++ b/trunk/dimbola/plugins/rotate_plugin_tests.py
@@ -0,0 +1,53 @@
+# 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/>.
+
+
+import unittest
+
+import rotate_plugin
+
+
+class RotatePluginNewAngleTests(unittest.TestCase):
+
+ def setUp(self):
+ self.plugin = rotate_plugin.RotatePhotos(None)
+
+ def test_rotates_bad_angle_to_zero(self):
+ self.assertEqual(self.plugin.new_angle(123, 1), 0)
+
+ def test_rotates_zero_left(self):
+ self.assertEqual(self.plugin.new_angle(0, -1), 90)
+
+ def test_rotates_zero_right(self):
+ self.assertEqual(self.plugin.new_angle(0, 1), 270)
+
+ def test_rotates_ninety_left(self):
+ self.assertEqual(self.plugin.new_angle(90, -1), 180)
+
+ def test_rotates_ninety_right(self):
+ self.assertEqual(self.plugin.new_angle(90, 1), 0)
+
+ def test_rotates_oneeighty_left(self):
+ self.assertEqual(self.plugin.new_angle(180, -1), 270)
+
+ def test_rotates_oneeighty_right(self):
+ self.assertEqual(self.plugin.new_angle(180, 1), 90)
+
+ def test_rotates_twoseventy_left(self):
+ self.assertEqual(self.plugin.new_angle(270, -1), 0)
+
+ def test_rotates_twoseventy_right(self):
+ self.assertEqual(self.plugin.new_angle(270, 1), 180)
+
diff --git a/trunk/dimbola/plugins/search.ui b/trunk/dimbola/plugins/search.ui
new file mode 100644
index 0000000..9adee53
--- /dev/null
+++ b/trunk/dimbola/plugins/search.ui
@@ -0,0 +1,214 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkExpander" id="search_expander">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <object class="GtkVBox" id="vbox9">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkVBox" id="vbox11">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox4">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkCheckButton" id="no_stars_checkbutton">
+ <property name="label" translatable="yes">No stars</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="one_star_checkbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkDrawingArea" id="search_one_star_drawingarea">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="two_stars_checkbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkDrawingArea" id="search_two_stars_drawingarea">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHButtonBox" id="hbuttonbox5">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkCheckButton" id="three_stars_checkbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkDrawingArea" id="search_three_stars_drawingarea">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="four_stars_checkbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkDrawingArea" id="search_four_stars_drawingarea">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="five_stars_checkbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <child>
+ <object class="GtkDrawingArea" id="search_five_stars_drawingarea">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox10">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="label21">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">1</property>
+ <property name="xpad">6</property>
+ <property name="label" translatable="yes">Required tags</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkTextView" id="searchtags_textview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="border_width">1</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">word</property>
+ <property name="cursor_visible">False</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label20">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Search</property>
+ </object>
+ </child>
+ </object>
+ <object class="GtkMenu" id="searchtags_textview_popup">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImageMenuItem" id="remove_search_tag_menuitem">
+ <property name="label">gtk-remove</property>
+ <property name="visible">True</property>
+ <property name="use_underline">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/trunk/dimbola/plugins/search_plugin.py b/trunk/dimbola/plugins/search_plugin.py
new file mode 100644
index 0000000..95abe8c
--- /dev/null
+++ b/trunk/dimbola/plugins/search_plugin.py
@@ -0,0 +1,181 @@
+# 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 gtk
+import pango
+
+import dimbola
+
+
+class Search(dimbola.Plugin):
+
+ '''Search for photos in the currently selected folders.'''
+
+ def __init__(self, mwc):
+ self.mwc = mwc
+ mwc.connect('setup-widgets', self.setup_widgets)
+ self.tagids = set()
+ self.photoids = list()
+
+ def setup_widgets(self, mwc):
+ self.textview = mwc.widgets['searchtags_textview']
+ self.popup = mwc.widgets['searchtags_textview_popup']
+ self.remove_menuitem = mwc.widgets['remove_search_tag_menuitem']
+ self.taglist = dimbola.Taglist(self.textview, self.popup,
+ self.prepare_popup)
+
+ stars = ('one_star', 'two_stars', 'three_stars', 'four_stars',
+ 'five_stars')
+ for n, x in enumerate(stars):
+ w = mwc.widgets['search_%s_drawingarea' % x]
+ height = w.get_style().font_desc.get_size()
+ pixels = max(height / pango.SCALE, 5)
+ w.set_size_request(pixels * (n+1), pixels)
+
+ def enable(self):
+ self.mwc.add_to_sidebar('left_sidebar', 'search_expander',
+ weight=dimbola.MAX_WEIGHT)
+
+ self.enable_signal(self.mwc, 'folder-selection-changed',
+ self.folder_selection_changed)
+ self.enable_signal(self.mwc, 'tagtree-changed',
+ lambda *args: self.refresh_tags())
+
+ def disable(self):
+ self.mwc.remove_from_sidebar('left_sidebar', 'search_expander')
+ self.disable_signals()
+
+ def refresh_tags(self):
+ with self.mwc.db:
+ tags = []
+ for tagid in self.tagids:
+ tagname = self.mwc.db.get_tagname(tagid)
+ if tagname is not None:
+ tags.append((tagname, tagid))
+ tags.sort()
+ self.taglist.set_tags(0, [(y, x) for x, y in tags])
+
+ def prepare_popup(self):
+ self.remove_menuitem.set_sensitive(
+ self.taglist.buf.get_has_selection())
+
+ def draw_stars(self, n_stars, drawingarea):
+ gc = drawingarea.get_style().fg_gc[drawingarea.state]
+ dim = drawingarea.window.get_size()[1]
+ dimbola.draw_stars(n_stars, drawingarea.window, gc, 0, 0, dim)
+
+ def on_search_one_star_drawingarea_expose_event(self, widget, event):
+ self.draw_stars(1, widget)
+
+ def on_search_two_stars_drawingarea_expose_event(self, widget, event):
+ self.draw_stars(2, widget)
+
+ def on_search_three_stars_drawingarea_expose_event(self, widget, event):
+ self.draw_stars(3, widget)
+
+ def on_search_four_stars_drawingarea_expose_event(self, widget, event):
+ self.draw_stars(4, widget)
+
+ def on_search_five_stars_drawingarea_expose_event(self, widget, event):
+ self.draw_stars(5, widget)
+
+ def on_searchtags_textview_button_press_event(self, widget, event):
+ return self.taglist.button_press_event(widget, event)
+
+ def on_remove_search_tag_menuitem_activate(self, *args):
+ photoid, tagid = self.taglist.selected_tag()
+ if tagid in self.tagids:
+ self.tagids.remove(tagid)
+ self.refresh_tags()
+ self.search_photos()
+
+ def on_searchtags_textview_drag_motion(self, *args):
+ return self.taglist.drag_motion(*args)
+
+ def on_searchtags_textview_drag_leave(self, *args):
+ return self.taglist.drag_leave(*args)
+
+ def on_searchtags_textview_drag_data_received(self, *args):
+ tagids = self.taglist.drag_data_received(*args)
+ for tagid in tagids:
+ self.tagids.add(tagid)
+ self.refresh_tags()
+ self.search_photos()
+
+ def select_on_stars(self, photoids, stars_set):
+ with self.mwc.db:
+ for photoid in photoids[:]:
+ a, b, rating, d = self.mwc.db.get_basic_photo_metadata(photoid)
+ if rating not in stars_set:
+ photoids.remove(photoid)
+ return photoids
+
+ def select_on_tags(self, photoids, tagids):
+ with self.mwc.db:
+ for photoid in photoids[:]:
+ photo_tagids = set(self.mwc.db.get_tagids(photoid))
+ if not photo_tagids.issuperset(tagids):
+ photoids.remove(photoid)
+ return photoids
+
+ def on_no_stars_checkbutton_toggled(self, button):
+ self.search_photos()
+
+ on_one_star_checkbutton_toggled = on_no_stars_checkbutton_toggled
+ on_two_stars_checkbutton_toggled = on_no_stars_checkbutton_toggled
+ on_three_stars_checkbutton_toggled = on_no_stars_checkbutton_toggled
+ on_four_stars_checkbutton_toggled = on_no_stars_checkbutton_toggled
+ on_five_stars_checkbutton_toggled = on_no_stars_checkbutton_toggled
+
+ def search_photos(self):
+ '''Set grid's list of photoids to matches for current search.'''
+
+ if self.mwc.db is None:
+ return
+
+ photoids = self.photoids[:]
+
+ buttons = (
+ ('no_stars_checkbutton', 0),
+ ('one_star_checkbutton', 1),
+ ('two_stars_checkbutton', 2),
+ ('three_stars_checkbutton', 3),
+ ('four_stars_checkbutton', 4),
+ ('five_stars_checkbutton', 5),
+ )
+ stars_set = set()
+ for name, stars in buttons:
+ w = self.mwc.widgets[name]
+ if w.get_active():
+ stars_set.add(stars)
+ if not stars_set:
+ stars_set = set(range(6))
+ photoids = self.select_on_stars(photoids, stars_set)
+
+ photoids = self.select_on_tags(photoids, self.tagids)
+
+ self.mwc.grid.model.photoids = list(photoids)
+ self.mwc.load_thumbnails_from_database()
+
+ def folder_selection_changed(self, mwc, folderids, photoids):
+ self.photoids = photoids
+ self.search_photos()
+
diff --git a/trunk/dimbola/plugins/tagtree.ui b/trunk/dimbola/plugins/tagtree.ui
new file mode 100644
index 0000000..c4600d9
--- /dev/null
+++ b/trunk/dimbola/plugins/tagtree.ui
@@ -0,0 +1,166 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkExpander" id="tag_tree_expander">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <object class="GtkVBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkHBox" id="hbox5">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="add_tag_button">
+ <property name="label" translatable="yes">+</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_tag_button">
+ <property name="label" translatable="yes">-</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkTreeView" id="tagtree">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="events">GDK_BUTTON_PRESS_MASK | GDK_STRUCTURE_MASK</property>
+ <property name="headers_visible">False</property>
+ <property name="reorderable">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label16">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">All tags</property>
+ </object>
+ </child>
+ </object>
+ <object class="GtkDialog" id="remove_tag_dialog">
+ <property name="border_width">5</property>
+ <property name="type_hint">normal</property>
+ <property name="has_separator">False</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="dialog-vbox3">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkLabel" id="remove_tag_label">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Really remove tag?</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="dialog-action_area3">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="button3">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button4">
+ <property name="label">gtk-remove</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button3</action-widget>
+ <action-widget response="-5">button4</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkMenu" id="tagtree_menu">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkMenuItem" id="tagtreemenu_add_menuitem">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Add new tag</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="tagtreemenu_rename_menuitem">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Rename tag</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ </object>
+</interface>
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
+