Moved gkofoto library files into a subdirectory of their own again. It
authorJoel Rosdahl <joel@rosdahl.net>
Sun, 8 Aug 2004 07:47:32 +0000 (07:47 +0000)
committerJoel Rosdahl <joel@rosdahl.net>
Sun, 8 Aug 2004 07:47:32 +0000 (07:47 +0000)
is more administrative work to keep them in the same directory as
other .py files, because the library files in that case must be
explicitly listed to avoid that non-library .py files are included in
the library when building a dist.

57 files changed:
MANIFEST.in
setup.py
src/gkofoto/__init__.py [deleted file]
src/gkofoto/albumdialog.py [deleted file]
src/gkofoto/albummembers.py [deleted file]
src/gkofoto/albums.py [deleted file]
src/gkofoto/categories.py [deleted file]
src/gkofoto/categorydialog.py [deleted file]
src/gkofoto/clipboard.py [deleted file]
src/gkofoto/controller.py [deleted file]
src/gkofoto/environment.py [deleted file]
src/gkofoto/generatehtmldialog.py [deleted file]
src/gkofoto/gkofoto/__init__.py [new file with mode: 0644]
src/gkofoto/gkofoto/albumdialog.py [new file with mode: 0644]
src/gkofoto/gkofoto/albummembers.py [new file with mode: 0644]
src/gkofoto/gkofoto/albums.py [new file with mode: 0644]
src/gkofoto/gkofoto/categories.py [new file with mode: 0644]
src/gkofoto/gkofoto/categorydialog.py [new file with mode: 0644]
src/gkofoto/gkofoto/clipboard.py [new file with mode: 0644]
src/gkofoto/gkofoto/controller.py [new file with mode: 0644]
src/gkofoto/gkofoto/environment.py [new file with mode: 0644]
src/gkofoto/gkofoto/generatehtmldialog.py [new file with mode: 0644]
src/gkofoto/gkofoto/handleimagesdialog.py [new file with mode: 0644]
src/gkofoto/gkofoto/imageview.py [new file with mode: 0644]
src/gkofoto/gkofoto/main.py [new file with mode: 0644]
src/gkofoto/gkofoto/mainwindow.py [new file with mode: 0644]
src/gkofoto/gkofoto/menuhandler.py [new file with mode: 0644]
src/gkofoto/gkofoto/mysortedmodel.py [new file with mode: 0644]
src/gkofoto/gkofoto/objectcollection.py [new file with mode: 0644]
src/gkofoto/gkofoto/objectcollectionfactory.py [new file with mode: 0644]
src/gkofoto/gkofoto/objectcollectionview.py [new file with mode: 0644]
src/gkofoto/gkofoto/objectselection.py [new file with mode: 0644]
src/gkofoto/gkofoto/registerimagesdialog.py [new file with mode: 0644]
src/gkofoto/gkofoto/searchresult.py [new file with mode: 0644]
src/gkofoto/gkofoto/singleobjectview.py [new file with mode: 0644]
src/gkofoto/gkofoto/sortableobjectcollection.py [new file with mode: 0644]
src/gkofoto/gkofoto/tableview.py [new file with mode: 0644]
src/gkofoto/gkofoto/taganddescriptiondialog.py [new file with mode: 0644]
src/gkofoto/gkofoto/thumbnailview.py [new file with mode: 0644]
src/gkofoto/handleimagesdialog.py [deleted file]
src/gkofoto/imageview.py [deleted file]
src/gkofoto/main.py [deleted file]
src/gkofoto/mainwindow.py [deleted file]
src/gkofoto/menuhandler.py [deleted file]
src/gkofoto/mysortedmodel.py [deleted file]
src/gkofoto/objectcollection.py [deleted file]
src/gkofoto/objectcollectionfactory.py [deleted file]
src/gkofoto/objectcollectionview.py [deleted file]
src/gkofoto/objectselection.py [deleted file]
src/gkofoto/registerimagesdialog.py [deleted file]
src/gkofoto/searchresult.py [deleted file]
src/gkofoto/singleobjectview.py [deleted file]
src/gkofoto/sortableobjectcollection.py [deleted file]
src/gkofoto/start-in-unix-source.py
src/gkofoto/tableview.py [deleted file]
src/gkofoto/taganddescriptiondialog.py [deleted file]
src/gkofoto/thumbnailview.py [deleted file]

index a09daf3..f0afdcc 100644 (file)
@@ -4,4 +4,7 @@ include doc/gkofoto.1
 include doc/kofoto.1
 include src/gkofoto/glade/*.glade
 include src/gkofoto/icons/*.png
+include src/gkofoto/*.py
+include src/gkofoto/*.pyw
+include src/gkofoto/scripts/*.py
 prune src/web
index 033f3fb..d843385 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ else:
 
 package_dir = {
     "kofoto": "src/lib/kofoto",
-    "gkofoto": "src/gkofoto",
+    "gkofoto": "src/gkofoto/gkofoto",
     }
 packages = [
     "kofoto",
@@ -27,17 +27,18 @@ data_files = [
     ]
 
 if windows_mode:
-    shutil.copy("src/gkofoto/start-on-windows.py", "src/gkofoto/gkofoto-start.pyw")
+    shutil.copy("src/gkofoto/start-on-windows.py",
+                "src/gkofoto/scripts/gkofoto-start.pyw")
     scripts = [
         "src/cmdline/kofoto",
-        "src/gkofoto/gkofoto-start.pyw",
+        "src/gkofoto/scripts/gkofoto-start.pyw",
         "src/gkofoto/scripts/gkofoto-windows-postinstall.py",
         ]
 else:
-    shutil.copy("src/gkofoto/start-on-unix.py", "src/gkofoto/gkofoto")
+    shutil.copy("src/gkofoto/start-on-unix.py", "src/gkofoto/scripts/gkofoto")
     scripts = [
         "src/cmdline/kofoto",
-        "src/gkofoto/gkofoto",
+        "src/gkofoto/scripts/gkofoto",
         ]
 
 versionDict = {}
@@ -57,6 +58,6 @@ setup(
     )
 
 if windows_mode:
-    os.unlink("src/gkofoto/gkofoto-start.pyw")
+    os.unlink("src/gkofoto/scripts/gkofoto-start.pyw")
 else:
-    os.unlink("src/gkofoto/gkofoto")
+    os.unlink("src/gkofoto/scripts/gkofoto")
diff --git a/src/gkofoto/__init__.py b/src/gkofoto/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/gkofoto/albumdialog.py b/src/gkofoto/albumdialog.py
deleted file mode 100644 (file)
index b97070a..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-import gtk
-import string
-import re
-from environment import env
-from gkofoto.taganddescriptiondialog import *
-
-class AlbumDialog(TagAndDescriptionDialog):
-    def __init__(self, title, albumId=None):
-        if albumId is not None:
-            self._album = env.shelf.getAlbum(albumId)
-            tagText = self._album.getTag()
-            descText = self._album.getAttribute(u"title")
-            if descText == None:
-                descText = u""
-        else:
-            self._album = None
-            tagText = u""
-            descText = u""
-        TagAndDescriptionDialog.__init__(self, title, tagText, descText)
-        label = self._widgets.get_widget("titleLabel")
-        label.set_label(u"Title:")
-
-    def _isTagOkay(self, tagString):
-        try:
-           # Check that the tag name is valid.
-           verifyValidAlbumTag(tagString)
-        except BadAlbumTagError:
-            return False
-        try:
-            album = env.shelf.getAlbum(tagString)
-            if album == self._album:
-                # The tag exists, but is same as before.
-                return True
-            else:
-                # The tag is taken by another album.
-                return False
-        except AlbumDoesNotExistError:
-            # The tag didn't exist.
-            return True
diff --git a/src/gkofoto/albummembers.py b/src/gkofoto/albummembers.py
deleted file mode 100644 (file)
index 984dc6b..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-from objectcollection import *
-from environment import env
-
-class AlbumMembers(ObjectCollection):
-
-######################################################################
-### Public functions and constants
-
-    def __init__(self):
-        env.debug("Init AlbumMembers")
-        ObjectCollection.__init__(self)
-        self.__album = None
-
-    def loadAlbum(self, album):
-        env.debug("Loading album: " + album.getTag())
-        self.__album = album
-        self._loadObjectList(album.getChildren())
-
-    def isReorderable(self):
-        return self.__album and self.__album.isMutable()
-
-    def isMutable(self):
-        return self.__album and self.__album.isMutable()
-
-    def getContainer(self):
-        return self.__album
-
-    def cut(self, *foo):
-        self.copy()
-        self.delete()
-
-    def paste(self, *foo):
-        # This method assumes that self.getModel() returns an unsorted
-        # and mutable model.
-        self._freezeViews()
-        locations = list(self.getObjectSelection())
-        newObjects = list(env.clipboard)
-        currentChildren = list(self.__album.getChildren())
-        if len(locations) > 0:
-            locations.sort()
-            insertLocation = locations[0]
-        else:
-            # Insert last.
-            insertLocation = len(currentChildren)
-        self.__album.setChildren(currentChildren[:insertLocation] +
-                                 newObjects +
-                                 currentChildren[insertLocation:])
-        self._insertObjectList(newObjects, insertLocation)
-        # TODO: If the added object is an album, update the album widget.
-        self.getObjectSelection().unselectAll()
-        self._thawViews()
-
-    def delete(self, *foo):
-        # This method assumes that self.getModel() returns an unsorted
-        # and mutable model
-        model = self.getModel()
-        self._freezeViews()
-        albumMembers = list(self.__album.getChildren())
-        locations = list(self.getObjectSelection())
-        locations.sort()
-        locations.reverse()
-        for loc in locations:
-            albumMembers.pop(loc)
-            del model[loc]
-        self.__album.setChildren(albumMembers)
-        self.getObjectSelection().unselectAll()
-        # TODO: If the removed objects are albums, update the album widget.
-        self._thawViews()
-
-######################################################################
-### Private functions
diff --git a/src/gkofoto/albums.py b/src/gkofoto/albums.py
deleted file mode 100644 (file)
index 9f53690..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-import gtk
-import gobject
-import gtk
-from environment import env
-from albumdialog import AlbumDialog
-from menuhandler import *
-from registerimagesdialog import RegisterImagesDialog
-
-class Albums:
-
-###############################################################################
-### Public
-
-    __createAlbumLabel = "Create child album..."
-    __registerImagesLabel = "Register and add images..."
-    __generateHtmlLabel = "Generate HTML..."
-    __destroyAlbumLabel = "Destroy album..."
-    __editAlbumLabel = "Album properties..."
-
-    # TODO This class should probably be splited in a model and a view when/if
-    #      a multiple windows feature is introduced.
-
-    def __init__(self, mainWindow):
-        self._connectedOids = []
-        self.__albumModel = gtk.TreeStore(gobject.TYPE_INT,      # ALBUM_ID
-                                          gobject.TYPE_STRING,   # TAG
-                                          gobject.TYPE_STRING,   # TEXT
-                                          gobject.TYPE_STRING,   # TYPE
-                                          gobject.TYPE_BOOLEAN)  # SELECTABLE
-        self.__mainWindow = mainWindow
-        self.__albumView = env.widgets["albumView"]
-        self.__albumView.set_model(self.__albumModel)
-        self.__albumView.connect("focus-in-event", self._treeViewFocusInEvent)
-        self.__albumView.connect("focus-out-event", self._treeViewFocusOutEvent)
-        renderer = gtk.CellRendererText()
-        column = gtk.TreeViewColumn("Albums", renderer, text=self.__COLUMN_TEXT)
-        column.set_clickable(True)
-        self.__albumView.append_column(column)
-        albumSelection = self.__albumView.get_selection()
-        albumSelection.connect("changed", self._albumSelectionUpdated)
-        albumSelection.set_select_function(self._isSelectable, self.__albumModel)
-        self.__contextMenu = self.__createContextMenu()
-        self.__albumView.connect("button_press_event", self._button_pressed)
-        self.loadAlbumTree()
-        iterator = self.__albumModel.get_iter_first()
-        albumSelection.select_iter(iterator)
-
-    def loadAlbumTree(self):
-        env.shelf.flushObjectCache()
-        self.__albumModel.clear()
-        self.__loadAlbumTreeHelper()
-        env.widgets["albumView"].expand_row(0, False) # Expand root album
-
-
-###############################################################################
-### Callback functions registered by this class but invoked from other classes.
-
-    def _isSelectable(self, path, model):
-        return model[path][self.__COLUMN_SELECTABLE]
-
-    def _albumSelectionUpdated(self, selection=None, load=True):
-        # The focus grab below is made to compensate for what could be
-        # some GTK bug. Without the call, the focus-out-event signal
-        # sometimes isn't emitted for the view widget in the table
-        # view, which messes up the menubar callback registrations.
-        self.__albumView.grab_focus()
-
-        if not selection:
-            selection = self.__albumView.get_selection()
-        albumModel, iterator =  self.__albumView.get_selection().get_selected()
-        createMenuItem = self.__menuGroup[self.__createAlbumLabel]
-        registerMenuItem = self.__menuGroup[self.__registerImagesLabel]
-        generateHtmlMenuItem = self.__menuGroup[self.__generateHtmlLabel]
-        destroyMenuItem = self.__menuGroup[self.__destroyAlbumLabel]
-        editMenuItem = self.__menuGroup[self.__editAlbumLabel]
-        if iterator:
-            albumTag = albumModel.get_value(iterator, self.__COLUMN_TAG)
-            if load:
-                self.__mainWindow.loadQuery("/" + albumTag.decode("utf-8"))
-            album = env.shelf.getAlbum(
-                albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID))
-            createMenuItem.set_sensitive(album.isMutable())
-            env.widgets["menubarCreateAlbumChild"].set_sensitive(album.isMutable())
-            registerMenuItem.set_sensitive(album.isMutable())
-            env.widgets["menubarRegisterAndAddImages"].set_sensitive(album.isMutable())
-            generateHtmlMenuItem.set_sensitive(True)
-            env.widgets["menubarGenerateHtml"].set_sensitive(True)
-            destroyMenuItem.set_sensitive(album != env.shelf.getRootAlbum())
-            env.widgets["menubarDestroy"].set_sensitive(album != env.shelf.getRootAlbum())
-            editMenuItem.set_sensitive(True)
-            env.widgets["menubarProperties"].set_sensitive(True)
-        else:
-            createMenuItem.set_sensitive(False)
-            registerMenuItem.set_sensitive(False)
-            generateHtmlMenuItem.set_sensitive(False)
-            destroyMenuItem.set_sensitive(False)
-            editMenuItem.set_sensitive(False)
-            env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
-            env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
-            env.widgets["menubarGenerateHtml"].set_sensitive(False)
-            env.widgets["menubarDestroy"].set_sensitive(False)
-            env.widgets["menubarProperties"].set_sensitive(False)
-
-    def _createChildAlbum(self, *dummies):
-        dialog = AlbumDialog("Create album")
-        dialog.run(self._createAlbumHelper)
-
-    def _registerImages(self, *dummies):
-        albumModel, iterator =  self.__albumView.get_selection().get_selected()
-        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
-        selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
-        dialog = RegisterImagesDialog(selectedAlbum)
-        if dialog.run() == gtk.RESPONSE_OK:
-            self.__mainWindow.reload() # TODO: don't reload everything.
-        dialog.destroy()
-
-    def _generateHtml(self, *dummies):
-        albumModel, iterator =  self.__albumView.get_selection().get_selected()
-        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
-        selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
-        self.__mainWindow.generateHtml(selectedAlbum)
-
-    def _createAlbumHelper(self, tag, desc):
-        newAlbum = env.shelf.createAlbum(tag)
-        if len(desc) > 0:
-            newAlbum.setAttribute(u"title", desc)
-        albumModel, iterator =  self.__albumView.get_selection().get_selected()
-        if iterator is None:
-            selectedAlbum = env.shelf.getRootAlbum()
-        else:
-            selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
-            selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
-        children = list(selectedAlbum.getChildren())
-        children.append(newAlbum)
-        selectedAlbum.setChildren(children)
-        # TODO The whole tree should not be reloaded
-        self.loadAlbumTree()
-        # TODO update objectCollection?
-
-    def _destroyAlbum(self, *dummies):
-        dialogId = "destroyAlbumsDialog"
-        widgets = gtk.glade.XML(env.gladeFile, dialogId)
-        dialog = widgets.get_widget(dialogId)
-        result = dialog.run()
-        if result == gtk.RESPONSE_OK:
-            albumModel, iterator =  self.__albumView.get_selection().get_selected()
-            selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
-            env.shelf.deleteAlbum(selectedAlbumId)
-            # TODO The whole tree should not be reloaded
-            self.loadAlbumTree()
-            # TODO update objectCollection?
-        dialog.destroy()
-
-    def _editAlbum(self, *dummies):
-        albumModel, iterator =  self.__albumView.get_selection().get_selected()
-        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
-        dialog = AlbumDialog("Edit album", selectedAlbumId)
-        dialog.run(self._editAlbumHelper)
-
-    def _editAlbumHelper(self, tag, desc):
-        albumModel, iterator =  self.__albumView.get_selection().get_selected()
-        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
-        selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
-        selectedAlbum.setTag(tag)
-        if len(desc) > 0:
-            selectedAlbum.setAttribute(u"title", desc)
-        else:
-            selectedAlbum.deleteAttribute(u"title")
-        # TODO The whole tree should not be reloaded
-        self.loadAlbumTree()
-        # TODO update objectCollection?
-
-    def _button_pressed(self, treeView, event):
-        if event.button == 3:
-            self.__contextMenu.popup(None,None,None,event.button,event.time)
-            return True
-        else:
-            return False
-
-    def _treeViewFocusInEvent(self, widget, event):
-        self._albumSelectionUpdated(None, load=False)
-        for widgetName, function in [
-                ("menubarCreateAlbumChild", self._createChildAlbum),
-                ("menubarRegisterAndAddImages", self._registerImages),
-                ("menubarGenerateHtml", self._generateHtml),
-                ("menubarProperties", self._editAlbum),
-                ("menubarDestroy", self._destroyAlbum),
-                ]:
-            w = env.widgets[widgetName]
-            oid = w.connect("activate", function, None)
-            self._connectedOids.append((w, oid))
-
-    def _treeViewFocusOutEvent(self, widget, event):
-        for (widget, oid) in self._connectedOids:
-            widget.disconnect(oid)
-        self._connectedOids = []
-        for widgetName in [
-                "menubarCreateAlbumChild",
-                "menubarRegisterAndAddImages",
-                "menubarGenerateHtml",
-                "menubarProperties",
-                ]:
-            env.widgets[widgetName].set_sensitive(False)
-
-###############################################################################
-### Private
-
-    __COLUMN_ALBUM_ID   = 0
-    __COLUMN_TAG        = 1
-    __COLUMN_TEXT       = 2
-    __COLUMN_TYPE       = 3
-    __COLUMN_SELECTABLE = 4
-
-    def __loadAlbumTreeHelper(self, parentAlbum=None, album=None, visited=[]):
-        if not album:
-            album = env.shelf.getRootAlbum()
-        iterator = self.__albumModel.append(parentAlbum)
-        # TODO Do we have to use iterators here or can we use pygtks simplified syntax?
-        self.__albumModel.set_value(iterator, self.__COLUMN_ALBUM_ID, album.getId())
-        self.__albumModel.set_value(iterator, self.__COLUMN_TYPE, album.getType())
-        self.__albumModel.set_value(iterator, self.__COLUMN_TAG, album.getTag())
-        self.__albumModel.set_value(iterator, self.__COLUMN_SELECTABLE, True)
-        albumTitle = album.getAttribute(u"title")
-        if albumTitle == None or len(albumTitle) < 1:
-            self.__albumModel.set_value(iterator, self.__COLUMN_TEXT, album.getTag())
-        else:
-            self.__albumModel.set_value(iterator, self.__COLUMN_TEXT, albumTitle)
-        if album.getId() not in visited:
-            for child in album.getAlbumChildren():
-                self.__loadAlbumTreeHelper(iterator, child, visited + [album.getId()])
-        else:
-            iterator = self.__albumModel.insert_before(iterator, None)
-            self.__albumModel.set_value(iterator, self.__COLUMN_TEXT, "[...]")
-            self.__albumModel.set_value(iterator, self.__COLUMN_SELECTABLE, False)
-
-    def __createContextMenu(self):
-        self.__menuGroup = MenuGroup()
-        self.__menuGroup.addMenuItem(
-            self.__createAlbumLabel, self._createChildAlbum)
-        self.__menuGroup.addMenuItem(
-            self.__registerImagesLabel, self._registerImages)
-        self.__menuGroup.addMenuItem(
-            self.__generateHtmlLabel, self._generateHtml)
-        self.__menuGroup.addMenuItem(
-            self.__destroyAlbumLabel, self._destroyAlbum)
-        self.__menuGroup.addStockImageMenuItem(
-            self.__editAlbumLabel,
-            gtk.STOCK_PROPERTIES,
-            self._editAlbum)
-        return self.__menuGroup.createGroupMenu()
diff --git a/src/gkofoto/categories.py b/src/gkofoto/categories.py
deleted file mode 100644 (file)
index e579e5e..0000000
+++ /dev/null
@@ -1,549 +0,0 @@
-import gobject
-import gtk
-import string
-
-from environment import env
-from categorydialog import CategoryDialog
-from menuhandler import *
-from kofoto.search import *
-from kofoto.shelf import *
-
-class Categories:
-
-######################################################################
-### Public
-
-    def __init__(self, mainWindow):
-        self.__toggleColumn = None
-        self.__objectCollection = None
-        self.__ignoreSelectEvent = False
-        self.__selectedCategoriesIds  = {}
-        self.__categoryModel = gtk.TreeStore(gobject.TYPE_INT,      # CATEGORY_ID
-                                             gobject.TYPE_STRING,   # DESCRIPTION
-                                             gobject.TYPE_BOOLEAN,  # CONNECTED
-                                             gobject.TYPE_BOOLEAN)  # INCONSISTENT
-        self.__categoryView = env.widgets["categoryView"]
-        self.__categoryView.realize()
-        self.__categoryView.set_model(self.__categoryModel)
-        self.__categoryView.connect("focus-in-event", self._categoryViewFocusInEvent)
-        self.__categoryView.connect("focus-out-event", self._categoryViewFocusOutEvent)
-        self.__mainWindow = mainWindow
-
-        # Create toggle column
-        toggleRenderer = gtk.CellRendererToggle()
-        toggleRenderer.connect("toggled", self._connectionToggled)
-        self.__toggleColumn = gtk.TreeViewColumn("",
-                                                 toggleRenderer,
-                                                 active=self.__COLUMN_CONNECTED,
-                                                 inconsistent=self.__COLUMN_INCONSISTENT)
-        self.__categoryView.append_column(self.__toggleColumn)
-
-        # Create text column
-        textRenderer = gtk.CellRendererText()
-        textColumn = gtk.TreeViewColumn("Category", textRenderer, text=self.__COLUMN_DESCRIPTION)
-        self.__categoryView.append_column(textColumn)
-        self.__categoryView.set_expander_column(textColumn)
-
-        # Create context menu
-        # TODO Is it possible to load a menu from a glade file instead?
-        #      If not, create some helper functions to construct the menu...
-        self._contextMenu = gtk.Menu()
-
-        self._contextMenuGroup = MenuGroup()
-        self._contextMenuGroup.addStockImageMenuItem(
-            self.__cutCategoryLabel,
-            gtk.STOCK_CUT,
-            self._cutCategory)
-        self._contextMenuGroup.addStockImageMenuItem(
-            self.__copyCategoryLabel,
-            gtk.STOCK_COPY,
-            self._copyCategory)
-        self._contextMenuGroup.addStockImageMenuItem(
-            self.__pasteCategoryLabel,
-            gtk.STOCK_PASTE,
-            self._pasteCategory)
-        self._contextMenuGroup.addStockImageMenuItem(
-            self.__destroyCategoryLabel,
-            gtk.STOCK_DELETE,
-            self._deleteCategories)
-        self._contextMenuGroup.addMenuItem(
-            self.__disconnectCategoryLabel,
-            self._disconnectCategory)
-        self._contextMenuGroup.addMenuItem(
-            self.__createChildCategoryLabel,
-            self._createChildCategory)
-        self._contextMenuGroup.addMenuItem(
-            self.__createRootCategoryLabel,
-            self._createRootCategory)
-        self._contextMenuGroup.addStockImageMenuItem(
-            self.__propertiesLabel,
-            gtk.STOCK_PROPERTIES,
-            self._editProperties)
-
-        for item in self._contextMenuGroup:
-            self._contextMenu.append(item)
-
-        env.widgets["categorySearchButton"].set_sensitive(False)
-
-        # Init menubar items.
-        env.widgets["menubarDisconnectFromParent"].connect(
-            "activate", self._disconnectCategory, None)
-        env.widgets["menubarCreateChild"].connect(
-            "activate", self._createChildCategory, None)
-        env.widgets["menubarCreateRoot"].connect(
-            "activate", self._createRootCategory, None)
-
-        # Init selection functions
-        categorySelection = self.__categoryView.get_selection()
-        categorySelection.set_mode(gtk.SELECTION_MULTIPLE)
-        categorySelection.set_select_function(self._selectionFunction, None)
-        categorySelection.connect("changed", self._categorySelectionChanged)
-
-        # Connect the rest of the UI events
-        self.__categoryView.connect("button_press_event", self._button_pressed)
-        self.__categoryView.connect("button_release_event", self._button_released)
-        self.__categoryView.connect("row-activated", self._rowActivated)
-        env.widgets["categorySearchButton"].connect('clicked', self._executeQuery)
-
-        self.loadCategoryTree()
-
-
-    def loadCategoryTree(self):
-        self.__categoryModel.clear()
-        env.shelf.flushCategoryCache()
-        for category in self.__sortCategories(env.shelf.getRootCategories()):
-            self.__loadCategorySubTree(None, category)
-        if self.__objectCollection is not None:
-            self.objectSelectionChanged()
-
-    def setCollection(self, objectCollection):
-        if self.__objectCollection is not None:
-            self.__objectCollection.getObjectSelection().removeChangedCallback(self.objectSelectionChanged)
-        self.__objectCollection = objectCollection
-        self.__objectCollection.getObjectSelection().addChangedCallback(self.objectSelectionChanged)
-        self.objectSelectionChanged()
-
-    def objectSelectionChanged(self, objectSelection=None):
-        self.__updateToggleColumn()
-        self.__updateContextMenu()
-        self.__expandAndCollapseRows(env.widgets["autoExpand"].get_active(),
-                                     env.widgets["autoCollapse"].get_active())
-
-
-###############################################################################
-### Callback functions registered by this class but invoked from other classes.
-
-    def _executeQuery(self, *foo):
-        query = self.__buildQueryFromSelection()
-        if query:
-            self.__mainWindow.loadQuery(query)
-
-    def _categoryViewFocusInEvent(self, widget, event):
-        self._menubarOids = []
-        for widgetName, function in [
-                ("menubarCut", lambda *x: self._cutCategory(None, None)),
-                ("menubarCopy", lambda *x: self._copyCategory(None, None)),
-                ("menubarPaste", lambda *x: self._pasteCategory(None, None)),
-                ("menubarDestroy", lambda *x: self._deleteCategories(None, None)),
-                ("menubarClear", lambda *x: widget.get_selection().unselect_all()),
-                ("menubarSelectAll", lambda *x: widget.get_selection().select_all()),
-                ("menubarProperties", lambda *x: self._editProperties(None, None)),
-                ]:
-            w = env.widgets[widgetName]
-            oid = w.connect("activate", function)
-            self._menubarOids.append((w, oid))
-        self.__updateContextMenu()
-
-    def _categoryViewFocusOutEvent(self, widget, event):
-        for (widget, oid) in self._menubarOids:
-            widget.disconnect(oid)
-
-    def _categorySelectionChanged(self, selection):
-        selectedCategoryRows = []
-        selection = self.__categoryView.get_selection()
-        # TODO replace with "get_selected_rows()" when it is introduced in Pygtk 2.2 API
-        selection.selected_foreach(lambda model,
-                                   path,
-                                   iter:
-                                   selectedCategoryRows.append(model[path]))
-        self.__selectedCategoriesIds  = {}
-
-        for categoryRow in selectedCategoryRows:
-            cid = categoryRow[self.__COLUMN_CATEGORY_ID]
-            # row.parent method gives assertion failed, dont know why. Using workaround instead.
-            parentPath = categoryRow.path[:-1]
-            if parentPath:
-                parentId = categoryRow.model[parentPath][self.__COLUMN_CATEGORY_ID]
-            else:
-                parentId = None
-            try:
-                 self.__selectedCategoriesIds[cid].append(parentId)
-            except KeyError:
-                 self.__selectedCategoriesIds[cid] = [parentId]
-        self.__updateContextMenu()
-        env.widgets["categorySearchButton"].set_sensitive(
-            len(selectedCategoryRows) > 0)
-
-    def _connectionToggled(self, renderer, path):
-        categoryRow = self.__categoryModel[path]
-        category = env.shelf.getCategory(categoryRow[self.__COLUMN_CATEGORY_ID])
-        if categoryRow[self.__COLUMN_INCONSISTENT] \
-               or not categoryRow[self.__COLUMN_CONNECTED]:
-            for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
-                try:
-                    obj.addCategory(category)
-                except CategoryPresentError:
-                    # The object was already connected to the category
-                    pass
-            categoryRow[self.__COLUMN_INCONSISTENT] = False
-            categoryRow[self.__COLUMN_CONNECTED] = True
-        else:
-            for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
-                obj.removeCategory(category)
-            categoryRow[self.__COLUMN_CONNECTED] = False
-            categoryRow[self.__COLUMN_INCONSISTENT] = False
-        self.__updateToggleColumn()
-
-    def _button_pressed(self, treeView, event):
-        if event.button == 3:
-            self._contextMenu.popup(None,None,None,event.button,event.time)
-            return True
-        rec = self.__categoryView.get_cell_area(0, self.__toggleColumn)
-        if event.x <= (rec.x + rec.width):
-            # Ignore selection event since the user clicked on the toggleColumn.
-            self.__ignoreSelectEvent = True
-        return False
-
-    def _button_released(self, treeView, event):
-        self.__ignoreSelectEvent = False
-        return False
-
-    def _rowActivated(self, a, b, c):
-        # TODO What should happen if the user dubble-click on a category?
-        pass
-
-    def _copyCategory(self, item, data):
-        cc = ClipboardCategories()
-        cc.type = cc.COPY
-        cc.categories = self.__selectedCategoriesIds
-        env.clipboard.setCategories(cc)
-
-    def _cutCategory(self, item, data):
-        cc = ClipboardCategories()
-        cc.type = cc.CUT
-        cc.categories = self.__selectedCategoriesIds
-        env.clipboard.setCategories(cc)
-
-    def _pasteCategory(self, item, data):
-        assert env.clipboard.hasCategories()
-        clipboardCategories = env.clipboard[0]
-        env.clipboard.clear()
-        try:
-            for (categoryId, previousParentIds) in clipboardCategories.categories.items():
-                for newParentId in self.__selectedCategoriesIds:
-                    if clipboardCategories.type == ClipboardCategories.COPY:
-                        self.__connectChildToCategory(categoryId, newParentId)
-                        for parentId in previousParentIds:
-                            if parentId is None:
-                                self.__disconnectChildHelper(categoryId, None,
-                                                             None, self.__categoryModel)
-                    else:
-                        if newParentId in previousParentIds:
-                            previousParentIds.remove(newParentId)
-                        else:
-                            self.__connectChildToCategory(categoryId, newParentId)
-                        for parentId in previousParentIds:
-                            if parentId is None:
-                                self.__disconnectChildHelper(categoryId, None,
-                                                             None, self.__categoryModel)
-                            else:
-                                self.__disconnectChild(categoryId, parentId)
-        except CategoryLoopError:
-            dialog = gtk.MessageDialog(
-                type=gtk.MESSAGE_ERROR,
-                buttons=gtk.BUTTONS_OK,
-                message_format="Category loop detected.")
-            dialog.run()
-            dialog.destroy()
-        self.__expandAndCollapseRows(False, False)
-
-    def _createRootCategory(self, item, data):
-        dialog = CategoryDialog("Create root category")
-        dialog.run(self._createRootCategoryHelper)
-
-    def _createRootCategoryHelper(self, tag, desc):
-        category = env.shelf.createCategory(tag, desc)
-        self.__loadCategorySubTree(None, category)
-
-    def _createChildCategory(self, item, data):
-        dialog = CategoryDialog("Create child category")
-        dialog.run(self._createChildCategoryHelper)
-
-    def _createChildCategoryHelper(self, tag, desc):
-        newCategory = env.shelf.createCategory(tag, desc)
-        for selectedCategoryId in self.__selectedCategoriesIds:
-            self.__connectChildToCategory(newCategory.getId(), selectedCategoryId)
-        self.__expandAndCollapseRows(False, False)
-
-    def _deleteCategories(self, item, data):
-        dialogId = "destroyCategoriesDialog"
-        widgets = gtk.glade.XML(env.gladeFile, dialogId)
-        dialog = widgets.get_widget(dialogId)
-        result = dialog.run()
-        if result == gtk.RESPONSE_OK:
-            for categoryId in self.__selectedCategoriesIds:
-                category = env.shelf.getCategory(categoryId)
-                for child in list(category.getChildren()):
-                    # The backend automatically disconnects childs
-                    # when a category is deleted, but we do it ourself
-                    # to make sure that the treeview widget is
-                    # updated.
-                    self.__disconnectChild(child.getId(), categoryId)
-                env.shelf.deleteCategory(categoryId)
-                env.shelf.flushCategoryCache()
-                self.__forEachCategoryRow(
-                    self.__deleteCategoriesHelper, categoryId)
-        dialog.destroy()
-
-    def __deleteCategoriesHelper(self, categoryRow, categoryIdToDelete):
-        if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryIdToDelete:
-            self.__categoryModel.remove(categoryRow.iter)
-
-    def _disconnectCategory(self, item, data):
-        for (categoryId, parentIds) in self.__selectedCategoriesIds.items():
-            for parentId in parentIds:
-                if not parentId == None: # Not possible to disconnect root categories
-                    self.__disconnectChild(categoryId, parentId)
-
-    def _editProperties(self, item, data):
-        for categoryId in self.__selectedCategoriesIds:
-            dialog = CategoryDialog("Change properties", categoryId)
-            dialog.run(self._editPropertiesHelper, data=categoryId)
-
-    def _editPropertiesHelper(self, tag, desc, categoryId):
-         category = env.shelf.getCategory(categoryId)
-         category.setTag(tag)
-         category.setDescription(desc)
-         env.shelf.flushCategoryCache()
-         self.__forEachCategoryRow(self.__updatePropertiesFromShelf, categoryId)
-
-    def _selectionFunction(self, path, b):
-        return not self.__ignoreSelectEvent
-
-
-######################################################################
-### Private
-
-    __cutCategoryLabel = "Cut"
-    __copyCategoryLabel = "Copy"
-    __pasteCategoryLabel = "Paste as child(ren)"
-    __destroyCategoryLabel = "Destroy..."
-    __disconnectCategoryLabel = "Disconnect from parent"
-    __createChildCategoryLabel = "Create child"
-    __createRootCategoryLabel = "Create root"
-    __propertiesLabel = "Properties"
-
-    __COLUMN_CATEGORY_ID  = 0
-    __COLUMN_DESCRIPTION  = 1
-    __COLUMN_CONNECTED    = 2
-    __COLUMN_INCONSISTENT = 3
-
-    def __loadCategorySubTree(self, parent, category):
-        # TODO Do we have to use iterators here or can we use pygtks simplified syntax?
-        iterator = self.__categoryModel.iter_children(parent)
-        while (iterator != None and
-               self.__categoryModel.get_value(iterator, self.__COLUMN_DESCRIPTION) <
-                   category.getDescription()):
-            iterator = self.__categoryModel.iter_next(iterator)
-        iterator = self.__categoryModel.insert_before(parent, iterator)
-        self.__categoryModel.set_value(iterator, self.__COLUMN_CATEGORY_ID, category.getId())
-        self.__categoryModel.set_value(iterator, self.__COLUMN_DESCRIPTION, category.getDescription())
-        self.__categoryModel.set_value(iterator, self.__COLUMN_CONNECTED, False)
-        self.__categoryModel.set_value(iterator, self.__COLUMN_INCONSISTENT, False)
-        for child in self.__sortCategories(category.getChildren()):
-            self.__loadCategorySubTree(iterator, child)
-
-    def __buildQueryFromSelection(self):
-        if env.widgets["categoriesOr"].get_active():
-            operator = " or "
-        else:
-            operator = " and "
-        return operator.join([env.shelf.getCategory(x).getTag()
-                              for x in self.__selectedCategoriesIds])
-
-    def __updateContextMenu(self):
-        # TODO Create helper functions to use from this method
-        menubarWidgetNames = [
-                "menubarCut",
-                "menubarCopy",
-                "menubarPaste",
-                "menubarDestroy",
-                "menubarProperties",
-                "menubarDisconnectFromParent",
-                "menubarCreateChild",
-                "menubarCreateRoot",
-                ]
-        if len(self.__selectedCategoriesIds) == 0:
-            self._contextMenuGroup.disable()
-            for widgetName in menubarWidgetNames:
-                env.widgets[widgetName].set_sensitive(False)
-            self._contextMenuGroup[
-                self.__createRootCategoryLabel].set_sensitive(True)
-            env.widgets["menubarCreateRoot"].set_sensitive(True)
-        else:
-            self._contextMenuGroup.enable()
-            for widgetName in menubarWidgetNames:
-                env.widgets[widgetName].set_sensitive(True)
-            if not env.clipboard.hasCategories():
-                self._contextMenuGroup[
-                    self.__pasteCategoryLabel].set_sensitive(False)
-                env.widgets["menubarPaste"].set_sensitive(False)
-        propertiesItem = self._contextMenuGroup[self.__propertiesLabel]
-        propertiesItemSensitive = len(self.__selectedCategoriesIds) == 1
-        propertiesItem.set_sensitive(propertiesItemSensitive)
-        env.widgets["menubarProperties"].set_sensitive(propertiesItemSensitive)
-
-    def __updateToggleColumn(self):
-        # find out which categories are connected, not connected or
-        # partitionally connected to selected objects
-        nrSelectedObjectsInCategory = {}
-        nrSelectedObjects = 0
-        for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
-            nrSelectedObjects += 1
-            for category in obj.getCategories():
-                categoryId = category.getId()
-                try:
-                    nrSelectedObjectsInCategory[categoryId] += 1
-                except KeyError:
-                        nrSelectedObjectsInCategory[categoryId] = 1
-        self.__forEachCategoryRow(self.__updateToggleColumnHelper,
-                                  (nrSelectedObjects, nrSelectedObjectsInCategory))
-
-    def __updateToggleColumnHelper(self,
-                                   categoryRow,
-                                   (nrSelectedObjects, nrSelectedObjectsInCategory)):
-        categoryId = categoryRow[self.__COLUMN_CATEGORY_ID]
-        if categoryId in nrSelectedObjectsInCategory:
-            if nrSelectedObjectsInCategory[categoryId] < nrSelectedObjects:
-                # Some of the selected objects are connected to the category
-                categoryRow[self.__COLUMN_CONNECTED] = False
-                categoryRow[self.__COLUMN_INCONSISTENT] = True
-            else:
-                # All of the selected objects are connected to the category
-                categoryRow[self.__COLUMN_CONNECTED] = True
-                categoryRow[self.__COLUMN_INCONSISTENT] = False
-        else:
-            # None of the selected objects are connected to the category
-            categoryRow[self.__COLUMN_CONNECTED] = False
-            categoryRow[self.__COLUMN_INCONSISTENT] = False
-
-    def __forEachCategoryRow(self, function, data=None, categoryRows=None):
-        # We can't use gtk.TreeModel.foreach() since it does not pass a row
-        # to the callback function.
-        if not categoryRows:
-            categoryRows=self.__categoryModel
-        for categoryRow in categoryRows:
-            function(categoryRow, data)
-            self.__forEachCategoryRow(function, data, categoryRow.iterchildren())
-
-    def __expandAndCollapseRows(self, autoExpand, autoCollapse, categoryRows=None):
-        if categoryRows is None:
-            categoryRows=self.__categoryModel
-        someRowsExpanded = False
-        for categoryRow in categoryRows:
-            expandThisRow = False
-            # Expand all rows that are selected or has expanded childs
-            childRowsExpanded = self.__expandAndCollapseRows(autoExpand,
-                                                            autoCollapse,
-                                                            categoryRow.iterchildren())
-            if (childRowsExpanded
-                or self.__categoryView.get_selection().path_is_selected(categoryRow.path)):
-                expandThisRow = True
-            # Auto expand all rows that has a checked toggle
-            if autoExpand:
-                if (categoryRow[self.__COLUMN_CONNECTED]
-                    or categoryRow[self.__COLUMN_INCONSISTENT]):
-                    expandThisRow = True
-            if expandThisRow:
-                for a in range(len(categoryRow.path)):
-                    self.__categoryView.expand_row(categoryRow.path[:a+1], False)
-                someRowsExpanded = True
-            # Auto collapse?
-            elif autoCollapse:
-                self.__categoryView.collapse_row(categoryRow.path)
-        return someRowsExpanded
-
-    def __connectChildToCategory(self, childId, parentId):
-        try:
-            # Update shelf
-            childCategory = env.shelf.getCategory(childId)
-            parentCategory = env.shelf.getCategory(parentId)
-            parentCategory.connectChild(childCategory)
-            env.shelf.flushCategoryCache()
-            # Update widget modell
-            # If we reload the whole category tree from the shelf, we would lose
-            # the widgets information about current selected categories,
-            # expanded categories and the widget's scroll position. Hence,
-            # we update our previously loaded model instead.
-            self.__connectChildToCategoryHelper(parentId,
-                                                childCategory,
-                                                self.__categoryModel)
-        except CategoriesAlreadyConnectedError:
-            # This is okay.
-            pass
-
-    def __connectChildToCategoryHelper(self, parentId, childCategory, categoryRows):
-        for categoryRow in categoryRows:
-            if categoryRow[self.__COLUMN_CATEGORY_ID] == parentId:
-                self.__loadCategorySubTree(categoryRow.iter, childCategory)
-            else:
-                self.__connectChildToCategoryHelper(parentId, childCategory, categoryRow.iterchildren())
-
-    def __disconnectChild(self, childId, parentId):
-        # Update shelf
-        childCategory = env.shelf.getCategory(childId)
-        parentCategory = env.shelf.getCategory(parentId)
-        if childCategory in env.shelf.getRootCategories():
-            alreadyWasRootCategory = True
-        else:
-            alreadyWasRootCategory = False
-        parentCategory.disconnectChild(childCategory)
-        env.shelf.flushCategoryCache()
-        # Update widget modell.
-        # If we reload the whole category tree from the shelf, we would lose
-        # the widgets information about current selected categories,
-        # expanded categories and the widget's scroll position. Hence,
-        # we update our previously loaded model instead.
-        self.__disconnectChildHelper(childId,
-                                    parentId,
-                                    None,
-                                    self.__categoryModel)
-        if not alreadyWasRootCategory:
-            for c in env.shelf.getRootCategories():
-                if c.getId() == childCategory.getId():
-                    self.__loadCategorySubTree(None, childCategory)
-                    break
-
-    def __disconnectChildHelper(self, wantedChildId, wantedParentId,
-                                parentId, categoryRows):
-        for categoryRow in categoryRows:
-            cid = categoryRow[self.__COLUMN_CATEGORY_ID]
-            if cid == wantedChildId and parentId == wantedParentId:
-                self.__categoryModel.remove(categoryRow.iter)
-            self.__disconnectChildHelper(wantedChildId, wantedParentId, cid, categoryRow.iterchildren())
-
-    def __updatePropertiesFromShelf(self, categoryRow, categoryId):
-        if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryId:
-            category = env.shelf.getCategory(categoryId)
-            categoryRow[self.__COLUMN_DESCRIPTION] = category.getDescription()
-
-    def __sortCategories(self, categoryIter):
-        categories = list(categoryIter)
-        categories.sort(lambda x, y: cmp(x.getDescription(), y.getDescription()))
-        return categories
-
-class ClipboardCategories:
-    COPY = 1
-    CUT = 2
-    categories = None
-    type = None
diff --git a/src/gkofoto/categorydialog.py b/src/gkofoto/categorydialog.py
deleted file mode 100644 (file)
index fdbb86e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-import gtk
-import string
-import re
-from environment import env
-from gkofoto.taganddescriptiondialog import *
-
-class CategoryDialog(TagAndDescriptionDialog):
-    def __init__(self, title, categoryId=None):
-        if categoryId:
-            self._category = env.shelf.getCategory(categoryId)
-            tagText = self._category.getTag()
-            descText = self._category.getDescription()
-        else:
-            self._category = None
-            tagText = u""
-            descText = u""
-        TagAndDescriptionDialog.__init__(self, title, tagText, descText)
-
-    def _isTagOkay(self, tagString):
-        try:
-           # Check that the tag name is valid.
-           verifyValidCategoryTag(tagString)
-        except BadCategoryTagError:
-            return False
-        try:
-            category = env.shelf.getCategory(tagString)
-            if category == self._category:
-                # The tag exists, but is same as before.
-                return True
-            else:
-                # The tag is taken by another category.
-                return False
-        except CategoryDoesNotExistError:
-            # The tag didn't exist.
-            return True
diff --git a/src/gkofoto/clipboard.py b/src/gkofoto/clipboard.py
deleted file mode 100644 (file)
index 91ee2f5..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-from sets import Set
-from environment import env
-from kofoto.shelf import Image
-from kofoto.shelf import Album
-from categories import ClipboardCategories
-
-class Clipboard:
-
-    # TYPES
-    OBJECTS    = 0 # shelf.Album and shelf.Image
-    CATEGORIES = 1 # shelf.Category
-
-    def __init__(self):
-        self.__changedCallbacks = Set()
-        self.clear()
-
-    def addChangedCallback(self, callback):
-        self.__changedCallbacks.add(callback)
-
-    def removeChangedCallback(self, callback):
-        self.__changedCallbacks.remove(callback)
-
-    def setObjects(self, iter):
-        self.__objects = []
-        self.__types = Clipboard.OBJECTS
-        for object in iter:
-            if (isinstance(object, Image) or isinstance(object, Album)):
-                self.__objects.append(object)
-            else:
-                self.clear()
-                raise "Object is not an Image nor an Album" # TODO
-        self.__invokeChangedCallbacks()
-
-    def setCategories(self, clipboardCategories):
-        self.__objects = []
-        if isinstance(clipboardCategories, ClipboardCategories):
-            self.__objects.append(clipboardCategories)
-        else:
-            self.clear()
-            raise "Object is not a ClipboardCategories" # TODO
-        self.__types = Clipboard.CATEGORIES
-        self.__invokeChangedCallbacks()
-
-    def clear(self):
-        self.__objects = []
-        self.__types = None
-        self.__invokeChangedCallbacks()
-
-    def hasCategories(self):
-        return (self.__types == Clipboard.CATEGORIES and len(self.__objects) > 0)
-
-    def hasObjects(self):
-        return (self.__types == Clipboard.OBJECTS and len(self.__objects) > 0)
-
-    def __len__(self):
-        return len(self.__objects)
-
-    def __iter__(self):
-        return self.__objects.__iter__()
-
-    def __getitem__(self, index):
-        return self.__objects.__getitem__(index)
-
-    def __invokeChangedCallbacks(self):
-        for callback in self.__changedCallbacks:
-            callback(self)
diff --git a/src/gkofoto/controller.py b/src/gkofoto/controller.py
deleted file mode 100644 (file)
index 6ae19d7..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-import sys
-import gtk
-from kofoto.shelf import ShelfLockedError
-from gkofoto.mainwindow import MainWindow
-from gkofoto.environment import env
-
-class Controller:
-    def __init__(self):
-        self.__clipboard = None
-
-    def start(self, setupOk):
-        if setupOk:
-            try:
-                env.shelf.begin()
-            except ShelfLockedError, e:
-                env.startupNotices += [
-                    "Error: Could not open shelf \"%s\"." % e +
-                    " Another process is locking it.\n"]
-                setupOk = False
-        if env.startupNotices:
-            if setupOk:
-                dialogtype = gtk.MESSAGE_INFO
-            else:
-                dialogtype = gtk.MESSAGE_ERROR
-            dialog = gtk.MessageDialog(
-                type=dialogtype,
-                buttons=gtk.BUTTONS_OK,
-                message_format="".join(env.startupNotices))
-            if setupOk:
-                # Doesn't work with x[0].destroy(). Don't know why.
-                dialog.connect("response", lambda *x: x[0].hide())
-            else:
-                # Doesn't work with gtk.main_quit(). Don't know why.
-                dialog.connect("response", lambda *x: sys.exit(1))
-            dialog.run()
-        if setupOk:
-            self.__mainWindow = MainWindow()
-            env.widgets["mainWindow"].connect("destroy", self.quit, False)
-            env.widgets["mainWindow"].show()
-        gtk.main()
-
-    def quit(self, app, cancelButton=True):
-        if env.shelf.isModified():
-            widgets = gtk.glade.XML(env.gladeFile, "quitDialog")
-            quitDialog = widgets.get_widget("quitDialog")
-            if not cancelButton:
-                widgets.get_widget("cancel").set_sensitive(False)
-            result = quitDialog.run()
-            if result == 0:
-                env.shelf.commit()
-                gtk.main_quit()
-            elif result == 1:
-                env.shelf.rollback()
-                gtk.main_quit()
-            else:
-                quitDialog.destroy()
-                return
-        else:
-            env.shelf.rollback()
-            gtk.main_quit()
-
-    def save(self, app):
-        env.shelf.commit()
-        env.shelf.begin()
-
-    def revert(self, app):
-        dialog = gtk.MessageDialog(
-            type=gtk.MESSAGE_QUESTION,
-            buttons=gtk.BUTTONS_YES_NO,
-            message_format="Revert to the previously saved state and lose all changes?")
-        if dialog.run() == gtk.RESPONSE_YES:
-            env.shelf.rollback()
-            env.shelf.begin()
-            self.__mainWindow.reload()
-        dialog.destroy()
diff --git a/src/gkofoto/environment.py b/src/gkofoto/environment.py
deleted file mode 100644 (file)
index c804fd2..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-import sys
-import os
-import getopt
-import locale
-import re
-
-import pygtk
-pygtk.require('2.0')
-import gtk
-import gobject
-import gtk.gdk
-import gtk.glade
-
-from kofoto.clientenvironment import *
-from kofoto.common import *
-from kofoto.shelf import *
-from kofoto.config import *
-from kofoto.imagecache import *
-
-class WidgetsWrapper:
-    def __init__(self):
-        self.widgets = gtk.glade.XML(env.gladeFile, "mainWindow")
-
-    def __getitem__(self, key):
-        return self.widgets.get_widget(key)
-
-class Environment(ClientEnvironment):
-    def __init__(self):
-        ClientEnvironment.__init__(self)
-        self.startupNotices = []
-
-    def setup(self, bindir, isDebug=False, configFileLocation=None,
-              shelfLocation=None):
-        try:
-            ClientEnvironment.setup(self, configFileLocation, shelfLocation)
-        except ClientEnvironmentError, e:
-            self.startupNotices += [e[0]]
-            return False
-
-        self.isDebug = isDebug
-        self.thumbnailSize = self.config.getcoordlist(
-            "gkofoto", "thumbnail_size_limit")[0]
-        self.defaultTableViewColumns = re.findall(
-            "\S+",
-            self.config.get("gkofoto", "default_table_columns"))
-        self.defaultSortColumn = self.config.get(
-            "gkofoto", "default_sort_column")
-        self.openCommand = self.config.get(
-            "gkofoto", "open_command", True)
-        self.rotateRightCommand = self.config.get(
-            "gkofoto", "rotate_right_command", True)
-        self.rotateLeftCommand = self.config.get(
-            "gkofoto", "rotate_left_command", True)
-
-        dataDir = os.path.join(bindir, "..", "share", "gkofoto")
-        if not os.path.exists(dataDir):
-            dataDir = bindir
-        self.iconDir = os.path.join(dataDir, "icons")
-        self.gladeFile = os.path.join(dataDir, "glade", "gkofoto.glade")
-        self.albumIconFileName = os.path.join(self.iconDir, "album.png")
-        self.albumIconPixbuf = gtk.gdk.pixbuf_new_from_file(self.albumIconFileName)
-        self.loadingPixbuf = self.albumIconPixbuf # TODO: create another icon with a hour-glass or something
-        self.unknownImageIconFileName = os.path.join(self.iconDir, "unknownimage.png")
-        self.unknownImageIconPixbuf = gtk.gdk.pixbuf_new_from_file(self.unknownImageIconFileName)
-        from clipboard import Clipboard
-        self.clipboard = Clipboard()
-
-        self.widgets = WidgetsWrapper()
-
-        return True
-
-    def _writeInfo(self, infoString):
-        self.startupNotices += [infoString]
-
-    def debug(self, msg):
-        if self.isDebug:
-            print msg
-
-    def enter(self, method):
-        if self.isDebug:
-            print "-->", method
-
-    def exit(self, method):
-        if self.isDebug:
-            print "<--", method
-
-    def assertUnicode(self, obj):
-        assert isinstance(obj, unicode), \
-               "%s is not a unicode object: \"%s\"" % (type(obj), obj)
-
-env = Environment()
diff --git a/src/gkofoto/generatehtmldialog.py b/src/gkofoto/generatehtmldialog.py
deleted file mode 100644 (file)
index f6022d4..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-import gtk
-import os
-import re
-from sets import Set
-from environment import env
-import kofoto.generate
-
-class GenerateHTMLDialog:
-    def __init__(self, album):
-        self.album = album
-        self.widgets = gtk.glade.XML(
-            env.gladeFile, "generateHtmlDialog")
-        self.dialog = self.widgets.get_widget("generateHtmlDialog")
-        self.browseButton = self.widgets.get_widget("browseButton")
-        self.cancelButton = self.widgets.get_widget("cancelButton")
-        self.directoryTextEntry = self.widgets.get_widget("directoryTextEntry")
-        self.generateButton = self.widgets.get_widget("generateButton")
-
-        self.browseButton.connect("clicked", self._onBrowse)
-        self.cancelButton.connect("clicked", self._onCancel)
-        self.generateButton.connect("clicked", self._onGenerate)
-
-        self.directoryTextEntry.connect(
-            "changed", self._onDirectoryTextEntryModified)
-
-        self.generateButton.set_sensitive(False)
-
-    def run(self):
-        self.dialog.show()
-
-    def _onDirectoryTextEntryModified(self, *unused):
-        self.generateButton.set_sensitive(
-            os.path.isdir(self.directoryTextEntry.get_text()))
-
-    def _onBrowse(self, *unused):
-        directorySelectedInDirList = False
-        dirDialog = gtk.FileSelection(title="Choose directory")
-        dirDialog.file_list.set_sensitive(False)
-        dirDialog.fileop_del_file.set_sensitive(False)
-        dirDialog.fileop_ren_file.set_sensitive(False)
-        if dirDialog.run() == gtk.RESPONSE_OK:
-            model, iterator = dirDialog.dir_list.get_selection().get_selected()
-            directory = dirDialog.get_filename()
-            if iterator:
-                directory = os.path.join(
-                    directory, model.get_value(iterator, 0))
-            self.directoryTextEntry.set_text(directory)
-        dirDialog.destroy()
-
-    def _onCancel(self, *unused):
-        self.dialog.destroy()
-
-    def _onGenerate(self, *unused):
-        for widget in [self.directoryTextEntry, self.browseButton,
-                       self.cancelButton, self.generateButton]:
-            widget.set_sensitive(False)
-        self._generate(self.directoryTextEntry.get_text())
-        self.dialog.destroy()
-
-    def _generate(self, directoryName):
-        # TODO: Rewrite this gross hack.
-
-        def outputParser(string):
-            m = re.match(
-                r"Creating album (\S+) \((\d+) of (\d+)\)",
-                string,
-                re.UNICODE)
-            if m:
-                progressBar.set_text(m.group(1).decode("latin1"))
-                progressBar.set_fraction(
-                    (int(m.group(2)) - 1) / float(m.group(3)))
-                while gtk.events_pending():
-                    gtk.main_iteration()
-
-        progressBar = self.widgets.get_widget("progressBar")
-
-        env.out = outputParser
-        env.verbose = True
-        env.thumbnailsizelimit = env.config.getcoordlist(
-            "album generation", "thumbnail_size_limit")[0]
-        env.defaultsizelimit = env.config.getcoordlist(
-            "album generation", "default_image_size_limit")[0]
-
-        imgsizesval = env.config.getcoordlist(
-            "album generation", "other_image_size_limits")
-        imgsizesset = Set(imgsizesval) # Get rid of duplicates.
-        defaultlimit = env.config.getcoordlist(
-            "album generation", "default_image_size_limit")[0]
-        imgsizesset.add(defaultlimit)
-        imgsizes = list(imgsizesset)
-        imgsizes.sort(lambda x, y: cmp(x[0] * x[1], y[0] * y[1]))
-        env.imagesizelimits = imgsizes
-
-        generator = kofoto.generate.Generator(u"woolly", env)
-        generator.generate(self.album, None, directoryName, "latin1")
-        progressBar.set_fraction(1)
-        while gtk.events_pending():
-            gtk.main_iteration()
diff --git a/src/gkofoto/gkofoto/__init__.py b/src/gkofoto/gkofoto/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/gkofoto/gkofoto/albumdialog.py b/src/gkofoto/gkofoto/albumdialog.py
new file mode 100644 (file)
index 0000000..b97070a
--- /dev/null
@@ -0,0 +1,39 @@
+import gtk
+import string
+import re
+from environment import env
+from gkofoto.taganddescriptiondialog import *
+
+class AlbumDialog(TagAndDescriptionDialog):
+    def __init__(self, title, albumId=None):
+        if albumId is not None:
+            self._album = env.shelf.getAlbum(albumId)
+            tagText = self._album.getTag()
+            descText = self._album.getAttribute(u"title")
+            if descText == None:
+                descText = u""
+        else:
+            self._album = None
+            tagText = u""
+            descText = u""
+        TagAndDescriptionDialog.__init__(self, title, tagText, descText)
+        label = self._widgets.get_widget("titleLabel")
+        label.set_label(u"Title:")
+
+    def _isTagOkay(self, tagString):
+        try:
+           # Check that the tag name is valid.
+           verifyValidAlbumTag(tagString)
+        except BadAlbumTagError:
+            return False
+        try:
+            album = env.shelf.getAlbum(tagString)
+            if album == self._album:
+                # The tag exists, but is same as before.
+                return True
+            else:
+                # The tag is taken by another album.
+                return False
+        except AlbumDoesNotExistError:
+            # The tag didn't exist.
+            return True
diff --git a/src/gkofoto/gkofoto/albummembers.py b/src/gkofoto/gkofoto/albummembers.py
new file mode 100644 (file)
index 0000000..984dc6b
--- /dev/null
@@ -0,0 +1,71 @@
+from objectcollection import *
+from environment import env
+
+class AlbumMembers(ObjectCollection):
+
+######################################################################
+### Public functions and constants
+
+    def __init__(self):
+        env.debug("Init AlbumMembers")
+        ObjectCollection.__init__(self)
+        self.__album = None
+
+    def loadAlbum(self, album):
+        env.debug("Loading album: " + album.getTag())
+        self.__album = album
+        self._loadObjectList(album.getChildren())
+
+    def isReorderable(self):
+        return self.__album and self.__album.isMutable()
+
+    def isMutable(self):
+        return self.__album and self.__album.isMutable()
+
+    def getContainer(self):
+        return self.__album
+
+    def cut(self, *foo):
+        self.copy()
+        self.delete()
+
+    def paste(self, *foo):
+        # This method assumes that self.getModel() returns an unsorted
+        # and mutable model.
+        self._freezeViews()
+        locations = list(self.getObjectSelection())
+        newObjects = list(env.clipboard)
+        currentChildren = list(self.__album.getChildren())
+        if len(locations) > 0:
+            locations.sort()
+            insertLocation = locations[0]
+        else:
+            # Insert last.
+            insertLocation = len(currentChildren)
+        self.__album.setChildren(currentChildren[:insertLocation] +
+                                 newObjects +
+                                 currentChildren[insertLocation:])
+        self._insertObjectList(newObjects, insertLocation)
+        # TODO: If the added object is an album, update the album widget.
+        self.getObjectSelection().unselectAll()
+        self._thawViews()
+
+    def delete(self, *foo):
+        # This method assumes that self.getModel() returns an unsorted
+        # and mutable model
+        model = self.getModel()
+        self._freezeViews()
+        albumMembers = list(self.__album.getChildren())
+        locations = list(self.getObjectSelection())
+        locations.sort()
+        locations.reverse()
+        for loc in locations:
+            albumMembers.pop(loc)
+            del model[loc]
+        self.__album.setChildren(albumMembers)
+        self.getObjectSelection().unselectAll()
+        # TODO: If the removed objects are albums, update the album widget.
+        self._thawViews()
+
+######################################################################
+### Private functions
diff --git a/src/gkofoto/gkofoto/albums.py b/src/gkofoto/gkofoto/albums.py
new file mode 100644 (file)
index 0000000..9f53690
--- /dev/null
@@ -0,0 +1,250 @@
+import gtk
+import gobject
+import gtk
+from environment import env
+from albumdialog import AlbumDialog
+from menuhandler import *
+from registerimagesdialog import RegisterImagesDialog
+
+class Albums:
+
+###############################################################################
+### Public
+
+    __createAlbumLabel = "Create child album..."
+    __registerImagesLabel = "Register and add images..."
+    __generateHtmlLabel = "Generate HTML..."
+    __destroyAlbumLabel = "Destroy album..."
+    __editAlbumLabel = "Album properties..."
+
+    # TODO This class should probably be splited in a model and a view when/if
+    #      a multiple windows feature is introduced.
+
+    def __init__(self, mainWindow):
+        self._connectedOids = []
+        self.__albumModel = gtk.TreeStore(gobject.TYPE_INT,      # ALBUM_ID
+                                          gobject.TYPE_STRING,   # TAG
+                                          gobject.TYPE_STRING,   # TEXT
+                                          gobject.TYPE_STRING,   # TYPE
+                                          gobject.TYPE_BOOLEAN)  # SELECTABLE
+        self.__mainWindow = mainWindow
+        self.__albumView = env.widgets["albumView"]
+        self.__albumView.set_model(self.__albumModel)
+        self.__albumView.connect("focus-in-event", self._treeViewFocusInEvent)
+        self.__albumView.connect("focus-out-event", self._treeViewFocusOutEvent)
+        renderer = gtk.CellRendererText()
+        column = gtk.TreeViewColumn("Albums", renderer, text=self.__COLUMN_TEXT)
+        column.set_clickable(True)
+        self.__albumView.append_column(column)
+        albumSelection = self.__albumView.get_selection()
+        albumSelection.connect("changed", self._albumSelectionUpdated)
+        albumSelection.set_select_function(self._isSelectable, self.__albumModel)
+        self.__contextMenu = self.__createContextMenu()
+        self.__albumView.connect("button_press_event", self._button_pressed)
+        self.loadAlbumTree()
+        iterator = self.__albumModel.get_iter_first()
+        albumSelection.select_iter(iterator)
+
+    def loadAlbumTree(self):
+        env.shelf.flushObjectCache()
+        self.__albumModel.clear()
+        self.__loadAlbumTreeHelper()
+        env.widgets["albumView"].expand_row(0, False) # Expand root album
+
+
+###############################################################################
+### Callback functions registered by this class but invoked from other classes.
+
+    def _isSelectable(self, path, model):
+        return model[path][self.__COLUMN_SELECTABLE]
+
+    def _albumSelectionUpdated(self, selection=None, load=True):
+        # The focus grab below is made to compensate for what could be
+        # some GTK bug. Without the call, the focus-out-event signal
+        # sometimes isn't emitted for the view widget in the table
+        # view, which messes up the menubar callback registrations.
+        self.__albumView.grab_focus()
+
+        if not selection:
+            selection = self.__albumView.get_selection()
+        albumModel, iterator =  self.__albumView.get_selection().get_selected()
+        createMenuItem = self.__menuGroup[self.__createAlbumLabel]
+        registerMenuItem = self.__menuGroup[self.__registerImagesLabel]
+        generateHtmlMenuItem = self.__menuGroup[self.__generateHtmlLabel]
+        destroyMenuItem = self.__menuGroup[self.__destroyAlbumLabel]
+        editMenuItem = self.__menuGroup[self.__editAlbumLabel]
+        if iterator:
+            albumTag = albumModel.get_value(iterator, self.__COLUMN_TAG)
+            if load:
+                self.__mainWindow.loadQuery("/" + albumTag.decode("utf-8"))
+            album = env.shelf.getAlbum(
+                albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID))
+            createMenuItem.set_sensitive(album.isMutable())
+            env.widgets["menubarCreateAlbumChild"].set_sensitive(album.isMutable())
+            registerMenuItem.set_sensitive(album.isMutable())
+            env.widgets["menubarRegisterAndAddImages"].set_sensitive(album.isMutable())
+            generateHtmlMenuItem.set_sensitive(True)
+            env.widgets["menubarGenerateHtml"].set_sensitive(True)
+            destroyMenuItem.set_sensitive(album != env.shelf.getRootAlbum())
+            env.widgets["menubarDestroy"].set_sensitive(album != env.shelf.getRootAlbum())
+            editMenuItem.set_sensitive(True)
+            env.widgets["menubarProperties"].set_sensitive(True)
+        else:
+            createMenuItem.set_sensitive(False)
+            registerMenuItem.set_sensitive(False)
+            generateHtmlMenuItem.set_sensitive(False)
+            destroyMenuItem.set_sensitive(False)
+            editMenuItem.set_sensitive(False)
+            env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
+            env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
+            env.widgets["menubarGenerateHtml"].set_sensitive(False)
+            env.widgets["menubarDestroy"].set_sensitive(False)
+            env.widgets["menubarProperties"].set_sensitive(False)
+
+    def _createChildAlbum(self, *dummies):
+        dialog = AlbumDialog("Create album")
+        dialog.run(self._createAlbumHelper)
+
+    def _registerImages(self, *dummies):
+        albumModel, iterator =  self.__albumView.get_selection().get_selected()
+        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
+        selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
+        dialog = RegisterImagesDialog(selectedAlbum)
+        if dialog.run() == gtk.RESPONSE_OK:
+            self.__mainWindow.reload() # TODO: don't reload everything.
+        dialog.destroy()
+
+    def _generateHtml(self, *dummies):
+        albumModel, iterator =  self.__albumView.get_selection().get_selected()
+        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
+        selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
+        self.__mainWindow.generateHtml(selectedAlbum)
+
+    def _createAlbumHelper(self, tag, desc):
+        newAlbum = env.shelf.createAlbum(tag)
+        if len(desc) > 0:
+            newAlbum.setAttribute(u"title", desc)
+        albumModel, iterator =  self.__albumView.get_selection().get_selected()
+        if iterator is None:
+            selectedAlbum = env.shelf.getRootAlbum()
+        else:
+            selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
+            selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
+        children = list(selectedAlbum.getChildren())
+        children.append(newAlbum)
+        selectedAlbum.setChildren(children)
+        # TODO The whole tree should not be reloaded
+        self.loadAlbumTree()
+        # TODO update objectCollection?
+
+    def _destroyAlbum(self, *dummies):
+        dialogId = "destroyAlbumsDialog"
+        widgets = gtk.glade.XML(env.gladeFile, dialogId)
+        dialog = widgets.get_widget(dialogId)
+        result = dialog.run()
+        if result == gtk.RESPONSE_OK:
+            albumModel, iterator =  self.__albumView.get_selection().get_selected()
+            selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
+            env.shelf.deleteAlbum(selectedAlbumId)
+            # TODO The whole tree should not be reloaded
+            self.loadAlbumTree()
+            # TODO update objectCollection?
+        dialog.destroy()
+
+    def _editAlbum(self, *dummies):
+        albumModel, iterator =  self.__albumView.get_selection().get_selected()
+        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
+        dialog = AlbumDialog("Edit album", selectedAlbumId)
+        dialog.run(self._editAlbumHelper)
+
+    def _editAlbumHelper(self, tag, desc):
+        albumModel, iterator =  self.__albumView.get_selection().get_selected()
+        selectedAlbumId = albumModel.get_value(iterator, self.__COLUMN_ALBUM_ID)
+        selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
+        selectedAlbum.setTag(tag)
+        if len(desc) > 0:
+            selectedAlbum.setAttribute(u"title", desc)
+        else:
+            selectedAlbum.deleteAttribute(u"title")
+        # TODO The whole tree should not be reloaded
+        self.loadAlbumTree()
+        # TODO update objectCollection?
+
+    def _button_pressed(self, treeView, event):
+        if event.button == 3:
+            self.__contextMenu.popup(None,None,None,event.button,event.time)
+            return True
+        else:
+            return False
+
+    def _treeViewFocusInEvent(self, widget, event):
+        self._albumSelectionUpdated(None, load=False)
+        for widgetName, function in [
+                ("menubarCreateAlbumChild", self._createChildAlbum),
+                ("menubarRegisterAndAddImages", self._registerImages),
+                ("menubarGenerateHtml", self._generateHtml),
+                ("menubarProperties", self._editAlbum),
+                ("menubarDestroy", self._destroyAlbum),
+                ]:
+            w = env.widgets[widgetName]
+            oid = w.connect("activate", function, None)
+            self._connectedOids.append((w, oid))
+
+    def _treeViewFocusOutEvent(self, widget, event):
+        for (widget, oid) in self._connectedOids:
+            widget.disconnect(oid)
+        self._connectedOids = []
+        for widgetName in [
+                "menubarCreateAlbumChild",
+                "menubarRegisterAndAddImages",
+                "menubarGenerateHtml",
+                "menubarProperties",
+                ]:
+            env.widgets[widgetName].set_sensitive(False)
+
+###############################################################################
+### Private
+
+    __COLUMN_ALBUM_ID   = 0
+    __COLUMN_TAG        = 1
+    __COLUMN_TEXT       = 2
+    __COLUMN_TYPE       = 3
+    __COLUMN_SELECTABLE = 4
+
+    def __loadAlbumTreeHelper(self, parentAlbum=None, album=None, visited=[]):
+        if not album:
+            album = env.shelf.getRootAlbum()
+        iterator = self.__albumModel.append(parentAlbum)
+        # TODO Do we have to use iterators here or can we use pygtks simplified syntax?
+        self.__albumModel.set_value(iterator, self.__COLUMN_ALBUM_ID, album.getId())
+        self.__albumModel.set_value(iterator, self.__COLUMN_TYPE, album.getType())
+        self.__albumModel.set_value(iterator, self.__COLUMN_TAG, album.getTag())
+        self.__albumModel.set_value(iterator, self.__COLUMN_SELECTABLE, True)
+        albumTitle = album.getAttribute(u"title")
+        if albumTitle == None or len(albumTitle) < 1:
+            self.__albumModel.set_value(iterator, self.__COLUMN_TEXT, album.getTag())
+        else:
+            self.__albumModel.set_value(iterator, self.__COLUMN_TEXT, albumTitle)
+        if album.getId() not in visited:
+            for child in album.getAlbumChildren():
+                self.__loadAlbumTreeHelper(iterator, child, visited + [album.getId()])
+        else:
+            iterator = self.__albumModel.insert_before(iterator, None)
+            self.__albumModel.set_value(iterator, self.__COLUMN_TEXT, "[...]")
+            self.__albumModel.set_value(iterator, self.__COLUMN_SELECTABLE, False)
+
+    def __createContextMenu(self):
+        self.__menuGroup = MenuGroup()
+        self.__menuGroup.addMenuItem(
+            self.__createAlbumLabel, self._createChildAlbum)
+        self.__menuGroup.addMenuItem(
+            self.__registerImagesLabel, self._registerImages)
+        self.__menuGroup.addMenuItem(
+            self.__generateHtmlLabel, self._generateHtml)
+        self.__menuGroup.addMenuItem(
+            self.__destroyAlbumLabel, self._destroyAlbum)
+        self.__menuGroup.addStockImageMenuItem(
+            self.__editAlbumLabel,
+            gtk.STOCK_PROPERTIES,
+            self._editAlbum)
+        return self.__menuGroup.createGroupMenu()
diff --git a/src/gkofoto/gkofoto/categories.py b/src/gkofoto/gkofoto/categories.py
new file mode 100644 (file)
index 0000000..e579e5e
--- /dev/null
@@ -0,0 +1,549 @@
+import gobject
+import gtk
+import string
+
+from environment import env
+from categorydialog import CategoryDialog
+from menuhandler import *
+from kofoto.search import *
+from kofoto.shelf import *
+
+class Categories:
+
+######################################################################
+### Public
+
+    def __init__(self, mainWindow):
+        self.__toggleColumn = None
+        self.__objectCollection = None
+        self.__ignoreSelectEvent = False
+        self.__selectedCategoriesIds  = {}
+        self.__categoryModel = gtk.TreeStore(gobject.TYPE_INT,      # CATEGORY_ID
+                                             gobject.TYPE_STRING,   # DESCRIPTION
+                                             gobject.TYPE_BOOLEAN,  # CONNECTED
+                                             gobject.TYPE_BOOLEAN)  # INCONSISTENT
+        self.__categoryView = env.widgets["categoryView"]
+        self.__categoryView.realize()
+        self.__categoryView.set_model(self.__categoryModel)
+        self.__categoryView.connect("focus-in-event", self._categoryViewFocusInEvent)
+        self.__categoryView.connect("focus-out-event", self._categoryViewFocusOutEvent)
+        self.__mainWindow = mainWindow
+
+        # Create toggle column
+        toggleRenderer = gtk.CellRendererToggle()
+        toggleRenderer.connect("toggled", self._connectionToggled)
+        self.__toggleColumn = gtk.TreeViewColumn("",
+                                                 toggleRenderer,
+                                                 active=self.__COLUMN_CONNECTED,
+                                                 inconsistent=self.__COLUMN_INCONSISTENT)
+        self.__categoryView.append_column(self.__toggleColumn)
+
+        # Create text column
+        textRenderer = gtk.CellRendererText()
+        textColumn = gtk.TreeViewColumn("Category", textRenderer, text=self.__COLUMN_DESCRIPTION)
+        self.__categoryView.append_column(textColumn)
+        self.__categoryView.set_expander_column(textColumn)
+
+        # Create context menu
+        # TODO Is it possible to load a menu from a glade file instead?
+        #      If not, create some helper functions to construct the menu...
+        self._contextMenu = gtk.Menu()
+
+        self._contextMenuGroup = MenuGroup()
+        self._contextMenuGroup.addStockImageMenuItem(
+            self.__cutCategoryLabel,
+            gtk.STOCK_CUT,
+            self._cutCategory)
+        self._contextMenuGroup.addStockImageMenuItem(
+            self.__copyCategoryLabel,
+            gtk.STOCK_COPY,
+            self._copyCategory)
+        self._contextMenuGroup.addStockImageMenuItem(
+            self.__pasteCategoryLabel,
+            gtk.STOCK_PASTE,
+            self._pasteCategory)
+        self._contextMenuGroup.addStockImageMenuItem(
+            self.__destroyCategoryLabel,
+            gtk.STOCK_DELETE,
+            self._deleteCategories)
+        self._contextMenuGroup.addMenuItem(
+            self.__disconnectCategoryLabel,
+            self._disconnectCategory)
+        self._contextMenuGroup.addMenuItem(
+            self.__createChildCategoryLabel,
+            self._createChildCategory)
+        self._contextMenuGroup.addMenuItem(
+            self.__createRootCategoryLabel,
+            self._createRootCategory)
+        self._contextMenuGroup.addStockImageMenuItem(
+            self.__propertiesLabel,
+            gtk.STOCK_PROPERTIES,
+            self._editProperties)
+
+        for item in self._contextMenuGroup:
+            self._contextMenu.append(item)
+
+        env.widgets["categorySearchButton"].set_sensitive(False)
+
+        # Init menubar items.
+        env.widgets["menubarDisconnectFromParent"].connect(
+            "activate", self._disconnectCategory, None)
+        env.widgets["menubarCreateChild"].connect(
+            "activate", self._createChildCategory, None)
+        env.widgets["menubarCreateRoot"].connect(
+            "activate", self._createRootCategory, None)
+
+        # Init selection functions
+        categorySelection = self.__categoryView.get_selection()
+        categorySelection.set_mode(gtk.SELECTION_MULTIPLE)
+        categorySelection.set_select_function(self._selectionFunction, None)
+        categorySelection.connect("changed", self._categorySelectionChanged)
+
+        # Connect the rest of the UI events
+        self.__categoryView.connect("button_press_event", self._button_pressed)
+        self.__categoryView.connect("button_release_event", self._button_released)
+        self.__categoryView.connect("row-activated", self._rowActivated)
+        env.widgets["categorySearchButton"].connect('clicked', self._executeQuery)
+
+        self.loadCategoryTree()
+
+
+    def loadCategoryTree(self):
+        self.__categoryModel.clear()
+        env.shelf.flushCategoryCache()
+        for category in self.__sortCategories(env.shelf.getRootCategories()):
+            self.__loadCategorySubTree(None, category)
+        if self.__objectCollection is not None:
+            self.objectSelectionChanged()
+
+    def setCollection(self, objectCollection):
+        if self.__objectCollection is not None:
+            self.__objectCollection.getObjectSelection().removeChangedCallback(self.objectSelectionChanged)
+        self.__objectCollection = objectCollection
+        self.__objectCollection.getObjectSelection().addChangedCallback(self.objectSelectionChanged)
+        self.objectSelectionChanged()
+
+    def objectSelectionChanged(self, objectSelection=None):
+        self.__updateToggleColumn()
+        self.__updateContextMenu()
+        self.__expandAndCollapseRows(env.widgets["autoExpand"].get_active(),
+                                     env.widgets["autoCollapse"].get_active())
+
+
+###############################################################################
+### Callback functions registered by this class but invoked from other classes.
+
+    def _executeQuery(self, *foo):
+        query = self.__buildQueryFromSelection()
+        if query:
+            self.__mainWindow.loadQuery(query)
+
+    def _categoryViewFocusInEvent(self, widget, event):
+        self._menubarOids = []
+        for widgetName, function in [
+                ("menubarCut", lambda *x: self._cutCategory(None, None)),
+                ("menubarCopy", lambda *x: self._copyCategory(None, None)),
+                ("menubarPaste", lambda *x: self._pasteCategory(None, None)),
+                ("menubarDestroy", lambda *x: self._deleteCategories(None, None)),
+                ("menubarClear", lambda *x: widget.get_selection().unselect_all()),
+                ("menubarSelectAll", lambda *x: widget.get_selection().select_all()),
+                ("menubarProperties", lambda *x: self._editProperties(None, None)),
+                ]:
+            w = env.widgets[widgetName]
+            oid = w.connect("activate", function)
+            self._menubarOids.append((w, oid))
+        self.__updateContextMenu()
+
+    def _categoryViewFocusOutEvent(self, widget, event):
+        for (widget, oid) in self._menubarOids:
+            widget.disconnect(oid)
+
+    def _categorySelectionChanged(self, selection):
+        selectedCategoryRows = []
+        selection = self.__categoryView.get_selection()
+        # TODO replace with "get_selected_rows()" when it is introduced in Pygtk 2.2 API
+        selection.selected_foreach(lambda model,
+                                   path,
+                                   iter:
+                                   selectedCategoryRows.append(model[path]))
+        self.__selectedCategoriesIds  = {}
+
+        for categoryRow in selectedCategoryRows:
+            cid = categoryRow[self.__COLUMN_CATEGORY_ID]
+            # row.parent method gives assertion failed, dont know why. Using workaround instead.
+            parentPath = categoryRow.path[:-1]
+            if parentPath:
+                parentId = categoryRow.model[parentPath][self.__COLUMN_CATEGORY_ID]
+            else:
+                parentId = None
+            try:
+                 self.__selectedCategoriesIds[cid].append(parentId)
+            except KeyError:
+                 self.__selectedCategoriesIds[cid] = [parentId]
+        self.__updateContextMenu()
+        env.widgets["categorySearchButton"].set_sensitive(
+            len(selectedCategoryRows) > 0)
+
+    def _connectionToggled(self, renderer, path):
+        categoryRow = self.__categoryModel[path]
+        category = env.shelf.getCategory(categoryRow[self.__COLUMN_CATEGORY_ID])
+        if categoryRow[self.__COLUMN_INCONSISTENT] \
+               or not categoryRow[self.__COLUMN_CONNECTED]:
+            for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
+                try:
+                    obj.addCategory(category)
+                except CategoryPresentError:
+                    # The object was already connected to the category
+                    pass
+            categoryRow[self.__COLUMN_INCONSISTENT] = False
+            categoryRow[self.__COLUMN_CONNECTED] = True
+        else:
+            for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
+                obj.removeCategory(category)
+            categoryRow[self.__COLUMN_CONNECTED] = False
+            categoryRow[self.__COLUMN_INCONSISTENT] = False
+        self.__updateToggleColumn()
+
+    def _button_pressed(self, treeView, event):
+        if event.button == 3:
+            self._contextMenu.popup(None,None,None,event.button,event.time)
+            return True
+        rec = self.__categoryView.get_cell_area(0, self.__toggleColumn)
+        if event.x <= (rec.x + rec.width):
+            # Ignore selection event since the user clicked on the toggleColumn.
+            self.__ignoreSelectEvent = True
+        return False
+
+    def _button_released(self, treeView, event):
+        self.__ignoreSelectEvent = False
+        return False
+
+    def _rowActivated(self, a, b, c):
+        # TODO What should happen if the user dubble-click on a category?
+        pass
+
+    def _copyCategory(self, item, data):
+        cc = ClipboardCategories()
+        cc.type = cc.COPY
+        cc.categories = self.__selectedCategoriesIds
+        env.clipboard.setCategories(cc)
+
+    def _cutCategory(self, item, data):
+        cc = ClipboardCategories()
+        cc.type = cc.CUT
+        cc.categories = self.__selectedCategoriesIds
+        env.clipboard.setCategories(cc)
+
+    def _pasteCategory(self, item, data):
+        assert env.clipboard.hasCategories()
+        clipboardCategories = env.clipboard[0]
+        env.clipboard.clear()
+        try:
+            for (categoryId, previousParentIds) in clipboardCategories.categories.items():
+                for newParentId in self.__selectedCategoriesIds:
+                    if clipboardCategories.type == ClipboardCategories.COPY:
+                        self.__connectChildToCategory(categoryId, newParentId)
+                        for parentId in previousParentIds:
+                            if parentId is None:
+                                self.__disconnectChildHelper(categoryId, None,
+                                                             None, self.__categoryModel)
+                    else:
+                        if newParentId in previousParentIds:
+                            previousParentIds.remove(newParentId)
+                        else:
+                            self.__connectChildToCategory(categoryId, newParentId)
+                        for parentId in previousParentIds:
+                            if parentId is None:
+                                self.__disconnectChildHelper(categoryId, None,
+                                                             None, self.__categoryModel)
+                            else:
+                                self.__disconnectChild(categoryId, parentId)
+        except CategoryLoopError:
+            dialog = gtk.MessageDialog(
+                type=gtk.MESSAGE_ERROR,
+                buttons=gtk.BUTTONS_OK,
+                message_format="Category loop detected.")
+            dialog.run()
+            dialog.destroy()
+        self.__expandAndCollapseRows(False, False)
+
+    def _createRootCategory(self, item, data):
+        dialog = CategoryDialog("Create root category")
+        dialog.run(self._createRootCategoryHelper)
+
+    def _createRootCategoryHelper(self, tag, desc):
+        category = env.shelf.createCategory(tag, desc)
+        self.__loadCategorySubTree(None, category)
+
+    def _createChildCategory(self, item, data):
+        dialog = CategoryDialog("Create child category")
+        dialog.run(self._createChildCategoryHelper)
+
+    def _createChildCategoryHelper(self, tag, desc):
+        newCategory = env.shelf.createCategory(tag, desc)
+        for selectedCategoryId in self.__selectedCategoriesIds:
+            self.__connectChildToCategory(newCategory.getId(), selectedCategoryId)
+        self.__expandAndCollapseRows(False, False)
+
+    def _deleteCategories(self, item, data):
+        dialogId = "destroyCategoriesDialog"
+        widgets = gtk.glade.XML(env.gladeFile, dialogId)
+        dialog = widgets.get_widget(dialogId)
+        result = dialog.run()
+        if result == gtk.RESPONSE_OK:
+            for categoryId in self.__selectedCategoriesIds:
+                category = env.shelf.getCategory(categoryId)
+                for child in list(category.getChildren()):
+                    # The backend automatically disconnects childs
+                    # when a category is deleted, but we do it ourself
+                    # to make sure that the treeview widget is
+                    # updated.
+                    self.__disconnectChild(child.getId(), categoryId)
+                env.shelf.deleteCategory(categoryId)
+                env.shelf.flushCategoryCache()
+                self.__forEachCategoryRow(
+                    self.__deleteCategoriesHelper, categoryId)
+        dialog.destroy()
+
+    def __deleteCategoriesHelper(self, categoryRow, categoryIdToDelete):
+        if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryIdToDelete:
+            self.__categoryModel.remove(categoryRow.iter)
+
+    def _disconnectCategory(self, item, data):
+        for (categoryId, parentIds) in self.__selectedCategoriesIds.items():
+            for parentId in parentIds:
+                if not parentId == None: # Not possible to disconnect root categories
+                    self.__disconnectChild(categoryId, parentId)
+
+    def _editProperties(self, item, data):
+        for categoryId in self.__selectedCategoriesIds:
+            dialog = CategoryDialog("Change properties", categoryId)
+            dialog.run(self._editPropertiesHelper, data=categoryId)
+
+    def _editPropertiesHelper(self, tag, desc, categoryId):
+         category = env.shelf.getCategory(categoryId)
+         category.setTag(tag)
+         category.setDescription(desc)
+         env.shelf.flushCategoryCache()
+         self.__forEachCategoryRow(self.__updatePropertiesFromShelf, categoryId)
+
+    def _selectionFunction(self, path, b):
+        return not self.__ignoreSelectEvent
+
+
+######################################################################
+### Private
+
+    __cutCategoryLabel = "Cut"
+    __copyCategoryLabel = "Copy"
+    __pasteCategoryLabel = "Paste as child(ren)"
+    __destroyCategoryLabel = "Destroy..."
+    __disconnectCategoryLabel = "Disconnect from parent"
+    __createChildCategoryLabel = "Create child"
+    __createRootCategoryLabel = "Create root"
+    __propertiesLabel = "Properties"
+
+    __COLUMN_CATEGORY_ID  = 0
+    __COLUMN_DESCRIPTION  = 1
+    __COLUMN_CONNECTED    = 2
+    __COLUMN_INCONSISTENT = 3
+
+    def __loadCategorySubTree(self, parent, category):
+        # TODO Do we have to use iterators here or can we use pygtks simplified syntax?
+        iterator = self.__categoryModel.iter_children(parent)
+        while (iterator != None and
+               self.__categoryModel.get_value(iterator, self.__COLUMN_DESCRIPTION) <
+                   category.getDescription()):
+            iterator = self.__categoryModel.iter_next(iterator)
+        iterator = self.__categoryModel.insert_before(parent, iterator)
+        self.__categoryModel.set_value(iterator, self.__COLUMN_CATEGORY_ID, category.getId())
+        self.__categoryModel.set_value(iterator, self.__COLUMN_DESCRIPTION, category.getDescription())
+        self.__categoryModel.set_value(iterator, self.__COLUMN_CONNECTED, False)
+        self.__categoryModel.set_value(iterator, self.__COLUMN_INCONSISTENT, False)
+        for child in self.__sortCategories(category.getChildren()):
+            self.__loadCategorySubTree(iterator, child)
+
+    def __buildQueryFromSelection(self):
+        if env.widgets["categoriesOr"].get_active():
+            operator = " or "
+        else:
+            operator = " and "
+        return operator.join([env.shelf.getCategory(x).getTag()
+                              for x in self.__selectedCategoriesIds])
+
+    def __updateContextMenu(self):
+        # TODO Create helper functions to use from this method
+        menubarWidgetNames = [
+                "menubarCut",
+                "menubarCopy",
+                "menubarPaste",
+                "menubarDestroy",
+                "menubarProperties",
+                "menubarDisconnectFromParent",
+                "menubarCreateChild",
+                "menubarCreateRoot",
+                ]
+        if len(self.__selectedCategoriesIds) == 0:
+            self._contextMenuGroup.disable()
+            for widgetName in menubarWidgetNames:
+                env.widgets[widgetName].set_sensitive(False)
+            self._contextMenuGroup[
+                self.__createRootCategoryLabel].set_sensitive(True)
+            env.widgets["menubarCreateRoot"].set_sensitive(True)
+        else:
+            self._contextMenuGroup.enable()
+            for widgetName in menubarWidgetNames:
+                env.widgets[widgetName].set_sensitive(True)
+            if not env.clipboard.hasCategories():
+                self._contextMenuGroup[
+                    self.__pasteCategoryLabel].set_sensitive(False)
+                env.widgets["menubarPaste"].set_sensitive(False)
+        propertiesItem = self._contextMenuGroup[self.__propertiesLabel]
+        propertiesItemSensitive = len(self.__selectedCategoriesIds) == 1
+        propertiesItem.set_sensitive(propertiesItemSensitive)
+        env.widgets["menubarProperties"].set_sensitive(propertiesItemSensitive)
+
+    def __updateToggleColumn(self):
+        # find out which categories are connected, not connected or
+        # partitionally connected to selected objects
+        nrSelectedObjectsInCategory = {}
+        nrSelectedObjects = 0
+        for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
+            nrSelectedObjects += 1
+            for category in obj.getCategories():
+                categoryId = category.getId()
+                try:
+                    nrSelectedObjectsInCategory[categoryId] += 1
+                except KeyError:
+                        nrSelectedObjectsInCategory[categoryId] = 1
+        self.__forEachCategoryRow(self.__updateToggleColumnHelper,
+                                  (nrSelectedObjects, nrSelectedObjectsInCategory))
+
+    def __updateToggleColumnHelper(self,
+                                   categoryRow,
+                                   (nrSelectedObjects, nrSelectedObjectsInCategory)):
+        categoryId = categoryRow[self.__COLUMN_CATEGORY_ID]
+        if categoryId in nrSelectedObjectsInCategory:
+            if nrSelectedObjectsInCategory[categoryId] < nrSelectedObjects:
+                # Some of the selected objects are connected to the category
+                categoryRow[self.__COLUMN_CONNECTED] = False
+                categoryRow[self.__COLUMN_INCONSISTENT] = True
+            else:
+                # All of the selected objects are connected to the category
+                categoryRow[self.__COLUMN_CONNECTED] = True
+                categoryRow[self.__COLUMN_INCONSISTENT] = False
+        else:
+            # None of the selected objects are connected to the category
+            categoryRow[self.__COLUMN_CONNECTED] = False
+            categoryRow[self.__COLUMN_INCONSISTENT] = False
+
+    def __forEachCategoryRow(self, function, data=None, categoryRows=None):
+        # We can't use gtk.TreeModel.foreach() since it does not pass a row
+        # to the callback function.
+        if not categoryRows:
+            categoryRows=self.__categoryModel
+        for categoryRow in categoryRows:
+            function(categoryRow, data)
+            self.__forEachCategoryRow(function, data, categoryRow.iterchildren())
+
+    def __expandAndCollapseRows(self, autoExpand, autoCollapse, categoryRows=None):
+        if categoryRows is None:
+            categoryRows=self.__categoryModel
+        someRowsExpanded = False
+        for categoryRow in categoryRows:
+            expandThisRow = False
+            # Expand all rows that are selected or has expanded childs
+            childRowsExpanded = self.__expandAndCollapseRows(autoExpand,
+                                                            autoCollapse,
+                                                            categoryRow.iterchildren())
+            if (childRowsExpanded
+                or self.__categoryView.get_selection().path_is_selected(categoryRow.path)):
+                expandThisRow = True
+            # Auto expand all rows that has a checked toggle
+            if autoExpand:
+                if (categoryRow[self.__COLUMN_CONNECTED]
+                    or categoryRow[self.__COLUMN_INCONSISTENT]):
+                    expandThisRow = True
+            if expandThisRow:
+                for a in range(len(categoryRow.path)):
+                    self.__categoryView.expand_row(categoryRow.path[:a+1], False)
+                someRowsExpanded = True
+            # Auto collapse?
+            elif autoCollapse:
+                self.__categoryView.collapse_row(categoryRow.path)
+        return someRowsExpanded
+
+    def __connectChildToCategory(self, childId, parentId):
+        try:
+            # Update shelf
+            childCategory = env.shelf.getCategory(childId)
+            parentCategory = env.shelf.getCategory(parentId)
+            parentCategory.connectChild(childCategory)
+            env.shelf.flushCategoryCache()
+            # Update widget modell
+            # If we reload the whole category tree from the shelf, we would lose
+            # the widgets information about current selected categories,
+            # expanded categories and the widget's scroll position. Hence,
+            # we update our previously loaded model instead.
+            self.__connectChildToCategoryHelper(parentId,
+                                                childCategory,
+                                                self.__categoryModel)
+        except CategoriesAlreadyConnectedError:
+            # This is okay.
+            pass
+
+    def __connectChildToCategoryHelper(self, parentId, childCategory, categoryRows):
+        for categoryRow in categoryRows:
+            if categoryRow[self.__COLUMN_CATEGORY_ID] == parentId:
+                self.__loadCategorySubTree(categoryRow.iter, childCategory)
+            else:
+                self.__connectChildToCategoryHelper(parentId, childCategory, categoryRow.iterchildren())
+
+    def __disconnectChild(self, childId, parentId):
+        # Update shelf
+        childCategory = env.shelf.getCategory(childId)
+        parentCategory = env.shelf.getCategory(parentId)
+        if childCategory in env.shelf.getRootCategories():
+            alreadyWasRootCategory = True
+        else:
+            alreadyWasRootCategory = False
+        parentCategory.disconnectChild(childCategory)
+        env.shelf.flushCategoryCache()
+        # Update widget modell.
+        # If we reload the whole category tree from the shelf, we would lose
+        # the widgets information about current selected categories,
+        # expanded categories and the widget's scroll position. Hence,
+        # we update our previously loaded model instead.
+        self.__disconnectChildHelper(childId,
+                                    parentId,
+                                    None,
+                                    self.__categoryModel)
+        if not alreadyWasRootCategory:
+            for c in env.shelf.getRootCategories():
+                if c.getId() == childCategory.getId():
+                    self.__loadCategorySubTree(None, childCategory)
+                    break
+
+    def __disconnectChildHelper(self, wantedChildId, wantedParentId,
+                                parentId, categoryRows):
+        for categoryRow in categoryRows:
+            cid = categoryRow[self.__COLUMN_CATEGORY_ID]
+            if cid == wantedChildId and parentId == wantedParentId:
+                self.__categoryModel.remove(categoryRow.iter)
+            self.__disconnectChildHelper(wantedChildId, wantedParentId, cid, categoryRow.iterchildren())
+
+    def __updatePropertiesFromShelf(self, categoryRow, categoryId):
+        if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryId:
+            category = env.shelf.getCategory(categoryId)
+            categoryRow[self.__COLUMN_DESCRIPTION] = category.getDescription()
+
+    def __sortCategories(self, categoryIter):
+        categories = list(categoryIter)
+        categories.sort(lambda x, y: cmp(x.getDescription(), y.getDescription()))
+        return categories
+
+class ClipboardCategories:
+    COPY = 1
+    CUT = 2
+    categories = None
+    type = None
diff --git a/src/gkofoto/gkofoto/categorydialog.py b/src/gkofoto/gkofoto/categorydialog.py
new file mode 100644 (file)
index 0000000..fdbb86e
--- /dev/null
@@ -0,0 +1,35 @@
+import gtk
+import string
+import re
+from environment import env
+from gkofoto.taganddescriptiondialog import *
+
+class CategoryDialog(TagAndDescriptionDialog):
+    def __init__(self, title, categoryId=None):
+        if categoryId:
+            self._category = env.shelf.getCategory(categoryId)
+            tagText = self._category.getTag()
+            descText = self._category.getDescription()
+        else:
+            self._category = None
+            tagText = u""
+            descText = u""
+        TagAndDescriptionDialog.__init__(self, title, tagText, descText)
+
+    def _isTagOkay(self, tagString):
+        try:
+           # Check that the tag name is valid.
+           verifyValidCategoryTag(tagString)
+        except BadCategoryTagError:
+            return False
+        try:
+            category = env.shelf.getCategory(tagString)
+            if category == self._category:
+                # The tag exists, but is same as before.
+                return True
+            else:
+                # The tag is taken by another category.
+                return False
+        except CategoryDoesNotExistError:
+            # The tag didn't exist.
+            return True
diff --git a/src/gkofoto/gkofoto/clipboard.py b/src/gkofoto/gkofoto/clipboard.py
new file mode 100644 (file)
index 0000000..91ee2f5
--- /dev/null
@@ -0,0 +1,66 @@
+from sets import Set
+from environment import env
+from kofoto.shelf import Image
+from kofoto.shelf import Album
+from categories import ClipboardCategories
+
+class Clipboard:
+
+    # TYPES
+    OBJECTS    = 0 # shelf.Album and shelf.Image
+    CATEGORIES = 1 # shelf.Category
+
+    def __init__(self):
+        self.__changedCallbacks = Set()
+        self.clear()
+
+    def addChangedCallback(self, callback):
+        self.__changedCallbacks.add(callback)
+
+    def removeChangedCallback(self, callback):
+        self.__changedCallbacks.remove(callback)
+
+    def setObjects(self, iter):
+        self.__objects = []
+        self.__types = Clipboard.OBJECTS
+        for object in iter:
+            if (isinstance(object, Image) or isinstance(object, Album)):
+                self.__objects.append(object)
+            else:
+                self.clear()
+                raise "Object is not an Image nor an Album" # TODO
+        self.__invokeChangedCallbacks()
+
+    def setCategories(self, clipboardCategories):
+        self.__objects = []
+        if isinstance(clipboardCategories, ClipboardCategories):
+            self.__objects.append(clipboardCategories)
+        else:
+            self.clear()
+            raise "Object is not a ClipboardCategories" # TODO
+        self.__types = Clipboard.CATEGORIES
+        self.__invokeChangedCallbacks()
+
+    def clear(self):
+        self.__objects = []
+        self.__types = None
+        self.__invokeChangedCallbacks()
+
+    def hasCategories(self):
+        return (self.__types == Clipboard.CATEGORIES and len(self.__objects) > 0)
+
+    def hasObjects(self):
+        return (self.__types == Clipboard.OBJECTS and len(self.__objects) > 0)
+
+    def __len__(self):
+        return len(self.__objects)
+
+    def __iter__(self):
+        return self.__objects.__iter__()
+
+    def __getitem__(self, index):
+        return self.__objects.__getitem__(index)
+
+    def __invokeChangedCallbacks(self):
+        for callback in self.__changedCallbacks:
+            callback(self)
diff --git a/src/gkofoto/gkofoto/controller.py b/src/gkofoto/gkofoto/controller.py
new file mode 100644 (file)
index 0000000..6ae19d7
--- /dev/null
@@ -0,0 +1,75 @@
+import sys
+import gtk
+from kofoto.shelf import ShelfLockedError
+from gkofoto.mainwindow import MainWindow
+from gkofoto.environment import env
+
+class Controller:
+    def __init__(self):
+        self.__clipboard = None
+
+    def start(self, setupOk):
+        if setupOk:
+            try:
+                env.shelf.begin()
+            except ShelfLockedError, e:
+                env.startupNotices += [
+                    "Error: Could not open shelf \"%s\"." % e +
+                    " Another process is locking it.\n"]
+                setupOk = False
+        if env.startupNotices:
+            if setupOk:
+                dialogtype = gtk.MESSAGE_INFO
+            else:
+                dialogtype = gtk.MESSAGE_ERROR
+            dialog = gtk.MessageDialog(
+                type=dialogtype,
+                buttons=gtk.BUTTONS_OK,
+                message_format="".join(env.startupNotices))
+            if setupOk:
+                # Doesn't work with x[0].destroy(). Don't know why.
+                dialog.connect("response", lambda *x: x[0].hide())
+            else:
+                # Doesn't work with gtk.main_quit(). Don't know why.
+                dialog.connect("response", lambda *x: sys.exit(1))
+            dialog.run()
+        if setupOk:
+            self.__mainWindow = MainWindow()
+            env.widgets["mainWindow"].connect("destroy", self.quit, False)
+            env.widgets["mainWindow"].show()
+        gtk.main()
+
+    def quit(self, app, cancelButton=True):
+        if env.shelf.isModified():
+            widgets = gtk.glade.XML(env.gladeFile, "quitDialog")
+            quitDialog = widgets.get_widget("quitDialog")
+            if not cancelButton:
+                widgets.get_widget("cancel").set_sensitive(False)
+            result = quitDialog.run()
+            if result == 0:
+                env.shelf.commit()
+                gtk.main_quit()
+            elif result == 1:
+                env.shelf.rollback()
+                gtk.main_quit()
+            else:
+                quitDialog.destroy()
+                return
+        else:
+            env.shelf.rollback()
+            gtk.main_quit()
+
+    def save(self, app):
+        env.shelf.commit()
+        env.shelf.begin()
+
+    def revert(self, app):
+        dialog = gtk.MessageDialog(
+            type=gtk.MESSAGE_QUESTION,
+            buttons=gtk.BUTTONS_YES_NO,
+            message_format="Revert to the previously saved state and lose all changes?")
+        if dialog.run() == gtk.RESPONSE_YES:
+            env.shelf.rollback()
+            env.shelf.begin()
+            self.__mainWindow.reload()
+        dialog.destroy()
diff --git a/src/gkofoto/gkofoto/environment.py b/src/gkofoto/gkofoto/environment.py
new file mode 100644 (file)
index 0000000..c804fd2
--- /dev/null
@@ -0,0 +1,91 @@
+import sys
+import os
+import getopt
+import locale
+import re
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+import gtk.gdk
+import gtk.glade
+
+from kofoto.clientenvironment import *
+from kofoto.common import *
+from kofoto.shelf import *
+from kofoto.config import *
+from kofoto.imagecache import *
+
+class WidgetsWrapper:
+    def __init__(self):
+        self.widgets = gtk.glade.XML(env.gladeFile, "mainWindow")
+
+    def __getitem__(self, key):
+        return self.widgets.get_widget(key)
+
+class Environment(ClientEnvironment):
+    def __init__(self):
+        ClientEnvironment.__init__(self)
+        self.startupNotices = []
+
+    def setup(self, bindir, isDebug=False, configFileLocation=None,
+              shelfLocation=None):
+        try:
+            ClientEnvironment.setup(self, configFileLocation, shelfLocation)
+        except ClientEnvironmentError, e:
+            self.startupNotices += [e[0]]
+            return False
+
+        self.isDebug = isDebug
+        self.thumbnailSize = self.config.getcoordlist(
+            "gkofoto", "thumbnail_size_limit")[0]
+        self.defaultTableViewColumns = re.findall(
+            "\S+",
+            self.config.get("gkofoto", "default_table_columns"))
+        self.defaultSortColumn = self.config.get(
+            "gkofoto", "default_sort_column")
+        self.openCommand = self.config.get(
+            "gkofoto", "open_command", True)
+        self.rotateRightCommand = self.config.get(
+            "gkofoto", "rotate_right_command", True)
+        self.rotateLeftCommand = self.config.get(
+            "gkofoto", "rotate_left_command", True)
+
+        dataDir = os.path.join(bindir, "..", "share", "gkofoto")
+        if not os.path.exists(dataDir):
+            dataDir = bindir
+        self.iconDir = os.path.join(dataDir, "icons")
+        self.gladeFile = os.path.join(dataDir, "glade", "gkofoto.glade")
+        self.albumIconFileName = os.path.join(self.iconDir, "album.png")
+        self.albumIconPixbuf = gtk.gdk.pixbuf_new_from_file(self.albumIconFileName)
+        self.loadingPixbuf = self.albumIconPixbuf # TODO: create another icon with a hour-glass or something
+        self.unknownImageIconFileName = os.path.join(self.iconDir, "unknownimage.png")
+        self.unknownImageIconPixbuf = gtk.gdk.pixbuf_new_from_file(self.unknownImageIconFileName)
+        from clipboard import Clipboard
+        self.clipboard = Clipboard()
+
+        self.widgets = WidgetsWrapper()
+
+        return True
+
+    def _writeInfo(self, infoString):
+        self.startupNotices += [infoString]
+
+    def debug(self, msg):
+        if self.isDebug:
+            print msg
+
+    def enter(self, method):
+        if self.isDebug:
+            print "-->", method
+
+    def exit(self, method):
+        if self.isDebug:
+            print "<--", method
+
+    def assertUnicode(self, obj):
+        assert isinstance(obj, unicode), \
+               "%s is not a unicode object: \"%s\"" % (type(obj), obj)
+
+env = Environment()
diff --git a/src/gkofoto/gkofoto/generatehtmldialog.py b/src/gkofoto/gkofoto/generatehtmldialog.py
new file mode 100644 (file)
index 0000000..f6022d4
--- /dev/null
@@ -0,0 +1,98 @@
+import gtk
+import os
+import re
+from sets import Set
+from environment import env
+import kofoto.generate
+
+class GenerateHTMLDialog:
+    def __init__(self, album):
+        self.album = album
+        self.widgets = gtk.glade.XML(
+            env.gladeFile, "generateHtmlDialog")
+        self.dialog = self.widgets.get_widget("generateHtmlDialog")
+        self.browseButton = self.widgets.get_widget("browseButton")
+        self.cancelButton = self.widgets.get_widget("cancelButton")
+        self.directoryTextEntry = self.widgets.get_widget("directoryTextEntry")
+        self.generateButton = self.widgets.get_widget("generateButton")
+
+        self.browseButton.connect("clicked", self._onBrowse)
+        self.cancelButton.connect("clicked", self._onCancel)
+        self.generateButton.connect("clicked", self._onGenerate)
+
+        self.directoryTextEntry.connect(
+            "changed", self._onDirectoryTextEntryModified)
+
+        self.generateButton.set_sensitive(False)
+
+    def run(self):
+        self.dialog.show()
+
+    def _onDirectoryTextEntryModified(self, *unused):
+        self.generateButton.set_sensitive(
+            os.path.isdir(self.directoryTextEntry.get_text()))
+
+    def _onBrowse(self, *unused):
+        directorySelectedInDirList = False
+        dirDialog = gtk.FileSelection(title="Choose directory")
+        dirDialog.file_list.set_sensitive(False)
+        dirDialog.fileop_del_file.set_sensitive(False)
+        dirDialog.fileop_ren_file.set_sensitive(False)
+        if dirDialog.run() == gtk.RESPONSE_OK:
+            model, iterator = dirDialog.dir_list.get_selection().get_selected()
+            directory = dirDialog.get_filename()
+            if iterator:
+                directory = os.path.join(
+                    directory, model.get_value(iterator, 0))
+            self.directoryTextEntry.set_text(directory)
+        dirDialog.destroy()
+
+    def _onCancel(self, *unused):
+        self.dialog.destroy()
+
+    def _onGenerate(self, *unused):
+        for widget in [self.directoryTextEntry, self.browseButton,
+                       self.cancelButton, self.generateButton]:
+            widget.set_sensitive(False)
+        self._generate(self.directoryTextEntry.get_text())
+        self.dialog.destroy()
+
+    def _generate(self, directoryName):
+        # TODO: Rewrite this gross hack.
+
+        def outputParser(string):
+            m = re.match(
+                r"Creating album (\S+) \((\d+) of (\d+)\)",
+                string,
+                re.UNICODE)
+            if m:
+                progressBar.set_text(m.group(1).decode("latin1"))
+                progressBar.set_fraction(
+                    (int(m.group(2)) - 1) / float(m.group(3)))
+                while gtk.events_pending():
+                    gtk.main_iteration()
+
+        progressBar = self.widgets.get_widget("progressBar")
+
+        env.out = outputParser
+        env.verbose = True
+        env.thumbnailsizelimit = env.config.getcoordlist(
+            "album generation", "thumbnail_size_limit")[0]
+        env.defaultsizelimit = env.config.getcoordlist(
+            "album generation", "default_image_size_limit")[0]
+
+        imgsizesval = env.config.getcoordlist(
+            "album generation", "other_image_size_limits")
+        imgsizesset = Set(imgsizesval) # Get rid of duplicates.
+        defaultlimit = env.config.getcoordlist(
+            "album generation", "default_image_size_limit")[0]
+        imgsizesset.add(defaultlimit)
+        imgsizes = list(imgsizesset)
+        imgsizes.sort(lambda x, y: cmp(x[0] * x[1], y[0] * y[1]))
+        env.imagesizelimits = imgsizes
+
+        generator = kofoto.generate.Generator(u"woolly", env)
+        generator.generate(self.album, None, directoryName, "latin1")
+        progressBar.set_fraction(1)
+        while gtk.events_pending():
+            gtk.main_iteration()
diff --git a/src/gkofoto/gkofoto/handleimagesdialog.py b/src/gkofoto/gkofoto/handleimagesdialog.py
new file mode 100644 (file)
index 0000000..a2a29e5
--- /dev/null
@@ -0,0 +1,162 @@
+import gtk
+import gobject
+import os
+from environment import env
+from kofoto.shelf import \
+     ImageDoesNotExistError, ImageExistsError, \
+     MultipleImagesAtOneLocationError, NotAnImageError, \
+     makeValidTag
+from kofoto.clientutils import walk_files
+
+class HandleImagesDialog(gtk.FileSelection):
+    def __init__(self):
+        gtk.FileSelection.__init__(self, title="Register images")
+        self.set_select_multiple(True)
+        self.ok_button.connect("clicked", self._ok)
+
+    def _ok(self, widget):
+        widgets = gtk.glade.XML(env.gladeFile, "handleImagesProgressDialog")
+        handleImagesProgressDialog = widgets.get_widget(
+            "handleImagesProgressDialog")
+        knownUnchangedImagesCount = widgets.get_widget(
+            "knownUnchangedImagesCount")
+        knownMovedImagesCount = widgets.get_widget(
+            "knownMovedImagesCount")
+        unknownModifiedImagesCount = widgets.get_widget(
+            "unknownModifiedImagesCount")
+        unknownFilesCount = widgets.get_widget(
+            "unknownFilesCount")
+        investigatedFilesCount = widgets.get_widget(
+            "investigatedFilesCount")
+        okButton = widgets.get_widget("okButton")
+        okButton.set_sensitive(False)
+
+        handleImagesProgressDialog.show()
+
+        knownUnchangedImages = 0
+        knownMovedImages = 0
+        unknownModifiedImages = 0
+        unknownFiles = 0
+        investigatedFiles = 0
+        modifiedImages = []
+        movedImages = []
+        for filepath in walk_files(self.get_selections()):
+            try:
+                filepath = filepath.decode("utf-8")
+            except UnicodeDecodeError:
+                filepath = filepath.decode("latin1")
+            try:
+                image = env.shelf.getImage(filepath)
+                if image.getLocation() == os.path.realpath(filepath):
+                    # Registered.
+                    knownUnchangedImages += 1
+                    knownUnchangedImagesCount.set_text(
+                        str(knownUnchangedImages))
+                else:
+                    # Moved.
+                    knownMovedImages += 1
+                    knownMovedImagesCount.set_text(str(knownMovedImages))
+                    movedImages.append(filepath)
+            except ImageDoesNotExistError:
+                try:
+                    image = env.shelf.getImage(
+                        filepath, identifyByLocation=True)
+                    # Modified.
+                    unknownModifiedImages += 1
+                    unknownModifiedImagesCount.set_text(
+                        str(unknownModifiedImages))
+                    modifiedImages.append(filepath)
+                except MultipleImagesAtOneLocationError:
+                    # Multiple images at one location.
+                    # TODO: Handle this error.
+                    pass
+                except ImageDoesNotExistError:
+                    # Unregistered.
+                    unknownFiles += 1
+                    unknownFilesCount.set_text(str(unknownFiles))
+            investigatedFiles += 1
+            investigatedFilesCount.set_text(str(investigatedFiles))
+            while gtk.events_pending():
+                gtk.main_iteration()
+
+        okButton.set_sensitive(True)
+        handleImagesProgressDialog.run()
+        handleImagesProgressDialog.destroy()
+
+        if modifiedImages or movedImages:
+            if modifiedImages:
+                self._dialogHelper(
+                    "Update modified images",
+                    "The above image files have been modified. Press OK to"
+                    " make Kofoto recognize the new contents.",
+                    modifiedImages,
+                    self._updateModifiedImages)
+            if movedImages:
+                self._dialogHelper(
+                    "Update moved or renamed images",
+                    "The above image files have been moved or renamed. Press OK to"
+                    " make Kofoto recognize the new locations.",
+                    movedImages,
+                    self._updateMovedImages)
+        else:
+            dialog = gtk.MessageDialog(
+                type=gtk.MESSAGE_INFO,
+                buttons=gtk.BUTTONS_OK,
+                message_format="No modified, renamed or moved images found.")
+            dialog.run()
+            dialog.destroy()
+
+    def _dialogHelper(self, title, text, filepaths, handlerFunction):
+        widgets = gtk.glade.XML(env.gladeFile, "updateImagesDialog")
+        dialog = widgets.get_widget("updateImagesDialog")
+        dialog.set_title(title)
+        filenameList = widgets.get_widget("filenameList")
+        renderer = gtk.CellRendererText()
+        column = gtk.TreeViewColumn("Image filename", renderer, text=0)
+        filenameList.append_column(column)
+        dialogText = widgets.get_widget("dialogText")
+        dialogText.set_text(text)
+        model = gtk.ListStore(gobject.TYPE_STRING)
+        for filepath in filepaths:
+            model.append([filepath])
+        filenameList.set_model(model)
+        if dialog.run() == gtk.RESPONSE_OK:
+            handlerFunction(filepaths)
+        dialog.destroy()
+
+    def _error(self, errorText):
+        dialog = gtk.MessageDialog(
+            type=gtk.MESSAGE_ERROR,
+            buttons=gtk.BUTTONS_OK,
+            message_format=errorText)
+        dialog.run()
+        dialog.destroy()
+
+    def _updateModifiedImages(self, filepaths):
+        for filepath in filepaths:
+            try:
+                image = env.shelf.getImage(
+                    filepath, identifyByLocation=True)
+                image.contentChanged()
+            except ImageDoesNotExistError:
+                self._error("Image does not exist: %s" % filepath)
+            except MultipleImagesAtOneLocationError:
+                # TODO: Handle this.
+                pass
+            except IOError, x:
+                self._error("Error while reading %s: %s" % (
+                    filepath, x))
+
+    def _updateMovedImages(self, filepaths):
+        for filepath in filepaths:
+            try:
+                image = env.shelf.getImage(filepath)
+                image.locationChanged(filepath)
+            except ImageDoesNotExistError:
+                self._error("Image does not exist: %s" % filepath)
+            except MultipleImagesAtOneLocationError:
+                # TODO: Handle this.
+                pass
+            except IOError, x:
+                self._error("Error while reading %s: %s" % (
+                    filepath, x))
diff --git a/src/gkofoto/gkofoto/imageview.py b/src/gkofoto/gkofoto/imageview.py
new file mode 100644 (file)
index 0000000..70ef8b3
--- /dev/null
@@ -0,0 +1,147 @@
+import gtk
+import gtk.gdk
+import math
+import gobject
+import gc
+from environment import env
+
+class ImageView(gtk.ScrolledWindow):
+    # TODO: Read from configuration file?
+    _INTERPOLATION_TYPE = gtk.gdk.INTERP_BILINEAR
+    # gtk.gdk.INTERP_HYPER is slower but gives better quality.
+    _MAX_IMAGE_SIZE = 2000
+    _MIN_IMAGE_SIZE = 1
+    _MIN_ZOOM = -100
+    _MAX_ZOOM = 1
+    _ZOOMFACTOR = 1.2
+
+    def __init__(self):
+        self._image = gtk.Image()
+        gtk.ScrolledWindow.__init__(self)
+        self.__loadedFileName = None
+        self.__pixBuf = None
+        self.__currentZoom = None
+        self.__wantedZoom = None
+        self.__fitToWindowMode = True
+        self.__previousWidgetWidth = 0
+        self.__previousWidgetHeight = 0
+        eventBox = gtk.EventBox()
+        eventBox.add(self._image)
+        self.add_with_viewport(eventBox)
+        self.add_events(gtk.gdk.ALL_EVENTS_MASK)
+        self.connect_after("size_allocate", self.resizeEventHandler)
+        self.connect("scroll_event", self.scrollEventHandler)
+
+    def loadFile(self, fileName, reload=True):
+        fileName = fileName.encode(env.codeset)
+        if (not reload) and self.__loadedFileName == fileName:
+            return
+        # TODO: Loading file should be asyncronous to avoid freezing the gtk-main loop
+        try:
+            self.clear()
+            env.debug("ImageView is loading image from file: " + fileName)
+            self.__pixBuf = gtk.gdk.pixbuf_new_from_file(fileName)
+            self.__loadedFileName = fileName
+        except gobject.GError, e:
+            dialog = gtk.MessageDialog(
+                type=gtk.MESSAGE_ERROR,
+                buttons=gtk.BUTTONS_OK,
+                message_format="Could not load image: %s" % fileName)
+            dialog.run()
+            dialog.destroy()
+            self.__pixBuf = env.unknownImageIconPixbuf
+            self.__loadedFileName = None
+        self._newImageLoaded = True
+        self._image.show()
+        self.fitToWindow()
+
+    def clear(self):
+        self._image.hide()
+        self._image.set_from_file(None)
+        self.__pixBuf = None
+        self.__loadedFileName = None
+        gc.collect()
+        env.debug("ImageView is cleared.")
+
+    def renderImage(self):
+        # TODO: Scaling should be asyncronous to avoid freezing the gtk-main loop
+        if self.__pixBuf == None:
+            # No image loaded
+            self._image.hide()
+            return
+        if self.__currentZoom == self.__wantedZoom and not self._newImageLoaded:
+            return
+        if self.__wantedZoom == 0:
+            pixBufResized = self.__pixBuf
+        else:
+            zoomMultiplicator = pow(self._ZOOMFACTOR, self.__wantedZoom)
+            wantedWidth = int(self.__pixBuf.get_width() * zoomMultiplicator)
+            wantedHeight = int(self.__pixBuf.get_height() * zoomMultiplicator)
+            if min(wantedWidth, wantedHeight) < self._MIN_IMAGE_SIZE:
+                # Too small image size
+                return
+            if max(wantedWidth, wantedHeight) > self._MAX_IMAGE_SIZE:
+                # Too large image size
+                return
+            pixBufResized = self.__pixBuf.scale_simple(wantedWidth,
+                                                      wantedHeight,
+                                                      self._INTERPOLATION_TYPE)
+        pixMap, mask = pixBufResized.render_pixmap_and_mask()
+        self._image.set_from_pixmap(pixMap, mask)
+        self._newImageLoaded = False
+        self.__currentZoom = self.__wantedZoom
+        gc.collect()
+
+    def resizeEventHandler(self, widget, gdkEvent):
+        if self.__fitToWindowMode:
+            x, y, width, height = self.get_allocation()
+            if height != self.__previousWidgetHeight or width != self.__previousWidgetWidth:
+                self.fitToWindow()
+        return False
+
+    def fitToWindow(self, *foo):
+        self.__fitToWindowMode = True
+        self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
+        y, x, widgetWidth, widgetHeight = self.get_allocation()
+        if self.__pixBuf != None:
+            self.__previousWidgetWidth = widgetWidth
+            self.__previousWidgetHeight = widgetHeight
+            a = min(float(widgetWidth) / self.__pixBuf.get_width(),
+                    float(widgetHeight) / self.__pixBuf.get_height())
+            self.__wantedZoom = self._log(self._ZOOMFACTOR, a)
+            self.__wantedZoom = min(self.__wantedZoom, 0)
+            self.__wantedZoom = max(self.__wantedZoom, self._MIN_ZOOM)
+            self.renderImage()
+
+    def _log(self, base, value):
+        return math.log(value) / math.log(base)
+
+    def zoomIn(self, *foo):
+        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        self.__fitToWindowMode = False
+        if self.__wantedZoom <= self._MAX_ZOOM:
+            self.__wantedZoom = math.floor(self.__wantedZoom + 1)
+            self.renderImage()
+
+    def zoomOut(self, *foo):
+        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        self.__fitToWindowMode = False
+        if self.__wantedZoom >= self._MIN_ZOOM:
+            self.__wantedZoom = math.ceil(self.__wantedZoom - 1)
+            self.renderImage()
+
+    def zoom100(self, *foo):
+        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        self.__fitToWindowMode = False
+        self.__wantedZoom = 0
+        self.renderImage()
+
+    def scrollEventHandler(self, widget, gdkEvent):
+        if gdkEvent.type == gtk.gdk.SCROLL:
+            if gdkEvent.direction == gtk.gdk.SCROLL_UP:
+                self.zoomOut()
+            elif gdkEvent.direction == gtk.gdk.SCROLL_DOWN:
+                self.zoomIn()
+            return True
+        else:
+            return False
diff --git a/src/gkofoto/gkofoto/main.py b/src/gkofoto/gkofoto/main.py
new file mode 100644 (file)
index 0000000..dbd492a
--- /dev/null
@@ -0,0 +1,35 @@
+import sys
+from kofoto.clientenvironment import DEFAULT_CONFIGFILE_LOCATION
+from gkofoto.environment import env
+from gkofoto.controller import Controller
+from optparse import OptionParser
+
+def main(bindir, argv):
+    parser = OptionParser(version=env.version)
+    parser.add_option(
+        "--configfile",
+        type="string",
+        dest="configfile",
+        help="use configuration file CONFIGFILE instead of the default (%s)" % (
+            DEFAULT_CONFIGFILE_LOCATION),
+        default=None)
+    parser.add_option(
+        "--debug",
+        action="store_true",
+        help="print debug messages to stdout",
+        default=False)
+    parser.add_option(
+        "--shelf",
+        type="string",
+        dest="shelf",
+        help="use shelf SHELF instead of the default (specified in the configuration file)",
+        default=None)
+    options, args = parser.parse_args(argv[1:])
+
+    if len(args) != 0:
+        parser.error("incorrect number of arguments")
+
+    setupOk = env.setup(
+        bindir, options.debug, options.configfile, options.shelf)
+    env.controller = Controller()
+    env.controller.start(setupOk)
diff --git a/src/gkofoto/gkofoto/mainwindow.py b/src/gkofoto/gkofoto/mainwindow.py
new file mode 100644 (file)
index 0000000..ba90717
--- /dev/null
@@ -0,0 +1,205 @@
+import gtk
+import gtk.gdk
+import os
+
+from gkofoto.categories import *
+from gkofoto.albums import *
+from environment import env
+from gkofoto.tableview import *
+from gkofoto.thumbnailview import *
+from gkofoto.singleobjectview import *
+from gkofoto.objectcollectionfactory import *
+from gkofoto.objectcollection import *
+from gkofoto.registerimagesdialog import RegisterImagesDialog
+from gkofoto.handleimagesdialog import HandleImagesDialog
+from gkofoto.generatehtmldialog import GenerateHTMLDialog
+
+class MainWindow(gtk.Window):
+    def __init__(self):
+        env.mainwindow = self
+        self._toggleLock = False
+        self.__currentObjectCollection = None
+        self._currentView = None
+        self.__sourceEntry = env.widgets["sourceEntry"]
+        env.widgets["expandViewToggleButton"].connect("toggled", self._toggleExpandView)
+        env.widgets["expandViewToggleButton"].get_child().add(self.getIconImage("fullscreen-24.png"))
+#        env.widgets["thumbnailsViewToggleButton"].connect("clicked", self._toggleThumbnailsView)
+        env.widgets["thumbnailsViewToggleButton"].set_sensitive(False)
+        env.widgets["thumbnailsViewToggleButton"].get_child().add(self.getIconImage("thumbnailsview.png"))
+        env.widgets["objectViewToggleButton"].connect("clicked", self._toggleObjectView)
+        env.widgets["objectViewToggleButton"].get_child().add(self.getIconImage("objectview.png"))
+        env.widgets["menubarObjectView"].connect("activate", self._toggleObjectView)
+        env.widgets["tableViewToggleButton"].connect("clicked", self._toggleTableView)
+        env.widgets["tableViewToggleButton"].get_child().add(self.getIconImage("tableview.png"))
+        env.widgets["menubarTableView"].connect("activate", self._toggleTableView)
+        env.widgets["previousButton"].set_sensitive(False)
+        env.widgets["nextButton"].set_sensitive(False)
+        env.widgets["zoom100"].set_sensitive(False)
+        env.widgets["zoomToFit"].set_sensitive(False)
+        env.widgets["zoomIn"].set_sensitive(False)
+        env.widgets["zoomOut"].set_sensitive(False)
+
+        env.widgets["menubarSave"].connect("activate", env.controller.save)
+        env.widgets["menubarSave"].set_sensitive(False)
+        env.widgets["menubarRevert"].connect("activate", env.controller.revert)
+        env.widgets["menubarRevert"].set_sensitive(False)
+        env.widgets["menubarQuit"].connect("activate", env.controller.quit)
+
+        env.widgets["menubarThumbnailsView"].set_sensitive(False)
+
+        env.widgets["menubarNextImage"].set_sensitive(False)
+        env.widgets["menubarPreviousImage"].set_sensitive(False)
+        env.widgets["menubarZoom"].set_sensitive(False)
+
+        env.widgets["menubarRegisterImages"].connect("activate", self.registerImages, None)
+        env.widgets["menubarHandleModifiedOrRenamedImages"].connect(
+            "activate", self.handleModifiedOrRenamedImages, None)
+
+        env.widgets["menubarRotateLeft"].get_children()[1].set_from_pixbuf(
+            gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, "rotateleft.png")))
+        env.widgets["menubarRotateRight"].get_children()[1].set_from_pixbuf(
+            gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, "rotateright.png")))
+        env.widgets["menubarAbout"].get_children()[1].set_from_pixbuf(
+            gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, "about-icon.png")))
+
+        env.widgets["menubarAbout"].connect("activate", self.showAboutBox)
+
+        self.__sourceEntry.connect("activate", self._sourceEntryActivated)
+
+        env.shelf.registerModificationCallback(self._shelfModificationChangedCallback)
+
+        self.__factory = ObjectCollectionFactory()
+        self.__categories = Categories(self)
+        self.__albums = Albums(self)
+        self.__thumbnailView = ThumbnailView()
+        self.__tableView = TableView()
+        self.__singleObjectView = SingleObjectView()
+        self.__showTableView()
+
+    def _sourceEntryActivated(self, widget):
+        self.__setObjectCollection(self.__factory.getObjectCollection(
+            widget.get_text().decode("utf-8")))
+        self.__sourceEntry.grab_remove()
+
+    def setQuery(self, query):
+        self.__query = query
+        self.__sourceEntry.set_text(query)
+
+    def loadQuery(self, query):
+        self.setQuery(query)
+        self.__setObjectCollection(self.__factory.getObjectCollection(query))
+
+    def reload(self):
+        self.__albums.loadAlbumTree()
+        self.__categories.loadCategoryTree()
+        self.loadQuery(self.__query)
+
+    def reloadAlbumTree(self):
+        self.__albums.loadAlbumTree()
+
+    def registerImages(self, widget, data):
+        dialog = RegisterImagesDialog()
+        if dialog.run() == gtk.RESPONSE_OK:
+            self.reload() # TODO: don't reload everything.
+        dialog.destroy()
+
+    def handleModifiedOrRenamedImages(self, widget, data):
+        dialog = HandleImagesDialog()
+        dialog.run()
+        dialog.destroy()
+
+    def showAboutBox(self, *unused):
+        widgets = gtk.glade.XML(env.gladeFile, "aboutDialog")
+        aboutDialog = widgets.get_widget("aboutDialog")
+        nameAndVersionLabel = widgets.get_widget("nameAndVersionLabel")
+        nameAndVersionLabel.set_text("Kofoto %s" % env.version)
+        aboutDialog.run()
+        aboutDialog.destroy()
+
+    def generateHtml(self, album):
+        dialog = GenerateHTMLDialog(album)
+        dialog.run()
+
+    def getIconImage(self, name):
+        pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, name))
+        image = gtk.Image()
+        image.set_from_pixbuf(pixbuf)
+        image.show()
+        return image
+
+    def _viewChanged(self):
+        for hiddenView in self._hiddenViews:
+            hiddenView.hide()
+        self._currentView.show(self.__currentObjectCollection)
+
+    def __showTableView(self):
+        self._currentView = self.__tableView
+        self._hiddenViews = [self.__thumbnailView, self.__singleObjectView]
+        self._viewChanged()
+
+    def __showThumbnailView(self):
+        self._currentView = self.__thumbnailView
+        self._hiddenViews = [self.__tableView, self.__singleObjectView]
+        self._viewChanged()
+
+    def __showSingleObjectView(self):
+        self._currentView = self.__singleObjectView
+        self._hiddenViews = [self.__tableView, self.__thumbnailView]
+        self._viewChanged()
+
+    def _toggleExpandView(self, button):
+        if button.get_active():
+            env.widgets["sourceNotebook"].hide()
+        else:
+            env.widgets["sourceNotebook"].show()
+
+    def _toggleThumbnailsView(self, button):
+        if not self._toggleLock:
+            self._toggleLock = True
+            button.set_active(True)
+            env.widgets["thumbnailsViewToggleButton"].set_active(True)
+            env.widgets["objectViewToggleButton"].set_active(False)
+            env.widgets["tableViewToggleButton"].set_active(False)
+            env.widgets["menubarThumbnailsView"].set_active(True)
+            env.widgets["menubarObjectView"].set_active(False)
+            env.widgets["menubarTableView"].set_active(False)
+            self.__showThumbnailView()
+            self._toggleLock = False
+
+    def _toggleObjectView(self, button):
+        if not self._toggleLock:
+            self._toggleLock = True
+            button.set_active(True)
+            env.widgets["thumbnailsViewToggleButton"].set_active(False)
+            env.widgets["objectViewToggleButton"].set_active(True)
+            env.widgets["tableViewToggleButton"].set_active(False)
+            env.widgets["menubarThumbnailsView"].set_active(False)
+            env.widgets["menubarObjectView"].set_active(True)
+            env.widgets["menubarTableView"].set_active(False)
+            self.__showSingleObjectView()
+            self._toggleLock = False
+
+    def _toggleTableView(self, button):
+        if not self._toggleLock:
+            self._toggleLock = True
+            button.set_active(True)
+            env.widgets["thumbnailsViewToggleButton"].set_active(False)
+            env.widgets["objectViewToggleButton"].set_active(False)
+            env.widgets["tableViewToggleButton"].set_active(True)
+            env.widgets["menubarThumbnailsView"].set_active(False)
+            env.widgets["menubarObjectView"].set_active(False)
+            env.widgets["menubarTableView"].set_active(True)
+            self.__showTableView()
+            self._toggleLock = False
+
+    def _shelfModificationChangedCallback(self, modified):
+        env.widgets["menubarRevert"].set_sensitive(modified)
+        env.widgets["menubarSave"].set_sensitive(modified)
+
+    def __setObjectCollection(self, objectCollection):
+        if self.__currentObjectCollection != objectCollection:
+            env.debug("MainWindow is propagating a new ObjectCollection")
+            self.__currentObjectCollection = objectCollection
+            self.__categories.setCollection(objectCollection)
+            if self._currentView is not None:
+                self._currentView.setObjectCollection(objectCollection)
diff --git a/src/gkofoto/gkofoto/menuhandler.py b/src/gkofoto/gkofoto/menuhandler.py
new file mode 100644 (file)
index 0000000..b471d3a
--- /dev/null
@@ -0,0 +1,88 @@
+import gtk
+
+class MenuGroup:
+    def __init__(self, label=""):
+        self.__label = label
+        self.__childItems = []
+        self.__childItemsMap = {}
+        self.__radioGroup = None
+
+    def addMenuItem(self, label, callback, callbackData=None):
+        item = gtk.MenuItem(label)
+        self.__addItem(item, label, callback, callbackData)
+
+    def addStockImageMenuItem(self, label, stockId, callback,
+                              callbackData=None):
+        item = gtk.ImageMenuItem(label)
+        image = gtk.Image()
+        image.set_from_stock(stockId, gtk.ICON_SIZE_MENU)
+        item.set_image(image)
+        self.__addItem(item, label, callback, callbackData)
+
+    def addImageMenuItem(self, label, imageFilename, callback,
+                         callbackData=None):
+        item = gtk.ImageMenuItem(label)
+        image = gtk.Image()
+        image.set_from_file(imageFilename)
+        item.set_image(image)
+        self.__addItem(item, label, callback, callbackData)
+
+    def addCheckedMenuItem(self, label, callback, callbackData=None):
+        item = gtk.CheckMenuItem(label)
+        self.__addItem(item, label, callback, callbackData)
+
+    def addRadioMenuItem(self, label, callback, callbackData=None):
+        item = gtk.RadioMenuItem(self.__radioGroup, label)
+        self.__addItem(item, label, callback, callbackData)
+        self.__radioGroup = item
+
+    def addSeparator(self):
+        separator = gtk.SeparatorMenuItem()
+        self.__childItems.append(separator)
+        separator.show()
+        self.__radioGroup = None
+
+    def __getitem__(self, key):
+        return self.__childItemsMap[key]
+
+    def createGroupMenu(self):
+        menu = gtk.Menu()
+        for item in self:
+            menu.append(item)
+        menu.show()
+        return menu
+
+    def createGroupMenuItem(self):
+        menuItem = gtk.MenuItem(self.__label)
+        subMenu = self.createGroupMenu()
+        if len(self) > 0:
+            menuItem.set_submenu(subMenu)
+        else:
+            menuItem.set_sensitive(False)
+        menuItem.show()
+        return menuItem
+
+    def __len__(self):
+        return len(self.__childItems)
+
+    def __iter__(self):
+        for child in self.__childItems:
+            yield child
+
+    def enable(self):
+        for child in self.__childItems:
+            child.set_sensitive(True)
+
+    def disable(self):
+        for child in self.__childItems:
+            child.set_sensitive(False)
+
+    def __addItem(self, item, label, callback, callbackData=None):
+        if callbackData == None:
+            key = label
+        else:
+            key = callbackData
+        self.__childItemsMap[key] = item
+        self.__childItems.append(item)
+        item.connect("activate", callback, callbackData)
+        item.show()
diff --git a/src/gkofoto/gkofoto/mysortedmodel.py b/src/gkofoto/gkofoto/mysortedmodel.py
new file mode 100644 (file)
index 0000000..8a6585d
--- /dev/null
@@ -0,0 +1,40 @@
+import gtk
+
+class MySortedModel(gtk.TreeModelSort):
+
+    def __init__(self, model):
+        gtk.TreeModelSort.__init__(self, model)
+        self._model = model
+
+    def __getitem__(self, path):
+        child_path = self.convert_path_to_child_path(path)
+        if child_path:
+            return self._model[child_path]
+        else:
+            raise IndexError
+
+    def __delitem__(self, path):
+        child_path = self.convert_path_to_child_path(path)
+        if child_path:
+            del self._model[child_path]
+        else:
+            raise IndexError
+
+    def set_value(self, iter, column, value):
+        childIter = self._model.get_iter_first()
+        self.convert_iter_to_child_iter(childIter, iter)
+        self._model.set_value(childIter, column, value)
+
+    # Workaround until http://bugzilla.gnome.org/show_bug.cgi?id=121633 is solved.
+    def get_iter_first(self):
+        if len(self) > 0:
+            return gtk.TreeModelSort.get_iter_first(self)
+        else:
+            return None
+
+    # Workaround until http://bugzilla.gnome.org/show_bug.cgi?id=121633 is solved.
+    def __iter__(self):
+        if len(self._model) > 0:
+            return gtk.TreeModelSort.__iter__(self)
+        else:
+            return self._model.__iter__()
diff --git a/src/gkofoto/gkofoto/objectcollection.py b/src/gkofoto/gkofoto/objectcollection.py
new file mode 100644 (file)
index 0000000..35f6767
--- /dev/null
@@ -0,0 +1,437 @@
+import os
+import gtk
+import gobject
+import gc
+from sets import *
+from kofoto.shelf import *
+from menuhandler import *
+from environment import env
+from objectselection import *
+from albumdialog import AlbumDialog
+from registerimagesdialog import RegisterImagesDialog
+
+class ObjectCollection(object):
+
+######################################################################
+### Public
+
+    def __init__(self):
+        env.debug("Init ObjectCollection")
+        self.__objectSelection = ObjectSelection(self)
+        self.__registeredViews = []
+        self.__disabledFields = Set()
+        self.__columnsType = [ gobject.TYPE_BOOLEAN,  # COLUMN_VALID_LOCATION
+                               gobject.TYPE_BOOLEAN,  # COLUMN_VALID_CHECKSUM
+                               gobject.TYPE_BOOLEAN,  # COLUMN_ROW_EDITABLE
+                               gobject.TYPE_BOOLEAN,  # COLUMN_IS_ALBUM
+                               gobject.TYPE_INT,      # COLUMN_OBJECT_ID
+                               gobject.TYPE_STRING,   # COLUMN_LOCATION
+                               gtk.gdk.Pixbuf,        # COLUMN_THUMBNAIL
+                               gobject.TYPE_STRING ]  # COLUMN_ALBUM_TAG
+        self.__objectMetadataMap = {
+            u"id"       :(gobject.TYPE_INT,    self.COLUMN_OBJECT_ID, None,                 None),
+            u"location" :(gobject.TYPE_STRING, self.COLUMN_LOCATION,  None,                 None),
+            u"thumbnail":(gtk.gdk.Pixbuf,      self.COLUMN_THUMBNAIL, None,                 None),
+            u"albumtag" :(gobject.TYPE_STRING, self.COLUMN_ALBUM_TAG, self._albumTagEdited, self.COLUMN_ALBUM_TAG) }
+        for name in env.shelf.getAllAttributeNames():
+            self.__addAttribute(name)
+        self.__treeModel = gtk.ListStore(*self.__columnsType)
+
+    # Return true if the objects has a defined order and may
+    # be reordered. An object that is reorderable is not
+    # allowed to also be sortable.
+    def isReorderable(self):
+        return False
+
+    # Return true if the objects may be sorted.
+    def isSortable(self):
+        return False
+
+    # Return true if objects may be added and removed from the collection.
+    def isMutable(self):
+        return False
+
+    def getCutLabel(self):
+        return "Cut reference"
+
+    def getCopyLabel(self):
+        return "Copy reference"
+
+    def getPasteLabel(self):
+        return "Paste reference"
+
+    def getDeleteLabel(self):
+        return "Delete reference"
+
+    def getDestroyLabel(self):
+        return "Destroy..."
+
+    def getCreateAlbumChildLabel(self):
+        return "Create album child..."
+
+    def getRegisterImagesLabel(self):
+        return "Register and add images..."
+
+    def getGenerateHtmlLabel(self):
+        return "Generate HTML..."
+
+    def getAlbumPropertiesLabel(self):
+        return "Album properties..."
+
+    def getOpenImageLabel(self):
+        return "Open image in external program..."
+
+    def getRotateImageLeftLabel(self):
+        return "Rotate image left"
+
+    def getRotateImageRightLabel(self):
+        return "Rotate image right"
+
+    def getObjectMetadataMap(self):
+        return self.__objectMetadataMap
+
+    def getModel(self):
+        return self.__treeModel
+
+    def getUnsortedModel(self):
+        return self.__treeModel
+
+    def convertToUnsortedRowNr(self, rowNr):
+        return rowNr
+
+    def convertFromUnsortedRowNr(self, unsortedRowNr):
+        return unsortedRowNr
+
+    def getObjectSelection(self):
+        return self.__objectSelection
+
+    def getDisabledFields(self):
+        return self.__disabledFields
+
+    def registerView(self, view):
+        env.debug("Register view to object collection")
+        self.__registeredViews.append(view)
+
+    def unRegisterView(self, view):
+        env.debug("Unregister view from object collection")
+        self.__registeredViews.remove(view)
+
+    def clear(self, freeze=True):
+        env.debug("Clearing object collection")
+        if freeze:
+            self._freezeViews()
+        self.__treeModel.clear()
+        gc.collect()
+        self.__nrOfAlbums = 0
+        self.__nrOfImages = 0
+        self._handleNrOfObjectsUpdate()
+        self.__objectSelection.unselectAll()
+        if freeze:
+            self._thawViews()
+
+    def cut(self, *foo):
+        raise Exception("Error. Not allowed to cut objects into objectCollection.") # TODO
+
+    def copy(self, *foo):
+        env.clipboard.setObjects(self.__objectSelection.getSelectedObjects())
+
+    def paste(self, *foo):
+        raise Exception("Error. Not allowed to paste objects into objectCollection.") # TODO
+
+    def delete(self, *foo):
+        raise Exception("Error. Not allowed to delete objects from objectCollection.") # TODO
+
+    def destroy(self, *foo):
+        model = self.getModel()
+
+        albumsSelected = False
+        imagesSelected = False
+        for position in self.__objectSelection:
+            iterator = model.get_iter(position)
+            isAlbum = model.get_value(
+                iterator, self.COLUMN_IS_ALBUM)
+            if isAlbum:
+                albumsSelected = True
+            else:
+                imagesSelected = True
+
+        assert albumsSelected ^ imagesSelected
+
+        self._freezeViews()
+        if albumsSelected:
+            dialogId = "destroyAlbumsDialog"
+        else:
+            dialogId = "destroyImagesDialog"
+        widgets = gtk.glade.XML(env.gladeFile, dialogId)
+        dialog = widgets.get_widget(dialogId)
+        result = dialog.run()
+        if result == gtk.RESPONSE_OK:
+            if albumsSelected:
+                deleteFiles = False
+            else:
+                checkbutton = widgets.get_widget("deleteImageFilesCheckbutton")
+                deleteFiles = checkbutton.get_active()
+            for obj in self.__objectSelection.getSelectedObjects():
+                if deleteFiles:
+                    try:
+                        os.remove(obj.getLocation())
+                        # TODO: Delete from image cache too?
+                    except OSError:
+                        pass
+                env.shelf.deleteObject(obj.getId())
+            locations = list(self.getObjectSelection())
+            locations.sort()
+            locations.reverse()
+            for loc in locations:
+                del model[loc]
+            self.getObjectSelection().unselectAll()
+        dialog.destroy()
+        # TODO: If the removed objects are albums, update the album widget.
+        self._thawViews()
+
+    COLUMN_VALID_LOCATION = 0
+    COLUMN_VALID_CHECKSUM = 1
+    COLUMN_ROW_EDITABLE   = 2
+    COLUMN_IS_ALBUM       = 3
+
+    # Columns visible to user
+    COLUMN_OBJECT_ID      = 4
+    COLUMN_LOCATION       = 5
+    COLUMN_THUMBNAIL      = 6
+    COLUMN_ALBUM_TAG      = 7
+
+    # Content in objectMetadata fields
+    TYPE                 = 0
+    COLUMN_NR            = 1
+    EDITED_CALLBACK      = 2
+    EDITED_CALLBACK_DATA = 3
+
+
+
+######################################################################
+### Only for subbclasses
+
+    def _getRegisteredViews(self):
+        return self.__registeredViews
+
+    def _loadObjectList(self, objectList):
+        env.enter("Object collection loading objects.")
+        self._freezeViews()
+        self.clear(False)
+        self._insertObjectList(objectList)
+        self._thawViews()
+        env.exit("Object collection loading objects. (albums=" + str(self.__nrOfAlbums) + " images=" + str(self.__nrOfImages) + ")")
+
+    def _insertObjectList(self, objectList, location=None):
+        widgets = gtk.glade.XML(env.gladeFile, "loadingProgressDialog")
+        loadingProgressDialog = widgets.get_widget(
+            "loadingProgressDialog")
+        loadingProgressDialog.show()
+        while gtk.events_pending():
+            gtk.main_iteration()
+
+        # location = None means insert last, otherwise insert before
+        # location.
+        #
+        # Note that this methods does NOT update objectSelection.
+        if location == None:
+            location = len(self.__treeModel)
+        for obj in objectList:
+            iterator = self.__treeModel.insert(location)
+            self.__treeModel.set_value(iterator, self.COLUMN_OBJECT_ID, obj.getId())
+            if obj.isAlbum():
+                self.__treeModel.set_value(iterator, self.COLUMN_IS_ALBUM, True)
+                self.__treeModel.set_value(iterator, self.COLUMN_ALBUM_TAG, obj.getTag())
+                self.__treeModel.set_value(iterator, self.COLUMN_LOCATION, None)
+                self.__nrOfAlbums += 1
+            else:
+                self.__treeModel.set_value(iterator, self.COLUMN_IS_ALBUM, False)
+                self.__treeModel.set_value(iterator, self.COLUMN_ALBUM_TAG, None)
+                self.__treeModel.set_value(iterator, self.COLUMN_LOCATION, obj.getLocation())
+                self.__nrOfImages += 1
+                # TODO Set COLUMN_VALID_LOCATION and COLUMN_VALID_CHECKSUM
+            for attribute, value in obj.getAttributeMap().items():
+                if "@" + attribute in self.__objectMetadataMap:
+                    column = self.__objectMetadataMap["@" + attribute][self.COLUMN_NR]
+                    self.__treeModel.set_value(iterator, column, value)
+            self.__treeModel.set_value(iterator, self.COLUMN_ROW_EDITABLE, True)
+            self.__loadThumbnail(self.__treeModel, iterator)
+            location += 1
+        self._handleNrOfObjectsUpdate()
+
+        loadingProgressDialog.destroy()
+
+    def _handleNrOfObjectsUpdate(self):
+        updatedDisabledFields = Set()
+        if self.__nrOfAlbums == 0:
+            updatedDisabledFields.add(u"albumtag")
+        if self.__nrOfImages == 0:
+            updatedDisabledFields.add(u"location")
+        for view in self.__registeredViews:
+            view.fieldsDisabled(updatedDisabledFields - self.__disabledFields)
+            view.fieldsEnabled(self.__disabledFields - updatedDisabledFields)
+        self.__disabledFields = updatedDisabledFields
+        env.debug("The following fields are disabled: " + str(self.__disabledFields))
+
+    def _getTreeModel(self):
+        return self.__treeModel
+
+    def _freezeViews(self):
+        for view in self.__registeredViews:
+            view.freeze()
+
+    def _thawViews(self):
+        for view in self.__registeredViews:
+            view.thaw()
+
+
+###############################################################################
+### Callback functions
+
+    def _attributeEdited(self, renderer, path, value, column, attributeName):
+        model = self.getModel()
+        columnNumber = self.__objectMetadataMap["@" + attributeName][self.COLUMN_NR]
+        iterator = model.get_iter(path)
+        oldValue = model.get_value(iterator, columnNumber)
+        if not oldValue:
+            oldValue = u""
+        value = unicode(value, "utf-8")
+        if oldValue != value:
+            # TODO Show dialog and ask for confirmation?
+            objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
+            obj = env.shelf.getObject(objectId)
+            obj.setAttribute(attributeName, value)
+            model.set_value(iterator, columnNumber, value)
+            env.debug("Object attribute edited")
+
+    def _albumTagEdited(self, renderer, path, value, column, columnNumber):
+        model = self.getModel()
+        assert model.get_value(iterator, self.COLUMN_IS_ALBUM)
+        iterator = model.get_iter(path)
+        oldValue = model.get_value(iterator, columnNumber)
+        if not oldValue:
+            oldValue = u""
+        value = unicode(value, "utf-8")
+        if oldValue != value:
+            # TODO Show dialog and ask for confirmation?
+            objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
+            obj = env.shelf.getAlbum(objectId)
+            obj.setTag(value)
+            # TODO Handle invalid album tag?
+            model.set_value(iterator, columnNumber, value)
+            # TODO Update the album tree widget.
+            env.debug("Album tag edited")
+
+    def createAlbumChild(self, *unused):
+        dialog = AlbumDialog("Create album")
+        dialog.run(self._createAlbumChildHelper)
+
+    def _createAlbumChildHelper(self, tag, desc):
+        newAlbum = env.shelf.createAlbum(tag)
+        if len(desc) > 0:
+            newAlbum.setAttribute(u"title", desc)
+        selectedObjects = self.__objectSelection.getSelectedObjects()
+        selectedAlbum = selectedObjects[0]
+        children = list(selectedAlbum.getChildren())
+        children.append(newAlbum)
+        selectedAlbum.setChildren(children)
+        env.mainwindow.reloadAlbumTree()
+
+    def registerAndAddImages(self, *unused):
+        selectedObjects = self.__objectSelection.getSelectedObjects()
+        assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
+        selectedAlbum = selectedObjects[0]
+        dialog = RegisterImagesDialog(selectedAlbum)
+        if dialog.run() == gtk.RESPONSE_OK:
+            env.mainwindow.reload() # TODO: Don't reload everything.
+        dialog.destroy()
+
+    def generateHtml(self, *unused):
+        selectedObjects = self.__objectSelection.getSelectedObjects()
+        assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
+        selectedAlbum = selectedObjects[0]
+        env.mainwindow.generateHtml(selectedAlbum)
+
+    def albumProperties(self, widget, data):
+        selectedObjects = self.__objectSelection.getSelectedObjects()
+        assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
+        selectedAlbumId = selectedObjects[0].getId()
+        dialog = AlbumDialog("Edit album", selectedAlbumId)
+        dialog.run(self._albumPropertiesHelper)
+
+    def _albumPropertiesHelper(self, tag, desc):
+        selectedObjects = self.__objectSelection.getSelectedObjects()
+        selectedAlbum = selectedObjects[0]
+        selectedAlbum.setTag(tag)
+        if len(desc) > 0:
+            selectedAlbum.setAttribute(u"title", desc)
+        else:
+            selectedAlbum.deleteAttribute(u"title")
+        env.mainwindow.reloadAlbumTree()
+        # TODO: Update objectCollection.
+
+    def rotateImage(self, widget, angle):
+        for (rowNr, obj) in self.__objectSelection.getMap().items():
+            if not obj.isAlbum():
+                location = obj.getLocation().encode(env.codeset)
+                if angle == 90:
+                    commandString = env.rotateRightCommand
+                else:
+                    commandString = env.rotateLeftCommand
+                command = commandString.encode(env.codeset) % { "location":location }
+                result = os.system(command)
+                if result == 0:
+                    obj.contentChanged()
+                    model = self.getUnsortedModel()
+                    self.__loadThumbnail(model, model.get_iter(rowNr))
+                else:
+                    dialog = gtk.MessageDialog(
+                        type=gtk.MESSAGE_ERROR,
+                        buttons=gtk.BUTTONS_OK,
+                        message_format="Failed to execute command: \"%s\"" % command)
+                    dialog.run()
+                    dialog.destroy()
+
+    def openImage(self, widget, data):
+        locations = ""
+        for obj in self.__objectSelection.getSelectedObjects():
+            if not obj.isAlbum():
+                location = obj.getLocation()
+                locations += location + " "
+        if locations != "":
+            command = env.openCommand % { "locations":locations }
+            # GIMP does not seem to be able to open locations containing swedish
+            # characters. I tried latin-1 and utf-8 without success.
+            result = os.system(command + " &")
+            if result != 0:
+                dialog = gtk.MessageDialog(
+                    type=gtk.MESSAGE_ERROR,
+                    buttons=gtk.BUTTONS_OK,
+                    message_format="Failed to execute command: \"%s\"" % command)
+                dialog.run()
+                dialog.destroy()
+
+######################################################################
+### Private
+
+    def __addAttribute(self, name):
+        self.__objectMetadataMap["@" + name] = (gobject.TYPE_STRING,
+                                                len(self.__columnsType),
+                                                self._attributeEdited,
+                                                name)
+        self.__columnsType.append(gobject.TYPE_STRING)
+
+    def __loadThumbnail(self, model, iterator):
+        objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
+        obj = env.shelf.getObject(objectId)
+        if obj.isAlbum():
+            pixbuf = env.albumIconPixbuf
+        else:
+            try:
+                thumbnailLocation = env.imageCache.get(
+                    obj, env.thumbnailSize[0], env.thumbnailSize[1])[0]
+                pixbuf = gtk.gdk.pixbuf_new_from_file(thumbnailLocation.encode(env.codeset))
+                # TODO Set and use COLUMN_VALID_LOCATION and COLUMN_VALID_CHECKSUM
+            except IOError:
+                pixbuf = env.unknownImageIconPixbuf
+        model.set_value(iterator, self.COLUMN_THUMBNAIL, pixbuf)
diff --git a/src/gkofoto/gkofoto/objectcollectionfactory.py b/src/gkofoto/gkofoto/objectcollectionfactory.py
new file mode 100644 (file)
index 0000000..ed9b4bd
--- /dev/null
@@ -0,0 +1,64 @@
+import string
+from searchresult import *
+from albummembers import *
+from environment import env
+from kofoto.search import *
+from kofoto.shelf import *
+
+class ObjectCollectionFactory:
+
+######################################################################
+### Public functions and constants
+
+    def __init__(self):
+        env.debug("Init ObjectCollectionFactory")
+        self.__searchResult = SearchResult()
+        self.__albumMembers = AlbumMembers()
+
+    def getObjectCollection(self, query):
+        env.debug("Object collection factory loading query: " + query);
+        self.__clear()
+        validAlbumTag = False
+        if query and query[0] == "/":
+            try:
+                verifyValidAlbumTag(query[1:])
+                validAlbumTag = True
+            except BadAlbumTagError:
+                pass
+        try:
+            if validAlbumTag:
+                self.__albumMembers.loadAlbum(env.shelf.getAlbum(query[1:]))
+                return self.__albumMembers
+            else:
+                self.__searchResult.loadQuery(query)
+                return self.__searchResult
+        except AlbumDoesNotExistError, tag:
+            errorText = "No such album tag: \"%s\"." % tag
+        except CategoryDoesNotExistError, tag:
+            errorText = "No such category tag: \"%s\"." % tag
+        except BadTokenError, pos:
+            errorText = "Error parsing query: bad token starting at position %s: \"%s\"." % (
+                pos,
+                query[pos[0]:])
+        except UnterminatedStringError, e:
+            errorText = "Error parsing query: unterminated string starting at position %s: \"%s\"." % (
+                e.args[0],
+                query[e.args[0]:])
+        except ParseError, text:
+            errorText = "Error parsing query: %s." % text
+        dialog = gtk.MessageDialog(
+            type=gtk.MESSAGE_ERROR,
+            buttons=gtk.BUTTONS_OK,
+            message_format=errorText)
+        dialog.run()
+        dialog.destroy()
+        self.__searchResult = SearchResult()
+        return self.__searchResult
+
+
+######################################################################
+### Private functions
+
+    def __clear(self):
+        self.__searchResult.clear()
+        self.__albumMembers.clear()
diff --git a/src/gkofoto/gkofoto/objectcollectionview.py b/src/gkofoto/gkofoto/objectcollectionview.py
new file mode 100644 (file)
index 0000000..7f57d5c
--- /dev/null
@@ -0,0 +1,327 @@
+import gtk
+from environment import env
+from menuhandler import *
+from objectcollection import *
+
+class ObjectCollectionView:
+
+###############################################################################
+### Public
+
+    def __init__(self, view):
+        self._viewWidget = view
+        self._objectCollection = None
+        self._contextMenu = None
+        self.__objectCollectionLoaded = False
+        self.__hidden = True
+        self.__connections = []
+        view.connect("button_press_event", self._mouse_button_pressed)
+
+    def show(self, objectCollection):
+        if self.__hidden:
+            self.__hidden = False
+            self.__connectObjectCollection(objectCollection)
+            self._showHelper()
+            self._connectMenubarImageItems()
+            self._updateMenubarSortMenu()
+        else:
+            self.setObjectCollection(objectCollection)
+
+    def _connectMenubarImageItems(self):
+        self._connect(
+            env.widgets["menubarOpenImage"],
+            "activate",
+            self._objectCollection.openImage)
+        self._connect(
+            env.widgets["menubarRotateLeft"],
+            "activate",
+            self._objectCollection.rotateImage,
+            270)
+        self._connect(
+            env.widgets["menubarRotateRight"],
+            "activate",
+            self._objectCollection.rotateImage,
+            90)
+
+    def _updateMenubarSortMenu(self):
+        sortMenuGroup = self.__createSortMenuGroup(self._objectCollection)
+        sortByItem = env.widgets["menubarSortBy"]
+        if self._objectCollection.isSortable():
+            sortByItem.set_sensitive(True)
+            sortByItem.set_submenu(sortMenuGroup.createGroupMenu())
+        else:
+            sortByItem.remove_submenu()
+            sortByItem.set_sensitive(False)
+
+    def hide(self):
+        if not self.__hidden:
+            self.__hidden = True
+            self._hideHelper()
+            self.__disconnectObjectCollection()
+
+    def setObjectCollection(self, objectCollection):
+        if not self.__hidden:
+            env.debug("ObjectCollectionView sets object collection")
+            self.__connectObjectCollection(objectCollection)
+
+    def freeze(self):
+        self._freezeHelper()
+        self._objectCollection.getObjectSelection().removeChangedCallback(self.importSelection)
+        env.clipboard.removeChangedCallback(self._updateContextMenu)
+
+    def thaw(self):
+        self._thawHelper()
+        self._objectCollection.getObjectSelection().addChangedCallback(self.importSelection)
+        env.clipboard.addChangedCallback(self._updateContextMenu)
+        self.importSelection(self._objectCollection.getObjectSelection())
+        # importSelection makes an implicit _updateContextMenu()
+
+    def sortOrderChanged(self, sortOrder):
+        env.debug("Sort order is " + str(sortOrder))
+        self.__sortMenuGroup[sortOrder].activate()
+
+    def sortColumnChanged(self, sortColumn):
+        env.debug("Sort column is " + str(sortColumn))
+        self.__sortMenuGroup[sortColumn].activate()
+
+    def fieldsDisabled(self, fields):
+        pass
+
+    def fieldsEnabled(self, fields):
+        pass
+
+    def _mouse_button_pressed(self, widget, event):
+        if event.button == 3:
+            self._contextMenu.popup(None, None, None, event.button, event.time)
+            return True
+        else:
+            return False
+
+##############################################################################
+### Methods used by and overloaded by subbclasses
+
+    def _connect(self, obj, signal, function, data=None):
+        oid = obj.connect(signal, function, data)
+        self.__connections.append((obj, oid))
+
+    def _disconnect(self, obj, oid):
+        obj.disconnect(oid)
+        self.__connections.remove((obj, oid))
+
+    def _clearAllConnections(self):
+        for (obj, oid) in self.__connections:
+            obj.disconnect(oid)
+        self.__connections = []
+
+    def _createContextMenu(self, objectCollection):
+        env.debug("Creating view context menu")
+        self._contextMenu = gtk.Menu()
+        self.__clipboardMenuGroup = self.__createClipboardMenuGroup(objectCollection)
+        for item in self.__clipboardMenuGroup:
+            self._contextMenu.add(item)
+        self.__objectMenuGroup = self.__createObjectMenuGroup(objectCollection)
+        for item in self.__objectMenuGroup:
+            self._contextMenu.add(item)
+        self.__albumMenuGroup = self.__createAlbumMenuGroup(objectCollection)
+        for item in self.__albumMenuGroup:
+            self._contextMenu.add(item)
+        self.__imageMenuGroup = self.__createImageMenuGroup(objectCollection)
+        for item in self.__imageMenuGroup:
+            self._contextMenu.add(item)
+        self.__sortMenuGroup = self.__createSortMenuGroup(objectCollection)
+        self._contextMenu.add(self.__sortMenuGroup.createGroupMenuItem())
+
+    def _clearContextMenu(self):
+        env.debug("Clearing view context menu")
+        self._contextMenu = None
+        self.__clipboardMenuGroup = None
+        self.__objectMenuGroup = None
+        self.__albumMenuGroup = None
+        self.__imageMenuGroup = None
+        self.__sortMenuGroup = None
+
+    def _updateContextMenu(self, *foo):
+        env.debug("Updating context menu")
+        self.__objectMenuGroup[self._objectCollection.getDestroyLabel()].set_sensitive(False)
+        env.widgets["menubarDestroy"].set_sensitive(False)
+        mutable = self._objectCollection.isMutable()
+        objectSelection = self._objectCollection.getObjectSelection()
+        if objectSelection:
+            model = self._objectCollection.getModel()
+            rootAlbumId = env.shelf.getRootAlbum().getId()
+
+            albumsSelected = 0
+            imagesSelected = 0
+            rootAlbumSelected = False
+            for position in objectSelection:
+                iterator = model.get_iter(position)
+                isAlbum = model.get_value(
+                    iterator, self._objectCollection.COLUMN_IS_ALBUM)
+                if isAlbum:
+                    albumsSelected += 1
+                    if rootAlbumId == model.get_value(
+                        iterator, self._objectCollection.COLUMN_OBJECT_ID):
+                        rootAlbumSelected = True
+                else:
+                    imagesSelected += 1
+
+            self.__clipboardMenuGroup[self._objectCollection.getCutLabel()].set_sensitive(mutable)
+            env.widgets["menubarCut"].set_sensitive(mutable)
+            self.__clipboardMenuGroup[self._objectCollection.getCopyLabel()].set_sensitive(True)
+            env.widgets["menubarCopy"].set_sensitive(True)
+            self.__clipboardMenuGroup[self._objectCollection.getDeleteLabel()].set_sensitive(mutable)
+            env.widgets["menubarDelete"].set_sensitive(mutable)
+            destroyActive = (imagesSelected == 0) ^ (albumsSelected == 0) and not rootAlbumSelected
+            self.__objectMenuGroup[self._objectCollection.getDestroyLabel()].set_sensitive(destroyActive)
+            env.widgets["menubarDestroy"].set_sensitive(destroyActive)
+            if albumsSelected == 1 and imagesSelected == 0:
+                selectedAlbumId = model.get_value(
+                    iterator, self._objectCollection.COLUMN_OBJECT_ID)
+                selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
+                if selectedAlbum.isMutable():
+                    self.__albumMenuGroup.enable()
+                    env.widgets["menubarCreateAlbumChild"].set_sensitive(True)
+                    env.widgets["menubarRegisterAndAddImages"].set_sensitive(True)
+                    env.widgets["menubarGenerateHtml"].set_sensitive(True)
+                    env.widgets["menubarProperties"].set_sensitive(True)
+                else:
+                    self.__albumMenuGroup.disable()
+                    self.__albumMenuGroup[self._objectCollection.getAlbumPropertiesLabel()].set_sensitive(True)
+                    env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
+                    env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
+                    env.widgets["menubarGenerateHtml"].set_sensitive(True)
+                    env.widgets["menubarProperties"].set_sensitive(True)
+            else:
+                self.__albumMenuGroup.disable()
+                env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
+                env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
+                env.widgets["menubarGenerateHtml"].set_sensitive(False)
+                env.widgets["menubarProperties"].set_sensitive(False)
+            if albumsSelected == 0 and imagesSelected > 0:
+                self.__imageMenuGroup.enable()
+                env.widgets["menubarOpenImage"].set_sensitive(True)
+                env.widgets["menubarRotateLeft"].set_sensitive(True)
+                env.widgets["menubarRotateRight"].set_sensitive(True)
+            else:
+                self.__imageMenuGroup.disable()
+                env.widgets["menubarOpenImage"].set_sensitive(False)
+                env.widgets["menubarRotateLeft"].set_sensitive(False)
+                env.widgets["menubarRotateRight"].set_sensitive(False)
+        else:
+            self.__clipboardMenuGroup.disable()
+            env.widgets["menubarCut"].set_sensitive(False)
+            env.widgets["menubarCopy"].set_sensitive(False)
+            env.widgets["menubarDelete"].set_sensitive(False)
+
+            self.__objectMenuGroup.disable()
+            env.widgets["menubarDestroy"].set_sensitive(False)
+
+            self.__albumMenuGroup.disable()
+            env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
+            env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
+            env.widgets["menubarGenerateHtml"].set_sensitive(False)
+            env.widgets["menubarProperties"].set_sensitive(False)
+
+            self.__imageMenuGroup.disable()
+            env.widgets["menubarOpenImage"].set_sensitive(False)
+            env.widgets["menubarRotateLeft"].set_sensitive(False)
+            env.widgets["menubarRotateRight"].set_sensitive(False)
+
+        if env.clipboard.hasObjects():
+            self.__clipboardMenuGroup[self._objectCollection.getPasteLabel()].set_sensitive(mutable)
+            env.widgets["menubarPaste"].set_sensitive(mutable)
+        else:
+            self.__clipboardMenuGroup[self._objectCollection.getPasteLabel()].set_sensitive(False)
+            env.widgets["menubarPaste"].set_sensitive(False)
+
+
+###############################################################################
+### Private
+
+    def __connectObjectCollection(self, objectCollection):
+        if self._objectCollection != None:
+            self.__disconnectObjectCollection()
+        self._objectCollection = objectCollection
+        self._createContextMenu(objectCollection)
+        self._connectObjectCollectionHelper()
+        self.thaw()
+        self._objectCollection.registerView(self)
+
+    def __disconnectObjectCollection(self):
+        if self._objectCollection is not None:
+            self._objectCollection.unRegisterView(self)
+            self.freeze()
+            self._disconnectObjectCollectionHelper()
+            self._clearContextMenu()
+            self._objectCollection = None
+
+    def __createSortMenuGroup(self, objectCollection):
+        menuGroup = MenuGroup("Sort by")
+        if objectCollection.isSortable():
+            env.debug("Creating sort menu group for sortable log collection")
+            menuGroup.addRadioMenuItem("Ascending",
+                                       objectCollection.setSortOrder,
+                                       gtk.SORT_ASCENDING)
+            menuGroup.addRadioMenuItem("Descending",
+                                       objectCollection.setSortOrder,
+                                       gtk.SORT_DESCENDING)
+            menuGroup.addSeparator()
+            objectMetadataMap = objectCollection.getObjectMetadataMap()
+            columnNames = list(objectMetadataMap.keys())
+            columnNames.sort()
+            for columnName in columnNames:
+                if objectMetadataMap[columnName][ObjectCollection.TYPE] != gtk.gdk.Pixbuf:
+                    menuGroup.addRadioMenuItem(columnName,
+                                               objectCollection.setSortColumnName,
+                                               columnName)
+        return menuGroup
+
+    def __createClipboardMenuGroup(self, oc):
+        menuGroup = MenuGroup()
+        env.debug("Creating clipboard menu")
+        menuGroup.addStockImageMenuItem(
+            oc.getCutLabel(), gtk.STOCK_CUT, oc.cut)
+        menuGroup.addStockImageMenuItem(
+            oc.getCopyLabel(), gtk.STOCK_COPY, oc.copy)
+        menuGroup.addStockImageMenuItem(
+            oc.getPasteLabel(), gtk.STOCK_PASTE, oc.paste)
+        menuGroup.addStockImageMenuItem(
+            oc.getDeleteLabel(), gtk.STOCK_DELETE, oc.delete)
+        menuGroup.addSeparator()
+        return menuGroup
+
+    def __createObjectMenuGroup(self, oc):
+        menuGroup = MenuGroup()
+        menuGroup.addStockImageMenuItem(
+            oc.getDestroyLabel(), gtk.STOCK_DELETE, oc.destroy)
+        menuGroup.addSeparator()
+        return menuGroup
+
+    def __createAlbumMenuGroup(self, oc):
+        menuGroup = MenuGroup()
+        menuGroup.addMenuItem(
+            oc.getCreateAlbumChildLabel(), oc.createAlbumChild)
+        menuGroup.addMenuItem(
+            oc.getRegisterImagesLabel(), oc.registerAndAddImages)
+        menuGroup.addMenuItem(
+            oc.getGenerateHtmlLabel(), oc.generateHtml)
+        menuGroup.addStockImageMenuItem(
+            oc.getAlbumPropertiesLabel(),
+            gtk.STOCK_PROPERTIES,
+            oc.albumProperties)
+        menuGroup.addSeparator()
+        return menuGroup
+
+    def __createImageMenuGroup(self, oc):
+        menuGroup = MenuGroup()
+        menuGroup.addMenuItem(oc.getOpenImageLabel(), oc.openImage)
+        menuGroup.addImageMenuItem(
+            oc.getRotateImageLeftLabel(),
+            os.path.join(env.iconDir, "rotateleft.png"),
+            oc.rotateImage, 270)
+        menuGroup.addImageMenuItem(
+            oc.getRotateImageRightLabel(),
+            os.path.join(env.iconDir, "rotateright.png"),
+            oc.rotateImage, 90)
+        menuGroup.addSeparator()
+        return menuGroup
diff --git a/src/gkofoto/gkofoto/objectselection.py b/src/gkofoto/gkofoto/objectselection.py
new file mode 100644 (file)
index 0000000..d127847
--- /dev/null
@@ -0,0 +1,85 @@
+from environment import env
+from sets import Set
+
+class ObjectSelection:
+    def __init__(self, objectCollection):
+        # Don't forget to update this class when the model is reordered or
+        # when rows are removed or added.
+        self.__selectedObjects = {}
+        # When objects are stored in self.__selectedObjects, the key MUST be
+        # the location in the UNSORTED model since this class is not
+        # notified when/if the model is re-sorted.
+        #
+        # This class must know about each object's row to be able to distinguish
+        # individual objects in an album that contains multiple instances
+        # of the same image or album.
+        self.__changedCallbacks = Set()
+        self.__objectCollection = objectCollection
+    def addChangedCallback(self, callback):
+        self.__changedCallbacks.add(callback)
+
+    def removeChangedCallback(self, callback):
+        self.__changedCallbacks.remove(callback)
+
+    def unselectAll(self, notify=True):
+        self.__selectedObjects.clear()
+        if notify:
+            self.__invokeChangedCallbacks()
+
+    def setSelection(self, rowNrs, notify=True):
+        self.__selectedObjects.clear()
+        for rowNr in rowNrs:
+            self.addSelection(rowNr, False)
+        if notify:
+            self.__invokeChangedCallbacks()
+
+    def addSelection(self, rowNr, notify=True):
+        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
+        self.__selectedObjects[unsortedRowNr] = self.__getObject(unsortedRowNr)
+        if notify:
+            self.__invokeChangedCallbacks()
+
+    def removeSelection(self, rowNr, notify=True):
+        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
+        del self.__selectedObjects[unsortedRowNr]
+        if notify:
+            self.__invokeChangedCallbacks()
+
+    def getSelectedObjects(self):
+        return self.__selectedObjects.values()
+
+    def getLowestSelectedRowNr(self):
+        rowNrs = list(self)
+        if (len(rowNrs) > 0):
+            rowNrs.sort()
+            return rowNrs[0]
+        else:
+            return None
+
+    def getMap(self):
+        return self.__selectedObjects
+
+    def __contains__(self, rowNr):
+        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
+        return unsortedRowNr in self.__selectedObjects.keys()
+
+    def __len__(self):
+        return len(self.__selectedObjects)
+
+    def __iter__(self):
+        for unsortedRowNr in self.__selectedObjects.keys():
+            rowNr = self.__objectCollection.convertFromUnsortedRowNr(unsortedRowNr)
+            yield rowNr
+
+    def __getitem__(self, rowNr):
+        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
+        return self.__selectedObjects[unsortedRowNr]
+
+    def __invokeChangedCallbacks(self):
+        env.debug("Invoking selection changed callbacks: " + str(self.__selectedObjects.keys()))
+        for callback in self.__changedCallbacks:
+            callback(self)
+
+    def __getObject(self, unsortedRowNr):
+        objectId = self.__objectCollection.getUnsortedModel()[unsortedRowNr][self.__objectCollection.COLUMN_OBJECT_ID]
+        return env.shelf.getObject(objectId)
diff --git a/src/gkofoto/gkofoto/registerimagesdialog.py b/src/gkofoto/gkofoto/registerimagesdialog.py
new file mode 100644 (file)
index 0000000..a86599c
--- /dev/null
@@ -0,0 +1,62 @@
+import gtk
+import os
+from environment import env
+from kofoto.shelf import ImageExistsError, NotAnImageError, makeValidTag
+from kofoto.clientutils import walk_files
+
+class RegisterImagesDialog(gtk.FileSelection):
+    def __init__(self, albumToAddTo=None):
+        gtk.FileSelection.__init__(self, title="Register images")
+        self.__albumToAddTo = albumToAddTo
+        self.set_select_multiple(True)
+        self.ok_button.connect("clicked", self._ok)
+
+    def _ok(self, widget):
+        widgets = gtk.glade.XML(env.gladeFile, "registrationProgressDialog")
+        registrationProgressDialog = widgets.get_widget(
+            "registrationProgressDialog")
+        newImagesCount = widgets.get_widget(
+            "newImagesCount")
+        alreadyRegisteredImagesCount = widgets.get_widget(
+            "alreadyRegisteredImagesCount")
+        nonImagesCount = widgets.get_widget(
+            "nonImagesCount")
+        filesInvestigatedCount = widgets.get_widget(
+            "filesInvestigatedCount")
+        okButton = widgets.get_widget("okButton")
+        okButton.set_sensitive(False)
+
+        registrationProgressDialog.show()
+
+        newImages = 0
+        alreadyRegisteredImages = 0
+        nonImages = 0
+        filesInvestigated = 0
+        images = []
+        for filepath in walk_files(self.get_selections()):
+            try:
+                try:
+                    filepath = filepath.decode("utf-8")
+                except UnicodeDecodeError:
+                    filepath = filepath.decode("latin1")
+                image = env.shelf.createImage(filepath)
+                images.append(image)
+                newImages += 1
+                newImagesCount.set_text(str(newImages))
+            except ImageExistsError:
+                alreadyRegisteredImages += 1
+                alreadyRegisteredImagesCount.set_text(str(alreadyRegisteredImages))
+            except NotAnImageError:
+                nonImages += 1
+                nonImagesCount.set_text(str(nonImages))
+            filesInvestigated += 1
+            filesInvestigatedCount.set_text(str(filesInvestigated))
+            while gtk.events_pending():
+                gtk.main_iteration()
+        if self.__albumToAddTo:
+            children = list(self.__albumToAddTo.getChildren())
+            self.__albumToAddTo.setChildren(children + images)
+
+        okButton.set_sensitive(True)
+        registrationProgressDialog.run()
+        registrationProgressDialog.destroy()
diff --git a/src/gkofoto/gkofoto/searchresult.py b/src/gkofoto/gkofoto/searchresult.py
new file mode 100644 (file)
index 0000000..b2aac8e
--- /dev/null
@@ -0,0 +1,19 @@
+from kofoto.shelf import *
+from kofoto.search import *
+from sortableobjectcollection import *
+from environment import env
+
+class SearchResult(SortableObjectCollection):
+
+######################################################################
+### Public functions and constants
+
+    def __init__(self):
+        SortableObjectCollection.__init__(self)
+
+    def loadQuery(self, query):
+        parser = Parser(env.shelf)
+        self._loadObjectList(env.shelf.search(parser.parse(query)))
+
+######################################################################
+### Private functions and datastructures
diff --git a/src/gkofoto/gkofoto/singleobjectview.py b/src/gkofoto/gkofoto/singleobjectview.py
new file mode 100644 (file)
index 0000000..4cd427d
--- /dev/null
@@ -0,0 +1,161 @@
+import gtk
+import sys
+from environment import env
+from gkofoto.imageview import *
+from gkofoto.objectcollectionview import *
+
+class SingleObjectView(ObjectCollectionView, ImageView):
+
+###############################################################################
+### Public
+
+    def __init__(self):
+        env.debug("Init SingleObjectView")
+        ImageView.__init__(self)
+        ObjectCollectionView.__init__(self, env.widgets["objectView"])
+        self._viewWidget.add(self)
+        self.show_all()
+        env.widgets["nextButton"].connect("clicked", self._goto, 1)
+        env.widgets["menubarNextImage"].connect("activate", self._goto, 1)
+        env.widgets["previousButton"].connect("clicked", self._goto, -1)
+        env.widgets["menubarPreviousImage"].connect("activate", self._goto, -1)
+        env.widgets["zoomToFit"].connect("clicked", self.fitToWindow)
+        env.widgets["menubarZoomToFit"].connect("activate", self.fitToWindow)
+        env.widgets["zoom100"].connect("clicked", self.zoom100)
+        env.widgets["menubarActualSize"].connect("activate", self.zoom100)
+        env.widgets["zoomIn"].connect("clicked", self.zoomIn)
+        env.widgets["menubarZoomIn"].connect("activate", self.zoomIn)
+        env.widgets["zoomOut"].connect("clicked", self.zoomOut)
+        env.widgets["menubarZoomOut"].connect("activate", self.zoomOut)
+        self.connect("button_press_event", self._mouse_button_pressed)
+        self.__selectionLocked = False
+
+    def importSelection(self, objectSelection):
+        if not self.__selectionLocked:
+            env.debug("SingleImageView is importing selection")
+            self.__selectionLocked = True
+            model = self._objectCollection.getModel()
+            if len(model) == 0:
+                # Model is empty. No rows can be selected.
+                self.__selectedRowNr = -1
+                self.clear()
+            else:
+                if len(objectSelection) == 0:
+                    # No objects is selected -> select first object
+                    self.__selectedRowNr = 0
+                    objectSelection.setSelection([self.__selectedRowNr])
+                elif len(objectSelection) > 1:
+                    # More than one object selected -> select first object
+                    self.__selectedRowNr = objectSelection.getLowestSelectedRowNr()
+                    objectSelection.setSelection([self.__selectedRowNr])
+                else:
+                    # Exactly one object selected
+                    self.__selectedRowNr = objectSelection.getLowestSelectedRowNr()
+                selectedObject = objectSelection[self.__selectedRowNr]
+                if selectedObject.isAlbum():
+                    self.loadFile(env.albumIconFileName, False)
+                else:
+                    self.loadFile(selectedObject.getLocation(), False)
+            enablePreviousButton = (self.__selectedRowNr > 0)
+            env.widgets["previousButton"].set_sensitive(enablePreviousButton)
+            env.widgets["menubarPreviousImage"].set_sensitive(enablePreviousButton)
+            enableNextButton = (self.__selectedRowNr != -1 and
+                                self.__selectedRowNr < len(model) - 1)
+            env.widgets["nextButton"].set_sensitive(enableNextButton)
+            env.widgets["menubarNextImage"].set_sensitive(enableNextButton)
+            self.__selectionLocked = False
+        self._updateContextMenu()
+
+        # Override sensitiveness set in _updateContextMenu.
+        for widgetName in [
+                "menubarCut",
+                "menubarCopy",
+                "menubarDelete",
+                "menubarDestroy",
+                "menubarProperties",
+                "menubarCreateAlbumChild",
+                "menubarRegisterAndAddImages",
+                "menubarGenerateHtml",
+                ]:
+            env.widgets[widgetName].set_sensitive(False)
+
+    def _showHelper(self):
+        env.enter("SingleObjectView.showHelper()")
+        env.widgets["objectView"].show()
+        env.widgets["objectView"].grab_focus()
+        for widgetName in [
+                "zoom100",
+                "zoomToFit",
+                "zoomIn",
+                "zoomOut",
+                "menubarZoom",
+                ]:
+            env.widgets[widgetName].set_sensitive(True)
+        env.exit("SingleObjectView.showHelper()")
+
+    def _hideHelper(self):
+        env.enter("SingleObjectView.hideHelper()")
+        env.widgets["objectView"].hide()
+        for widgetName in [
+                "previousButton",
+                "nextButton",
+                "menubarPreviousImage",
+                "menubarNextImage",
+                "zoom100",
+                "zoomToFit",
+                "zoomIn",
+                "zoomOut",
+                "menubarZoom",
+                ]:
+            env.widgets[widgetName].set_sensitive(False)
+        env.exit("SingleObjectView.hideHelper()")
+
+    def _connectObjectCollectionHelper(self):
+        env.enter("Connecting SingleObjectView to object collection")
+        env.exit("Connecting SingleObjectView to object collection")
+
+    def _disconnectObjectCollectionHelper(self):
+        env.enter("Disconnecting SingleObjectView from object collection")
+        env.exit("Disconnecting SingleObjectView from object collection")
+
+    def _freezeHelper(self):
+        env.enter("SingleObjectView.freezeHelper()")
+        self._clearAllConnections()
+        self.clear()
+        env.exit("SingleObjectView.freezeHelper()")
+
+    def _thawHelper(self):
+        env.enter("SingleObjectView.thawHelper()")
+        model = self._objectCollection.getModel()
+        # The row_changed event is needed when the location attribute of the image object is changed.
+        self._connect(model, "row_changed", self._rowChanged)
+        # The following events are needed to update the previous and next navigation buttons.
+        self._connect(model, "rows_reordered", self._modelUpdated)
+        self._connect(model, "row_inserted", self._modelUpdated)
+        self._connect(model, "row_deleted", self._modelUpdated)
+        self.importSelection(self._objectCollection.getObjectSelection())
+        env.exit("SingleObjectView.thawHelper()")
+
+    def _modelUpdated(self, *foo):
+        env.debug("SingleObjectView is handling model update")
+        self.importSelection(self._objectCollection.getObjectSelection())
+
+    def _rowChanged(self, model, path, iter, arg, *unused):
+        if path[0] == self.__selectedRowNr:
+            env.debug("selected object in SingleObjectView changed")
+            objectSelection = self._objectCollection.getObjectSelection()
+            obj = objectSelection[path[0]]
+            if not obj.isAlbum():
+                self.loadFile(obj.getLocation(), True)
+
+    def _goto(self, button, direction):
+        objectSelection = self._objectCollection.getObjectSelection()
+        objectSelection.setSelection([self.__selectedRowNr + direction])
+
+    def _viewWidgetFocusInEvent(self, widget, event):
+        ObjectCollectionView._viewWidgetFocusInEvent(self, widget, event)
+        for widgetName in [
+                "menubarClear",
+                "menubarSelectAll",
+                ]:
+            env.widgets[widgetName].set_sensitive(False)
diff --git a/src/gkofoto/gkofoto/sortableobjectcollection.py b/src/gkofoto/gkofoto/sortableobjectcollection.py
new file mode 100644 (file)
index 0000000..18bf3e3
--- /dev/null
@@ -0,0 +1,102 @@
+import gtk
+from environment import env
+from mysortedmodel import *
+from objectcollection import *
+
+def attributeSortFunc(model, iterA, iterB, column):
+    valueA = model.get_value(iterA, column)
+    valueB = model.get_value(iterB, column)
+    try:
+        result = cmp(float(valueA), float(valueB))
+    except (ValueError, TypeError):
+        result = cmp(valueA, valueB)
+    if result == 0:
+        result = cmp(model.get_value(iterA, ObjectCollection.COLUMN_OBJECT_ID),
+                     model.get_value(iterB, ObjectCollection.COLUMN_OBJECT_ID))
+    return result
+
+class SortableObjectCollection(ObjectCollection):
+
+######################################################################
+### Public
+
+    def __init__(self):
+        ObjectCollection.__init__(self)
+        self.__sortOrder = None
+        self.__sortColumnName = None
+        self.__sortedTreeModel = MySortedModel(self.getUnsortedModel())
+        self.setSortOrder(order=gtk.SORT_ASCENDING)
+        self.setSortColumnName(columnName=env.defaultSortColumn)
+
+    def isSortable(self):
+        return True
+
+    def isReorderable(self):
+        return False
+
+    def getModel(self):
+        return self.__sortedTreeModel
+
+    def getUnsortedModel(self):
+        return ObjectCollection.getModel(self)
+
+    def convertToUnsortedRowNr(self, rowNr):
+        return self.__sortedTreeModel.convert_path_to_child_path(rowNr)[0]
+
+    def convertFromUnsortedRowNr(self, unsortedRowNr):
+        return self.__sortedTreeModel. convert_child_path_to_path(unsortedRowNr)[0]
+
+    def getSortOrder(self):
+        return self.__sortOrder
+
+    def getSortColumnName(self):
+        return self.__sortColumnName
+
+    def setSortOrder(self, widget=None, order=None):
+        if widget != None and not widget.get_active():
+            # ignore the callback when the radio menu item is unselected
+            return
+        if self.__sortOrder != order:
+            env.debug("Setting sort order to: " + str(order))
+            self.__sortOrder = order
+            self.__configureSortedModel(self.__sortColumnName, self.__sortOrder)
+            self.__emitSortOrderChanged()
+
+    def setSortColumnName(self, widget=None, columnName=None):
+        if widget != None and not widget.get_active():
+            # ignore the callback when the radio menu item is unselected
+            return
+        if self.__sortColumnName != columnName:
+            if not columnName in self.getObjectMetadataMap():
+                columnName = "id"
+            env.debug("Setting sort column to: " + columnName)
+            self.__sortColumnName = columnName
+            self.__configureSortedModel(self.__sortColumnName, self.__sortOrder)
+            self.__emitSortColumnChanged()
+
+    def registerView(self, view):
+        ObjectCollection.registerView(self, view)
+        self.__emitSortOrderChanged()
+        self.__emitSortColumnChanged()
+
+    def __emitSortOrderChanged(self):
+        for view in self._getRegisteredViews():
+            view.sortOrderChanged(self.__sortOrder)
+
+    def __emitSortColumnChanged(self):
+        for view in self._getRegisteredViews():
+            view.sortColumnChanged(self.__sortColumnName)
+
+    def __configureSortedModel(self, sortColumnName, sortOrder):
+        if (sortOrder != None and sortColumnName != None):
+            metaDataMap = self.getObjectMetadataMap()
+            if not metaDataMap.has_key(sortColumnName):
+                sortColumnName = u"id"
+            sortColumnNr = metaDataMap[sortColumnName][self.COLUMN_NR]
+            model = self.getModel()
+            model.set_sort_column_id(sortColumnNr, self.__sortOrder)
+            # It is important that the attributeSortFunc is not an class member method,
+            # otherwise we are leaking memmory.
+            model.set_sort_func(sortColumnNr,
+                                attributeSortFunc,
+                                sortColumnNr)
diff --git a/src/gkofoto/gkofoto/tableview.py b/src/gkofoto/gkofoto/tableview.py
new file mode 100644 (file)
index 0000000..1710148
--- /dev/null
@@ -0,0 +1,363 @@
+import gtk
+from environment import env
+from sets import Set
+from gkofoto.objectcollectionview import *
+from sets import Set
+from objectcollection import *
+from menuhandler import *
+
+class TableView(ObjectCollectionView):
+
+###############################################################################
+### Public
+
+    def __init__(self):
+        env.debug("Init TableView")
+        ObjectCollectionView.__init__(self, env.widgets["tableView"])
+        selection = self._viewWidget.get_selection()
+        selection.set_mode(gtk.SELECTION_MULTIPLE)
+        self.__selectionLocked = False
+        self._viewWidget.connect("drag_data_received", self._onDragDataReceived)
+        self._viewWidget.connect("drag-data-get", self._onDragDataGet)
+        self.__userChosenColumns = {}
+        self.__createdColumns = {}
+        self.__editedCallbacks = {}
+        self._connectedOids = []
+        # Import the users setting in the configuration file for
+        # which columns that shall be shown.
+        columnLocation = 0
+        for columnName in env.defaultTableViewColumns:
+            self.__userChosenColumns[columnName] = columnLocation
+            columnLocation += 1
+
+    def importSelection(self, objectSelection):
+        if not self.__selectionLocked:
+            env.debug("TableView is importing selection")
+            self.__selectionLocked = True
+            selection = self._viewWidget.get_selection()
+            selection.unselect_all()
+            for rowNr in objectSelection:
+                selection.select_path(rowNr)
+            rowNr = self._objectCollection.getObjectSelection().getLowestSelectedRowNr()
+            if rowNr is None:
+                if len(self._objectCollection.getModel()) > 0:
+                    # Scroll to first object in view
+                    self._viewWidget.scroll_to_cell((0,), None, False, 0, 0)
+            else:
+                # Scroll to first selected object in view
+                self._viewWidget.scroll_to_cell(rowNr, None, False, 0, 0)
+            self.__selectionLocked = False
+        self._updateContextMenu()
+        self._updateMenubarSortMenu()
+
+    def fieldsDisabled(self, fields):
+        env.debug("Table view disable fields: " + str(fields))
+        self.__removeColumnsAndUpdateLocation(fields)
+        for columnName in fields:
+            self.__viewGroup[columnName].set_sensitive(False)
+
+    def fieldsEnabled(self, fields):
+        env.debug("Table view enable fields: " + str(fields))
+        objectMetadataMap = self._objectCollection.getObjectMetadataMap()
+        for columnName in fields:
+            self.__viewGroup[columnName].set_sensitive(True)
+            if columnName not in self.__createdColumns:
+                if columnName in self.__userChosenColumns:
+                    self.__createColumn(columnName, objectMetadataMap, self.__userChosenColumns[columnName])
+
+    def _showHelper(self):
+        env.enter("TableView.showHelper()")
+        env.widgets["tableViewScroll"].show()
+        self._viewWidget.grab_focus()
+        env.exit("TableView.showHelper()")
+
+    def _hideHelper(self):
+        env.enter("TableView.hideHelper()")
+        env.widgets["tableViewScroll"].hide()
+        env.exit("TableView.hideHelper()")
+
+    def _connectObjectCollectionHelper(self):
+        env.enter("Connecting TableView to object collection")
+        # Set model
+        self._viewWidget.set_model(self._objectCollection.getModel())
+        # Create columns
+        objectMetadataMap = self._objectCollection.getObjectMetadataMap()
+        disabledFields = self._objectCollection.getDisabledFields()
+        columnLocationList = self.__userChosenColumns.items()
+        columnLocationList.sort(lambda x, y: cmp(x[1], y[1]))
+        env.debug("Column locations: " + str(columnLocationList))
+        for (columnName, columnLocation) in columnLocationList:
+            if (columnName in objectMetadataMap and
+                columnName not in disabledFields):
+                self.__createColumn(columnName, objectMetadataMap)
+                self.__viewGroup[columnName].activate()
+        self.fieldsDisabled(self._objectCollection.getDisabledFields())
+        env.exit("Connecting TableView to object collection")
+
+    def _initDragAndDrop(self):
+        # Init drag & drop
+        if self._objectCollection.isReorderable() and not self._objectCollection.isSortable():
+            targetEntries = [("STRING", gtk.TARGET_SAME_WIDGET, 0)]
+            self._viewWidget.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
+                                                      targetEntries,
+                                                      gtk.gdk.ACTION_MOVE)
+            self._viewWidget.enable_model_drag_dest(targetEntries, gtk.gdk.ACTION_COPY)
+        else:
+            self._viewWidget.unset_rows_drag_source()
+            self._viewWidget.unset_rows_drag_dest()
+
+    def _disconnectObjectCollectionHelper(self):
+        env.enter("Disconnecting TableView from object collection")
+        self.__removeColumnsAndUpdateLocation()
+        self._viewWidget.set_model(None)
+        env.exit("Disconnecting TableView from object collection")
+
+    def _freezeHelper(self):
+        env.enter("TableView.freezeHelper()")
+        self._clearAllConnections()
+        env.exit("TableView.freezeHelper()")
+
+    def _thawHelper(self):
+        env.enter("TableView.thawHelper()")
+        self._initDragAndDrop()
+        self._connect(self._viewWidget, "focus-in-event", self._treeViewFocusInEvent)
+        self._connect(self._viewWidget, "focus-out-event", self._treeViewFocusOutEvent)
+        self._connect(self._viewWidget.get_selection(), "changed", self._widgetSelectionChanged)
+        env.exit("TableView.thawHelper()")
+
+    def _createContextMenu(self, objectCollection):
+        ObjectCollectionView._createContextMenu(self, objectCollection)
+        self.__viewGroup = self.__createTableColumnsMenuGroup(objectCollection)
+        self._contextMenu.add(self.__viewGroup.createGroupMenuItem())
+
+    def __createTableColumnsMenuGroup(self, objectCollection):
+        menuGroup = MenuGroup("View columns")
+        columnNames = objectCollection.getObjectMetadataMap().keys()
+        columnNames.sort()
+        for columnName in columnNames:
+            menuGroup.addCheckedMenuItem(
+                columnName,
+                self._viewColumnToggled,
+                columnName)
+        return menuGroup
+
+    def _clearContextMenu(self):
+        ObjectCollectionView._clearContextMenu(self)
+        self.__viewGroup = None
+
+###############################################################################
+### Callback functions registered by this class but invoked from other classes.
+
+    def _treeViewFocusInEvent(self, widget, event, data):
+        oc = self._objectCollection
+        for widgetName, function in [
+                ("menubarCut", self._objectCollection.cut),
+                ("menubarCopy", self._objectCollection.copy),
+                ("menubarPaste", self._objectCollection.paste),
+                ("menubarDestroy", oc.destroy),
+                ("menubarClear", lambda x: widget.get_selection().unselect_all()),
+                ("menubarSelectAll", lambda x: widget.get_selection().select_all()),
+                ("menubarCreateAlbumChild", oc.createAlbumChild),
+                ("menubarRegisterAndAddImages", oc.registerAndAddImages),
+                ("menubarGenerateHtml", oc.generateHtml),
+                ("menubarProperties", oc.albumProperties),
+                ]:
+            w = env.widgets[widgetName]
+            oid = w.connect("activate", function)
+            self._connectedOids.append((w, oid))
+
+        self._updateContextMenu()
+
+        for widgetName in [
+                "menubarClear",
+                "menubarSelectAll"
+                ]:
+            env.widgets[widgetName].set_sensitive(True)
+
+    def _treeViewFocusOutEvent(self, widget, event, data):
+        for (widget, oid) in self._connectedOids:
+            widget.disconnect(oid)
+        self._connectedOids = []
+        for widgetName in [
+                "menubarCut",
+                "menubarCopy",
+                "menubarPaste",
+                "menubarDestroy",
+                "menubarClear",
+                "menubarSelectAll",
+                "menubarCreateAlbumChild",
+                "menubarRegisterAndAddImages",
+                "menubarGenerateHtml",
+                "menubarProperties",
+                ]:
+            env.widgets[widgetName].set_sensitive(False)
+
+    def _widgetSelectionChanged(self, selection, data):
+        if not self.__selectionLocked:
+            env.enter("TableView selection changed")
+            self.__selectionLocked = True
+            rowNrs = []
+            selection.selected_foreach(lambda model,
+                                       path,
+                                       iter:
+                                       rowNrs.append(path[0]))
+            self._objectCollection.getObjectSelection().setSelection(rowNrs)
+            self.__selectionLocked = False
+            env.exit("TableView selection changed")
+
+    def _onDragDataGet(self, widget, dragContext, selection, info, timestamp):
+        selectedRows = []
+        # TODO replace with "get_selected_rows()" when it is introduced in Pygtk 2.2 API
+        self._viewWidget.get_selection().selected_foreach(lambda model,
+                                                          path,
+                                                          iter:
+                                                          selectedRows.append(model[path]))
+        if len(selectedRows) == 1:
+            # Ignore drag & drop if zero or more then one row is selected
+            # Drag & drop of multiple rows will probably come in gtk 2.4.
+            # http://mail.gnome.org/archives/gtk-devel-list/2003-December/msg00160.html
+            sourceRowNumber = str(selectedRows[0].path[0])
+            selection.set_text(sourceRowNumber, len(sourceRowNumber))
+        else:
+            env.debug("Ignoring drag&drop when only one row is selected")
+
+
+    def _onDragDataReceived(self, treeview, dragContext, x, y, selection, info, eventtime):
+        targetData = treeview.get_dest_row_at_pos(x, y)
+        if selection.get_text() == None:
+            dragContext.finish(False, False, eventtime)
+        else:
+            model = self._objectCollection.getModel()
+            if targetData == None:
+                targetPath = (len(model) - 1,)
+                dropPosition = gtk.TREE_VIEW_DROP_AFTER
+            else:
+                targetPath, dropPosition = targetData
+            sourceRowNumber = int(selection.get_text())
+            if sourceRowNumber == targetPath[0]:
+                # dropped on itself
+                dragContext.finish(False, False, eventtime)
+            else:
+                # The continer must have a getChildren() and a setChildren()
+                # method as for example the album class has.
+                container = self._objectCollection.getContainer()
+                children = list(container.getChildren())
+                sourceRow = model[sourceRowNumber]
+                targetIter = model.get_iter(targetPath)
+                objectSelection = self._objectCollection.getObjectSelection()
+                if (dropPosition == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE
+                    or dropPosition == gtk.TREE_VIEW_DROP_BEFORE):
+                    container.setChildren(self.__moveListItem(children,
+                                                              sourceRowNumber,
+                                                              targetPath[0]))
+                    model.insert_before(sibling=targetIter, row=sourceRow)
+                    model.remove(sourceRow.iter)
+                    # TODO update the album tree widget?
+                elif (dropPosition == gtk.TREE_VIEW_DROP_INTO_OR_AFTER
+                      or dropPosition == gtk.TREE_VIEW_DROP_AFTER):
+                    container.setChildren(self.__moveListItem(children,
+                                                              sourceRowNumber,
+                                                              targetPath[0] + 1))
+                    model.insert_after(sibling=targetIter, row=sourceRow)
+                    model.remove(sourceRow.iter)
+                    # TODO update the album tree widget?
+                objectSelection.setSelection([targetPath[0]])
+                # I've experienced that the drag-data-delete signal isn't
+                # always emitted when I drag & drop rapidly in the TreeView.
+                # And when it is missing the source row is not removed as is
+                # should. It is probably an bug in gtk+ (or maybe in pygtk).
+                # It only happens sometimes and I have not managed to reproduce
+                # it with a simpler example. Hence we remove the row ourself
+                # and are not relying on the drag-data-delete-signal.
+                # http://bugzilla.gnome.org/show_bug.cgi?id=134997
+                removeSourceRowAutomatically = False
+                dragContext.finish(True, removeSourceRowAutomatically, eventtime)
+
+    def _viewColumnToggled(self, checkMenuItem, columnName):
+        if checkMenuItem.get_active():
+            if columnName not in self.__createdColumns:
+                self.__createColumn(columnName,
+                                    self._objectCollection.getObjectMetadataMap())
+                # The correct columnLocation is stored when the column is removed
+                # there is no need to store the location when it is created
+                # since the column order may be reordered later before it is removed.
+        else:
+            # Since the column has been removed explicitly by the user
+            # we dont store the column's relative location.
+            try:
+                del self.__userChosenColumns[columnName]
+            except KeyError:
+                pass
+            if columnName in self.__createdColumns:
+                self.__removeColumn(columnName)
+
+###############################################################################
+### Private
+
+    def __createColumn(self, columnName, objectMetadataMap, location=-1):
+        (objtype, column, editedCallback, editedCallbackData) = objectMetadataMap[columnName]
+        if objtype == gtk.gdk.Pixbuf:
+            renderer = gtk.CellRendererPixbuf()
+            column = gtk.TreeViewColumn(columnName, renderer, pixbuf=column)
+            env.debug("Created a PixBuf column for " + columnName)
+        elif objtype == gobject.TYPE_STRING or objtype == gobject.TYPE_INT:
+            renderer = gtk.CellRendererText()
+            column = gtk.TreeViewColumn(columnName,
+                                        renderer,
+                                        text=column,
+                                        editable=ObjectCollection.COLUMN_ROW_EDITABLE)
+            column.set_resizable(True)
+            if editedCallback:
+                cid = renderer.connect("edited",
+                                       editedCallback,
+                                       column,
+                                       editedCallbackData)
+                self.__editedCallbacks[columnName] = (cid, renderer)
+                env.debug("Created a Text column with editing callback for " + columnName)
+            else:
+                env.debug("Created a Text column without editing callback for " + columnName)
+        else:
+            print "Warning, unsupported type for column ", columnName
+            return
+        column.set_reorderable(True)
+        self._viewWidget.insert_column(column, location)
+        self.__createdColumns[columnName] = column
+        return column
+
+    def __removeColumn(self, columnName):
+        column = self.__createdColumns[columnName]
+        self._viewWidget.remove_column(column)
+        if columnName in self.__editedCallbacks:
+            (cid, renderer) = self.__editedCallbacks[columnName]
+            renderer.disconnect(cid)
+            del self.__editedCallbacks[columnName]
+        del self.__createdColumns[columnName]
+        column.destroy()
+        env.debug("Removed column " + columnName)
+
+    def __removeColumnsAndUpdateLocation(self, columnNames=None):
+       # Remove columns and store their relative locations for next time
+       # they are re-created.
+       columnLocation = 0
+       for column in self._viewWidget.get_columns():
+           columnName = column.get_title()
+           # TODO Store the column width and reuse it when the column is
+           #      recreated. I don't know how to store the width since
+           #      column.get_width() return correct values for columns
+           #      containing a gtk.CellRendererPixbuf but only 0 for all
+           #      columns containing a gtk.CellRendererText. It is probably
+           #      a bug in gtk och pygtk. I have not yet reported the bug.
+           if columnNames is None or columnName in columnNames:
+               if columnName in self.__createdColumns:
+                   self.__removeColumn(columnName)
+                   self.__userChosenColumns[columnName] = columnLocation
+           columnLocation += 1
+
+    def __moveListItem(self, list, currentIndex, newIndex):
+        if currentIndex == newIndex:
+            return list
+        if currentIndex < newIndex:
+            newIndex -= 1
+        movingChild = list[currentIndex]
+        del list[currentIndex]
+        return list[:newIndex] + [movingChild] + list[newIndex:]
diff --git a/src/gkofoto/gkofoto/taganddescriptiondialog.py b/src/gkofoto/gkofoto/taganddescriptiondialog.py
new file mode 100644 (file)
index 0000000..e5c88d5
--- /dev/null
@@ -0,0 +1,71 @@
+import gtk
+import string
+import re
+from environment import env
+from kofoto.shelf import *
+
+class TagAndDescriptionDialog:
+    def __init__(self, title, tagText=u"", descText=u""):
+        env.assertUnicode(tagText)
+        env.assertUnicode(descText)
+        self._widgets = gtk.glade.XML(env.gladeFile, "tagAndDescriptionDialog")
+        self._dialog = self._widgets.get_widget("tagAndDescriptionDialog")
+        self._dialog.set_title(title)
+        self._tagWidget = self._widgets.get_widget("tag")
+        self._tagWidget.set_text(tagText)
+        self._descWidget = self._widgets.get_widget("description")
+        self._descWidget.set_text(descText)
+        self._descWidget.connect("changed", self._descriptionChanged, self._tagWidget)
+        okbutton = self._widgets.get_widget("okbutton")
+        self._tagWidget.connect("changed", self._tagChanged, okbutton)
+        self.__descText = descText
+        okbutton.set_sensitive(self._isTagOkay(tagText))
+
+    def run(self, ok=None, data=None):
+        result = self._dialog.run()
+        tag = self._tagWidget.get_text().decode("utf-8")
+        desc = self._descWidget.get_text().decode("utf-8")
+        self._dialog.destroy()
+        if result == gtk.RESPONSE_OK:
+            if ok == None:
+                return None
+            else:
+                if data:
+                    return ok(tag, desc, data)
+                else:
+                    return ok(tag, desc)
+        else:
+            return None
+
+    def __generateTagName(self, descText):
+        env.assertUnicode(descText)
+        return re.sub(r"(?Lu)\W", "", descText).lower()
+
+    def __generateTagNameDeprecated1(self, descText):
+        # An algoritm for generating tag names used in previous gkofoto
+        # versions (2004-04-26 -- 2004-05-15). This algoritm
+        # must always remove all swedish characters, regardles of LOCAL
+        # or UNICODE setting, to be backward compatible with the old version.
+        env.assertUnicode(descText)
+        return re.sub("\W", "", descText)
+
+    def __generateTagNameDeprecated2(self, descText):
+        # An algoritm for generating tag names used in previous gkofoto
+        # versions (< 2004-04-26)
+        env.assertUnicode(descText)
+        return string.translate(descText.encode(env.codeset),
+                                string.maketrans("", ""),
+                                string.whitespace)
+
+    def _descriptionChanged(self, description, tag):
+        newDescText = description.get_text().decode("utf-8")
+        currentTagText = self._tagWidget.get_text()
+        if (currentTagText == self.__generateTagName(self.__descText) or
+            currentTagText == self.__generateTagNameDeprecated1(self.__descText) or
+            currentTagText == self.__generateTagNameDeprecated2(self.__descText)):
+            tag.set_text(self.__generateTagName(newDescText))
+        self.__descText = newDescText
+
+    def _tagChanged(self, tag, button):
+        tagString = tag.get_text().decode("utf-8")
+        button.set_sensitive(self._isTagOkay(tagString))
diff --git a/src/gkofoto/gkofoto/thumbnailview.py b/src/gkofoto/gkofoto/thumbnailview.py
new file mode 100644 (file)
index 0000000..16f7105
--- /dev/null
@@ -0,0 +1,152 @@
+import gtk
+from gkofoto.objectcollectionview import *
+from gkofoto.objectcollection import *
+from environment import env
+
+class ThumbnailView(ObjectCollectionView):
+
+###############################################################################
+### Public
+
+    def __init__(self):
+        env.debug("Init ThumbnailView")
+##        ObjectCollectionView.__init__(self,
+##                                      env.widgets["thumbnailList"])
+        ObjectCollectionView.__init__(self,
+                                      env.widgets["thumbnailView"])
+        self.__currentMaxWidth = env.thumbnailSize[0]
+        self.__selectionLocked = False
+        return
+        self._viewWidget.connect("select_icon", self._widgetIconSelected)
+        self._viewWidget.connect("unselect_icon", self._widgetIconUnselected)
+
+    def importSelection(self, objectSelection):
+        if not self.__selectionLocked:
+            env.debug("ThumbnailView is importing selection.")
+            self.__selectionLocked = True
+            self._viewWidget.unselect_all()
+            for rowNr in objectSelection:
+                self._viewWidget.select_icon(rowNr)
+            self.__selectionLocked = False
+        self._updateContextMenu()
+
+    def _showHelper(self):
+        env.enter("ThumbnailView.showHelper()")
+        env.widgets["thumbnailView"].show()
+        self._viewWidget.grab_focus()
+        self.__scrollToFirstSelectedObject()
+        env.exit("ThumbnailView.showHelper()")
+
+    def _hideHelper(self):
+        env.enter("ThumbnailView.hideHelper()")
+        env.widgets["thumbnailView"].hide()
+        env.exit("ThumbnailView.hideHelper()")
+
+    def _connectObjectCollectionHelper(self):
+        env.enter("Connecting ThumbnailView to object collection")
+        # The model is loaded in thawHelper instead.
+        env.exit("Connecting ThumbnailView to object collection")
+
+    def _disconnectObjectCollectionHelper(self):
+        env.enter("Disconnecting ThumbnailView from object collection")
+        # The model is unloaded in freezeHelper instead.
+        env.exit("Disconnecting ThumbnailView from object collection")
+
+    def _freezeHelper(self):
+        env.enter("ThumbnailView.freezeHelper()")
+        self._clearAllConnections()
+        self._viewWidget.clear()
+        env.exit("ThumbnailView.freezeHelper()")
+
+    def _thawHelper(self):
+        env.enter("ThumbnailView.thawHelper()")
+        model = self._objectCollection.getModel()
+        for row in model:
+            self.__loadRow(row)
+        self._connect(model, "row_inserted",   self._rowInserted)
+        self._connect(model, "row_deleted",    self._rowDeleted)
+        self._connect(model, "rows_reordered", self._rowsReordered)
+        self._connect(model, "row_changed",    self._rowChanged)
+        env.exit("ThumbnailView.thawHelper()")
+
+###############################################################################
+### Callback functions registered by this class but invoked from other classes.
+
+    def _rowChanged(self, model, path, iterator):
+        env.debug("ThumbnailView row changed.")
+        self.__selectionLocked = True
+        self._viewWidget.remove(path[0])
+        # For some reason that I don't understand model[path].path != path
+        # Hence we pass by path as the location where the icon shall be
+        # inserted.
+        self.__loadRow(model[path[0]], path[0])
+        if path[0] in self._objectCollection.getObjectSelection():
+            self._viewWidget.select_icon(path[0])
+        self.__selectionLocked = False
+
+    def _rowInserted(self, model, path, iterator):
+        env.debug("ThumbnailView row inserted.")
+        self.__loadRow(model[path])
+
+    def _rowsReordered(self, model, b, c, d):
+        env.debug("ThumbnailView rows reordered.")
+        # TODO I Don't know how to parse which rows that has
+        #      been reordered. Hence I must reload all rows.
+        self._viewWidget.clear()
+        for row in self._objectCollection.getModel():
+            self.__loadRow(row)
+        self.importSelection(self._objectCollection.getObjectSelection())
+
+    def _rowDeleted(self, model, path):
+        env.debug("ThumbnailView row deleted.")
+        self._viewWidget.remove(path[0])
+
+    def _widgetIconSelected(self, widget, index, event):
+        if not self.__selectionLocked:
+            env.enter("ThumbnailView selection changed")
+            self.__selectionLocked = True
+            self._objectCollection.getObjectSelection().addSelection(index)
+            self.__selectionLocked = False
+
+    def _widgetIconUnselected(self, widget, index, event):
+        if not self.__selectionLocked:
+            env.enter("ThumbnailView selection changed")
+            self.__selectionLocked = True
+            self._objectCollection.getObjectSelection().removeSelection(index)
+            self.__selectionLocked = False
+
+###############################################################################
+### Private
+
+    def __loadRow(self, row, location=None):
+        if location is None:
+            location = row.path[0]
+        if row[ObjectCollection.COLUMN_IS_ALBUM]:
+            text = row[ObjectCollection.COLUMN_ALBUM_TAG]
+        else:
+            # TODO Let configuration decide what to show...
+            text = row[ObjectCollection.COLUMN_OBJECT_ID]
+        pixbuf = row[ObjectCollection.COLUMN_THUMBNAIL]
+        if pixbuf == None:
+            # It is possible that we get the row inserted event before
+            # the thumbnail is loaded. The temporary icon will be removed
+            # when we receive the row changed event.
+            pixbuf = env.loadingPixbuf
+        self._viewWidget.insert_pixbuf(location, pixbuf, "", str(text))
+        self.__currentMaxWidth = max(self.__currentMaxWidth, pixbuf.get_width())
+        self._viewWidget.set_icon_width(self.__currentMaxWidth)
+
+    def __scrollToFirstSelectedObject(self):
+        numberOfIcons = self._viewWidget.get_num_icons()
+        if numberOfIcons > 1:
+            # First check that the widget contains icons because I don't know
+            # how icon_is_visible() is handled if the view is empty.
+            if (self._viewWidget.icon_is_visible(0) == gtk.VISIBILITY_FULL
+                and self._viewWidget.icon_is_visible(numberOfIcons - 1) == gtk.VISIBILITY_FULL):
+                # All icons already visible. No need to scroll widget.
+                pass
+            else:
+                # Scroll widget to first selected icon
+                rowNr = self._objectCollection.getObjectSelection().getLowestSelectedRowNr()
+                if rowNr is not None:
+                    self._viewWidget.moveto(rowNr, 0.4)
diff --git a/src/gkofoto/handleimagesdialog.py b/src/gkofoto/handleimagesdialog.py
deleted file mode 100644 (file)
index a2a29e5..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-import gtk
-import gobject
-import os
-from environment import env
-from kofoto.shelf import \
-     ImageDoesNotExistError, ImageExistsError, \
-     MultipleImagesAtOneLocationError, NotAnImageError, \
-     makeValidTag
-from kofoto.clientutils import walk_files
-
-class HandleImagesDialog(gtk.FileSelection):
-    def __init__(self):
-        gtk.FileSelection.__init__(self, title="Register images")
-        self.set_select_multiple(True)
-        self.ok_button.connect("clicked", self._ok)
-
-    def _ok(self, widget):
-        widgets = gtk.glade.XML(env.gladeFile, "handleImagesProgressDialog")
-        handleImagesProgressDialog = widgets.get_widget(
-            "handleImagesProgressDialog")
-        knownUnchangedImagesCount = widgets.get_widget(
-            "knownUnchangedImagesCount")
-        knownMovedImagesCount = widgets.get_widget(
-            "knownMovedImagesCount")
-        unknownModifiedImagesCount = widgets.get_widget(
-            "unknownModifiedImagesCount")
-        unknownFilesCount = widgets.get_widget(
-            "unknownFilesCount")
-        investigatedFilesCount = widgets.get_widget(
-            "investigatedFilesCount")
-        okButton = widgets.get_widget("okButton")
-        okButton.set_sensitive(False)
-
-        handleImagesProgressDialog.show()
-
-        knownUnchangedImages = 0
-        knownMovedImages = 0
-        unknownModifiedImages = 0
-        unknownFiles = 0
-        investigatedFiles = 0
-        modifiedImages = []
-        movedImages = []
-        for filepath in walk_files(self.get_selections()):
-            try:
-                filepath = filepath.decode("utf-8")
-            except UnicodeDecodeError:
-                filepath = filepath.decode("latin1")
-            try:
-                image = env.shelf.getImage(filepath)
-                if image.getLocation() == os.path.realpath(filepath):
-                    # Registered.
-                    knownUnchangedImages += 1
-                    knownUnchangedImagesCount.set_text(
-                        str(knownUnchangedImages))
-                else:
-                    # Moved.
-                    knownMovedImages += 1
-                    knownMovedImagesCount.set_text(str(knownMovedImages))
-                    movedImages.append(filepath)
-            except ImageDoesNotExistError:
-                try:
-                    image = env.shelf.getImage(
-                        filepath, identifyByLocation=True)
-                    # Modified.
-                    unknownModifiedImages += 1
-                    unknownModifiedImagesCount.set_text(
-                        str(unknownModifiedImages))
-                    modifiedImages.append(filepath)
-                except MultipleImagesAtOneLocationError:
-                    # Multiple images at one location.
-                    # TODO: Handle this error.
-                    pass
-                except ImageDoesNotExistError:
-                    # Unregistered.
-                    unknownFiles += 1
-                    unknownFilesCount.set_text(str(unknownFiles))
-            investigatedFiles += 1
-            investigatedFilesCount.set_text(str(investigatedFiles))
-            while gtk.events_pending():
-                gtk.main_iteration()
-
-        okButton.set_sensitive(True)
-        handleImagesProgressDialog.run()
-        handleImagesProgressDialog.destroy()
-
-        if modifiedImages or movedImages:
-            if modifiedImages:
-                self._dialogHelper(
-                    "Update modified images",
-                    "The above image files have been modified. Press OK to"
-                    " make Kofoto recognize the new contents.",
-                    modifiedImages,
-                    self._updateModifiedImages)
-            if movedImages:
-                self._dialogHelper(
-                    "Update moved or renamed images",
-                    "The above image files have been moved or renamed. Press OK to"
-                    " make Kofoto recognize the new locations.",
-                    movedImages,
-                    self._updateMovedImages)
-        else:
-            dialog = gtk.MessageDialog(
-                type=gtk.MESSAGE_INFO,
-                buttons=gtk.BUTTONS_OK,
-                message_format="No modified, renamed or moved images found.")
-            dialog.run()
-            dialog.destroy()
-
-    def _dialogHelper(self, title, text, filepaths, handlerFunction):
-        widgets = gtk.glade.XML(env.gladeFile, "updateImagesDialog")
-        dialog = widgets.get_widget("updateImagesDialog")
-        dialog.set_title(title)
-        filenameList = widgets.get_widget("filenameList")
-        renderer = gtk.CellRendererText()
-        column = gtk.TreeViewColumn("Image filename", renderer, text=0)
-        filenameList.append_column(column)
-        dialogText = widgets.get_widget("dialogText")
-        dialogText.set_text(text)
-        model = gtk.ListStore(gobject.TYPE_STRING)
-        for filepath in filepaths:
-            model.append([filepath])
-        filenameList.set_model(model)
-        if dialog.run() == gtk.RESPONSE_OK:
-            handlerFunction(filepaths)
-        dialog.destroy()
-
-    def _error(self, errorText):
-        dialog = gtk.MessageDialog(
-            type=gtk.MESSAGE_ERROR,
-            buttons=gtk.BUTTONS_OK,
-            message_format=errorText)
-        dialog.run()
-        dialog.destroy()
-
-    def _updateModifiedImages(self, filepaths):
-        for filepath in filepaths:
-            try:
-                image = env.shelf.getImage(
-                    filepath, identifyByLocation=True)
-                image.contentChanged()
-            except ImageDoesNotExistError:
-                self._error("Image does not exist: %s" % filepath)
-            except MultipleImagesAtOneLocationError:
-                # TODO: Handle this.
-                pass
-            except IOError, x:
-                self._error("Error while reading %s: %s" % (
-                    filepath, x))
-
-    def _updateMovedImages(self, filepaths):
-        for filepath in filepaths:
-            try:
-                image = env.shelf.getImage(filepath)
-                image.locationChanged(filepath)
-            except ImageDoesNotExistError:
-                self._error("Image does not exist: %s" % filepath)
-            except MultipleImagesAtOneLocationError:
-                # TODO: Handle this.
-                pass
-            except IOError, x:
-                self._error("Error while reading %s: %s" % (
-                    filepath, x))
diff --git a/src/gkofoto/imageview.py b/src/gkofoto/imageview.py
deleted file mode 100644 (file)
index 70ef8b3..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-import gtk
-import gtk.gdk
-import math
-import gobject
-import gc
-from environment import env
-
-class ImageView(gtk.ScrolledWindow):
-    # TODO: Read from configuration file?
-    _INTERPOLATION_TYPE = gtk.gdk.INTERP_BILINEAR
-    # gtk.gdk.INTERP_HYPER is slower but gives better quality.
-    _MAX_IMAGE_SIZE = 2000
-    _MIN_IMAGE_SIZE = 1
-    _MIN_ZOOM = -100
-    _MAX_ZOOM = 1
-    _ZOOMFACTOR = 1.2
-
-    def __init__(self):
-        self._image = gtk.Image()
-        gtk.ScrolledWindow.__init__(self)
-        self.__loadedFileName = None
-        self.__pixBuf = None
-        self.__currentZoom = None
-        self.__wantedZoom = None
-        self.__fitToWindowMode = True
-        self.__previousWidgetWidth = 0
-        self.__previousWidgetHeight = 0
-        eventBox = gtk.EventBox()
-        eventBox.add(self._image)
-        self.add_with_viewport(eventBox)
-        self.add_events(gtk.gdk.ALL_EVENTS_MASK)
-        self.connect_after("size_allocate", self.resizeEventHandler)
-        self.connect("scroll_event", self.scrollEventHandler)
-
-    def loadFile(self, fileName, reload=True):
-        fileName = fileName.encode(env.codeset)
-        if (not reload) and self.__loadedFileName == fileName:
-            return
-        # TODO: Loading file should be asyncronous to avoid freezing the gtk-main loop
-        try:
-            self.clear()
-            env.debug("ImageView is loading image from file: " + fileName)
-            self.__pixBuf = gtk.gdk.pixbuf_new_from_file(fileName)
-            self.__loadedFileName = fileName
-        except gobject.GError, e:
-            dialog = gtk.MessageDialog(
-                type=gtk.MESSAGE_ERROR,
-                buttons=gtk.BUTTONS_OK,
-                message_format="Could not load image: %s" % fileName)
-            dialog.run()
-            dialog.destroy()
-            self.__pixBuf = env.unknownImageIconPixbuf
-            self.__loadedFileName = None
-        self._newImageLoaded = True
-        self._image.show()
-        self.fitToWindow()
-
-    def clear(self):
-        self._image.hide()
-        self._image.set_from_file(None)
-        self.__pixBuf = None
-        self.__loadedFileName = None
-        gc.collect()
-        env.debug("ImageView is cleared.")
-
-    def renderImage(self):
-        # TODO: Scaling should be asyncronous to avoid freezing the gtk-main loop
-        if self.__pixBuf == None:
-            # No image loaded
-            self._image.hide()
-            return
-        if self.__currentZoom == self.__wantedZoom and not self._newImageLoaded:
-            return
-        if self.__wantedZoom == 0:
-            pixBufResized = self.__pixBuf
-        else:
-            zoomMultiplicator = pow(self._ZOOMFACTOR, self.__wantedZoom)
-            wantedWidth = int(self.__pixBuf.get_width() * zoomMultiplicator)
-            wantedHeight = int(self.__pixBuf.get_height() * zoomMultiplicator)
-            if min(wantedWidth, wantedHeight) < self._MIN_IMAGE_SIZE:
-                # Too small image size
-                return
-            if max(wantedWidth, wantedHeight) > self._MAX_IMAGE_SIZE:
-                # Too large image size
-                return
-            pixBufResized = self.__pixBuf.scale_simple(wantedWidth,
-                                                      wantedHeight,
-                                                      self._INTERPOLATION_TYPE)
-        pixMap, mask = pixBufResized.render_pixmap_and_mask()
-        self._image.set_from_pixmap(pixMap, mask)
-        self._newImageLoaded = False
-        self.__currentZoom = self.__wantedZoom
-        gc.collect()
-
-    def resizeEventHandler(self, widget, gdkEvent):
-        if self.__fitToWindowMode:
-            x, y, width, height = self.get_allocation()
-            if height != self.__previousWidgetHeight or width != self.__previousWidgetWidth:
-                self.fitToWindow()
-        return False
-
-    def fitToWindow(self, *foo):
-        self.__fitToWindowMode = True
-        self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
-        y, x, widgetWidth, widgetHeight = self.get_allocation()
-        if self.__pixBuf != None:
-            self.__previousWidgetWidth = widgetWidth
-            self.__previousWidgetHeight = widgetHeight
-            a = min(float(widgetWidth) / self.__pixBuf.get_width(),
-                    float(widgetHeight) / self.__pixBuf.get_height())
-            self.__wantedZoom = self._log(self._ZOOMFACTOR, a)
-            self.__wantedZoom = min(self.__wantedZoom, 0)
-            self.__wantedZoom = max(self.__wantedZoom, self._MIN_ZOOM)
-            self.renderImage()
-
-    def _log(self, base, value):
-        return math.log(value) / math.log(base)
-
-    def zoomIn(self, *foo):
-        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-        self.__fitToWindowMode = False
-        if self.__wantedZoom <= self._MAX_ZOOM:
-            self.__wantedZoom = math.floor(self.__wantedZoom + 1)
-            self.renderImage()
-
-    def zoomOut(self, *foo):
-        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-        self.__fitToWindowMode = False
-        if self.__wantedZoom >= self._MIN_ZOOM:
-            self.__wantedZoom = math.ceil(self.__wantedZoom - 1)
-            self.renderImage()
-
-    def zoom100(self, *foo):
-        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-        self.__fitToWindowMode = False
-        self.__wantedZoom = 0
-        self.renderImage()
-
-    def scrollEventHandler(self, widget, gdkEvent):
-        if gdkEvent.type == gtk.gdk.SCROLL:
-            if gdkEvent.direction == gtk.gdk.SCROLL_UP:
-                self.zoomOut()
-            elif gdkEvent.direction == gtk.gdk.SCROLL_DOWN:
-                self.zoomIn()
-            return True
-        else:
-            return False
diff --git a/src/gkofoto/main.py b/src/gkofoto/main.py
deleted file mode 100644 (file)
index dbd492a..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-import sys
-from kofoto.clientenvironment import DEFAULT_CONFIGFILE_LOCATION
-from gkofoto.environment import env
-from gkofoto.controller import Controller
-from optparse import OptionParser
-
-def main(bindir, argv):
-    parser = OptionParser(version=env.version)
-    parser.add_option(
-        "--configfile",
-        type="string",
-        dest="configfile",
-        help="use configuration file CONFIGFILE instead of the default (%s)" % (
-            DEFAULT_CONFIGFILE_LOCATION),
-        default=None)
-    parser.add_option(
-        "--debug",
-        action="store_true",
-        help="print debug messages to stdout",
-        default=False)
-    parser.add_option(
-        "--shelf",
-        type="string",
-        dest="shelf",
-        help="use shelf SHELF instead of the default (specified in the configuration file)",
-        default=None)
-    options, args = parser.parse_args(argv[1:])
-
-    if len(args) != 0:
-        parser.error("incorrect number of arguments")
-
-    setupOk = env.setup(
-        bindir, options.debug, options.configfile, options.shelf)
-    env.controller = Controller()
-    env.controller.start(setupOk)
diff --git a/src/gkofoto/mainwindow.py b/src/gkofoto/mainwindow.py
deleted file mode 100644 (file)
index ba90717..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-import gtk
-import gtk.gdk
-import os
-
-from gkofoto.categories import *
-from gkofoto.albums import *
-from environment import env
-from gkofoto.tableview import *
-from gkofoto.thumbnailview import *
-from gkofoto.singleobjectview import *
-from gkofoto.objectcollectionfactory import *
-from gkofoto.objectcollection import *
-from gkofoto.registerimagesdialog import RegisterImagesDialog
-from gkofoto.handleimagesdialog import HandleImagesDialog
-from gkofoto.generatehtmldialog import GenerateHTMLDialog
-
-class MainWindow(gtk.Window):
-    def __init__(self):
-        env.mainwindow = self
-        self._toggleLock = False
-        self.__currentObjectCollection = None
-        self._currentView = None
-        self.__sourceEntry = env.widgets["sourceEntry"]
-        env.widgets["expandViewToggleButton"].connect("toggled", self._toggleExpandView)
-        env.widgets["expandViewToggleButton"].get_child().add(self.getIconImage("fullscreen-24.png"))
-#        env.widgets["thumbnailsViewToggleButton"].connect("clicked", self._toggleThumbnailsView)
-        env.widgets["thumbnailsViewToggleButton"].set_sensitive(False)
-        env.widgets["thumbnailsViewToggleButton"].get_child().add(self.getIconImage("thumbnailsview.png"))
-        env.widgets["objectViewToggleButton"].connect("clicked", self._toggleObjectView)
-        env.widgets["objectViewToggleButton"].get_child().add(self.getIconImage("objectview.png"))
-        env.widgets["menubarObjectView"].connect("activate", self._toggleObjectView)
-        env.widgets["tableViewToggleButton"].connect("clicked", self._toggleTableView)
-        env.widgets["tableViewToggleButton"].get_child().add(self.getIconImage("tableview.png"))
-        env.widgets["menubarTableView"].connect("activate", self._toggleTableView)
-        env.widgets["previousButton"].set_sensitive(False)
-        env.widgets["nextButton"].set_sensitive(False)
-        env.widgets["zoom100"].set_sensitive(False)
-        env.widgets["zoomToFit"].set_sensitive(False)
-        env.widgets["zoomIn"].set_sensitive(False)
-        env.widgets["zoomOut"].set_sensitive(False)
-
-        env.widgets["menubarSave"].connect("activate", env.controller.save)
-        env.widgets["menubarSave"].set_sensitive(False)
-        env.widgets["menubarRevert"].connect("activate", env.controller.revert)
-        env.widgets["menubarRevert"].set_sensitive(False)
-        env.widgets["menubarQuit"].connect("activate", env.controller.quit)
-
-        env.widgets["menubarThumbnailsView"].set_sensitive(False)
-
-        env.widgets["menubarNextImage"].set_sensitive(False)
-        env.widgets["menubarPreviousImage"].set_sensitive(False)
-        env.widgets["menubarZoom"].set_sensitive(False)
-
-        env.widgets["menubarRegisterImages"].connect("activate", self.registerImages, None)
-        env.widgets["menubarHandleModifiedOrRenamedImages"].connect(
-            "activate", self.handleModifiedOrRenamedImages, None)
-
-        env.widgets["menubarRotateLeft"].get_children()[1].set_from_pixbuf(
-            gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, "rotateleft.png")))
-        env.widgets["menubarRotateRight"].get_children()[1].set_from_pixbuf(
-            gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, "rotateright.png")))
-        env.widgets["menubarAbout"].get_children()[1].set_from_pixbuf(
-            gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, "about-icon.png")))
-
-        env.widgets["menubarAbout"].connect("activate", self.showAboutBox)
-
-        self.__sourceEntry.connect("activate", self._sourceEntryActivated)
-
-        env.shelf.registerModificationCallback(self._shelfModificationChangedCallback)
-
-        self.__factory = ObjectCollectionFactory()
-        self.__categories = Categories(self)
-        self.__albums = Albums(self)
-        self.__thumbnailView = ThumbnailView()
-        self.__tableView = TableView()
-        self.__singleObjectView = SingleObjectView()
-        self.__showTableView()
-
-    def _sourceEntryActivated(self, widget):
-        self.__setObjectCollection(self.__factory.getObjectCollection(
-            widget.get_text().decode("utf-8")))
-        self.__sourceEntry.grab_remove()
-
-    def setQuery(self, query):
-        self.__query = query
-        self.__sourceEntry.set_text(query)
-
-    def loadQuery(self, query):
-        self.setQuery(query)
-        self.__setObjectCollection(self.__factory.getObjectCollection(query))
-
-    def reload(self):
-        self.__albums.loadAlbumTree()
-        self.__categories.loadCategoryTree()
-        self.loadQuery(self.__query)
-
-    def reloadAlbumTree(self):
-        self.__albums.loadAlbumTree()
-
-    def registerImages(self, widget, data):
-        dialog = RegisterImagesDialog()
-        if dialog.run() == gtk.RESPONSE_OK:
-            self.reload() # TODO: don't reload everything.
-        dialog.destroy()
-
-    def handleModifiedOrRenamedImages(self, widget, data):
-        dialog = HandleImagesDialog()
-        dialog.run()
-        dialog.destroy()
-
-    def showAboutBox(self, *unused):
-        widgets = gtk.glade.XML(env.gladeFile, "aboutDialog")
-        aboutDialog = widgets.get_widget("aboutDialog")
-        nameAndVersionLabel = widgets.get_widget("nameAndVersionLabel")
-        nameAndVersionLabel.set_text("Kofoto %s" % env.version)
-        aboutDialog.run()
-        aboutDialog.destroy()
-
-    def generateHtml(self, album):
-        dialog = GenerateHTMLDialog(album)
-        dialog.run()
-
-    def getIconImage(self, name):
-        pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(env.iconDir, name))
-        image = gtk.Image()
-        image.set_from_pixbuf(pixbuf)
-        image.show()
-        return image
-
-    def _viewChanged(self):
-        for hiddenView in self._hiddenViews:
-            hiddenView.hide()
-        self._currentView.show(self.__currentObjectCollection)
-
-    def __showTableView(self):
-        self._currentView = self.__tableView
-        self._hiddenViews = [self.__thumbnailView, self.__singleObjectView]
-        self._viewChanged()
-
-    def __showThumbnailView(self):
-        self._currentView = self.__thumbnailView
-        self._hiddenViews = [self.__tableView, self.__singleObjectView]
-        self._viewChanged()
-
-    def __showSingleObjectView(self):
-        self._currentView = self.__singleObjectView
-        self._hiddenViews = [self.__tableView, self.__thumbnailView]
-        self._viewChanged()
-
-    def _toggleExpandView(self, button):
-        if button.get_active():
-            env.widgets["sourceNotebook"].hide()
-        else:
-            env.widgets["sourceNotebook"].show()
-
-    def _toggleThumbnailsView(self, button):
-        if not self._toggleLock:
-            self._toggleLock = True
-            button.set_active(True)
-            env.widgets["thumbnailsViewToggleButton"].set_active(True)
-            env.widgets["objectViewToggleButton"].set_active(False)
-            env.widgets["tableViewToggleButton"].set_active(False)
-            env.widgets["menubarThumbnailsView"].set_active(True)
-            env.widgets["menubarObjectView"].set_active(False)
-            env.widgets["menubarTableView"].set_active(False)
-            self.__showThumbnailView()
-            self._toggleLock = False
-
-    def _toggleObjectView(self, button):
-        if not self._toggleLock:
-            self._toggleLock = True
-            button.set_active(True)
-            env.widgets["thumbnailsViewToggleButton"].set_active(False)
-            env.widgets["objectViewToggleButton"].set_active(True)
-            env.widgets["tableViewToggleButton"].set_active(False)
-            env.widgets["menubarThumbnailsView"].set_active(False)
-            env.widgets["menubarObjectView"].set_active(True)
-            env.widgets["menubarTableView"].set_active(False)
-            self.__showSingleObjectView()
-            self._toggleLock = False
-
-    def _toggleTableView(self, button):
-        if not self._toggleLock:
-            self._toggleLock = True
-            button.set_active(True)
-            env.widgets["thumbnailsViewToggleButton"].set_active(False)
-            env.widgets["objectViewToggleButton"].set_active(False)
-            env.widgets["tableViewToggleButton"].set_active(True)
-            env.widgets["menubarThumbnailsView"].set_active(False)
-            env.widgets["menubarObjectView"].set_active(False)
-            env.widgets["menubarTableView"].set_active(True)
-            self.__showTableView()
-            self._toggleLock = False
-
-    def _shelfModificationChangedCallback(self, modified):
-        env.widgets["menubarRevert"].set_sensitive(modified)
-        env.widgets["menubarSave"].set_sensitive(modified)
-
-    def __setObjectCollection(self, objectCollection):
-        if self.__currentObjectCollection != objectCollection:
-            env.debug("MainWindow is propagating a new ObjectCollection")
-            self.__currentObjectCollection = objectCollection
-            self.__categories.setCollection(objectCollection)
-            if self._currentView is not None:
-                self._currentView.setObjectCollection(objectCollection)
diff --git a/src/gkofoto/menuhandler.py b/src/gkofoto/menuhandler.py
deleted file mode 100644 (file)
index b471d3a..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-import gtk
-
-class MenuGroup:
-    def __init__(self, label=""):
-        self.__label = label
-        self.__childItems = []
-        self.__childItemsMap = {}
-        self.__radioGroup = None
-
-    def addMenuItem(self, label, callback, callbackData=None):
-        item = gtk.MenuItem(label)
-        self.__addItem(item, label, callback, callbackData)
-
-    def addStockImageMenuItem(self, label, stockId, callback,
-                              callbackData=None):
-        item = gtk.ImageMenuItem(label)
-        image = gtk.Image()
-        image.set_from_stock(stockId, gtk.ICON_SIZE_MENU)
-        item.set_image(image)
-        self.__addItem(item, label, callback, callbackData)
-
-    def addImageMenuItem(self, label, imageFilename, callback,
-                         callbackData=None):
-        item = gtk.ImageMenuItem(label)
-        image = gtk.Image()
-        image.set_from_file(imageFilename)
-        item.set_image(image)
-        self.__addItem(item, label, callback, callbackData)
-
-    def addCheckedMenuItem(self, label, callback, callbackData=None):
-        item = gtk.CheckMenuItem(label)
-        self.__addItem(item, label, callback, callbackData)
-
-    def addRadioMenuItem(self, label, callback, callbackData=None):
-        item = gtk.RadioMenuItem(self.__radioGroup, label)
-        self.__addItem(item, label, callback, callbackData)
-        self.__radioGroup = item
-
-    def addSeparator(self):
-        separator = gtk.SeparatorMenuItem()
-        self.__childItems.append(separator)
-        separator.show()
-        self.__radioGroup = None
-
-    def __getitem__(self, key):
-        return self.__childItemsMap[key]
-
-    def createGroupMenu(self):
-        menu = gtk.Menu()
-        for item in self:
-            menu.append(item)
-        menu.show()
-        return menu
-
-    def createGroupMenuItem(self):
-        menuItem = gtk.MenuItem(self.__label)
-        subMenu = self.createGroupMenu()
-        if len(self) > 0:
-            menuItem.set_submenu(subMenu)
-        else:
-            menuItem.set_sensitive(False)
-        menuItem.show()
-        return menuItem
-
-    def __len__(self):
-        return len(self.__childItems)
-
-    def __iter__(self):
-        for child in self.__childItems:
-            yield child
-
-    def enable(self):
-        for child in self.__childItems:
-            child.set_sensitive(True)
-
-    def disable(self):
-        for child in self.__childItems:
-            child.set_sensitive(False)
-
-    def __addItem(self, item, label, callback, callbackData=None):
-        if callbackData == None:
-            key = label
-        else:
-            key = callbackData
-        self.__childItemsMap[key] = item
-        self.__childItems.append(item)
-        item.connect("activate", callback, callbackData)
-        item.show()
diff --git a/src/gkofoto/mysortedmodel.py b/src/gkofoto/mysortedmodel.py
deleted file mode 100644 (file)
index 8a6585d..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import gtk
-
-class MySortedModel(gtk.TreeModelSort):
-
-    def __init__(self, model):
-        gtk.TreeModelSort.__init__(self, model)
-        self._model = model
-
-    def __getitem__(self, path):
-        child_path = self.convert_path_to_child_path(path)
-        if child_path:
-            return self._model[child_path]
-        else:
-            raise IndexError
-
-    def __delitem__(self, path):
-        child_path = self.convert_path_to_child_path(path)
-        if child_path:
-            del self._model[child_path]
-        else:
-            raise IndexError
-
-    def set_value(self, iter, column, value):
-        childIter = self._model.get_iter_first()
-        self.convert_iter_to_child_iter(childIter, iter)
-        self._model.set_value(childIter, column, value)
-
-    # Workaround until http://bugzilla.gnome.org/show_bug.cgi?id=121633 is solved.
-    def get_iter_first(self):
-        if len(self) > 0:
-            return gtk.TreeModelSort.get_iter_first(self)
-        else:
-            return None
-
-    # Workaround until http://bugzilla.gnome.org/show_bug.cgi?id=121633 is solved.
-    def __iter__(self):
-        if len(self._model) > 0:
-            return gtk.TreeModelSort.__iter__(self)
-        else:
-            return self._model.__iter__()
diff --git a/src/gkofoto/objectcollection.py b/src/gkofoto/objectcollection.py
deleted file mode 100644 (file)
index 35f6767..0000000
+++ /dev/null
@@ -1,437 +0,0 @@
-import os
-import gtk
-import gobject
-import gc
-from sets import *
-from kofoto.shelf import *
-from menuhandler import *
-from environment import env
-from objectselection import *
-from albumdialog import AlbumDialog
-from registerimagesdialog import RegisterImagesDialog
-
-class ObjectCollection(object):
-
-######################################################################
-### Public
-
-    def __init__(self):
-        env.debug("Init ObjectCollection")
-        self.__objectSelection = ObjectSelection(self)
-        self.__registeredViews = []
-        self.__disabledFields = Set()
-        self.__columnsType = [ gobject.TYPE_BOOLEAN,  # COLUMN_VALID_LOCATION
-                               gobject.TYPE_BOOLEAN,  # COLUMN_VALID_CHECKSUM
-                               gobject.TYPE_BOOLEAN,  # COLUMN_ROW_EDITABLE
-                               gobject.TYPE_BOOLEAN,  # COLUMN_IS_ALBUM
-                               gobject.TYPE_INT,      # COLUMN_OBJECT_ID
-                               gobject.TYPE_STRING,   # COLUMN_LOCATION
-                               gtk.gdk.Pixbuf,        # COLUMN_THUMBNAIL
-                               gobject.TYPE_STRING ]  # COLUMN_ALBUM_TAG
-        self.__objectMetadataMap = {
-            u"id"       :(gobject.TYPE_INT,    self.COLUMN_OBJECT_ID, None,                 None),
-            u"location" :(gobject.TYPE_STRING, self.COLUMN_LOCATION,  None,                 None),
-            u"thumbnail":(gtk.gdk.Pixbuf,      self.COLUMN_THUMBNAIL, None,                 None),
-            u"albumtag" :(gobject.TYPE_STRING, self.COLUMN_ALBUM_TAG, self._albumTagEdited, self.COLUMN_ALBUM_TAG) }
-        for name in env.shelf.getAllAttributeNames():
-            self.__addAttribute(name)
-        self.__treeModel = gtk.ListStore(*self.__columnsType)
-
-    # Return true if the objects has a defined order and may
-    # be reordered. An object that is reorderable is not
-    # allowed to also be sortable.
-    def isReorderable(self):
-        return False
-
-    # Return true if the objects may be sorted.
-    def isSortable(self):
-        return False
-
-    # Return true if objects may be added and removed from the collection.
-    def isMutable(self):
-        return False
-
-    def getCutLabel(self):
-        return "Cut reference"
-
-    def getCopyLabel(self):
-        return "Copy reference"
-
-    def getPasteLabel(self):
-        return "Paste reference"
-
-    def getDeleteLabel(self):
-        return "Delete reference"
-
-    def getDestroyLabel(self):
-        return "Destroy..."
-
-    def getCreateAlbumChildLabel(self):
-        return "Create album child..."
-
-    def getRegisterImagesLabel(self):
-        return "Register and add images..."
-
-    def getGenerateHtmlLabel(self):
-        return "Generate HTML..."
-
-    def getAlbumPropertiesLabel(self):
-        return "Album properties..."
-
-    def getOpenImageLabel(self):
-        return "Open image in external program..."
-
-    def getRotateImageLeftLabel(self):
-        return "Rotate image left"
-
-    def getRotateImageRightLabel(self):
-        return "Rotate image right"
-
-    def getObjectMetadataMap(self):
-        return self.__objectMetadataMap
-
-    def getModel(self):
-        return self.__treeModel
-
-    def getUnsortedModel(self):
-        return self.__treeModel
-
-    def convertToUnsortedRowNr(self, rowNr):
-        return rowNr
-
-    def convertFromUnsortedRowNr(self, unsortedRowNr):
-        return unsortedRowNr
-
-    def getObjectSelection(self):
-        return self.__objectSelection
-
-    def getDisabledFields(self):
-        return self.__disabledFields
-
-    def registerView(self, view):
-        env.debug("Register view to object collection")
-        self.__registeredViews.append(view)
-
-    def unRegisterView(self, view):
-        env.debug("Unregister view from object collection")
-        self.__registeredViews.remove(view)
-
-    def clear(self, freeze=True):
-        env.debug("Clearing object collection")
-        if freeze:
-            self._freezeViews()
-        self.__treeModel.clear()
-        gc.collect()
-        self.__nrOfAlbums = 0
-        self.__nrOfImages = 0
-        self._handleNrOfObjectsUpdate()
-        self.__objectSelection.unselectAll()
-        if freeze:
-            self._thawViews()
-
-    def cut(self, *foo):
-        raise Exception("Error. Not allowed to cut objects into objectCollection.") # TODO
-
-    def copy(self, *foo):
-        env.clipboard.setObjects(self.__objectSelection.getSelectedObjects())
-
-    def paste(self, *foo):
-        raise Exception("Error. Not allowed to paste objects into objectCollection.") # TODO
-
-    def delete(self, *foo):
-        raise Exception("Error. Not allowed to delete objects from objectCollection.") # TODO
-
-    def destroy(self, *foo):
-        model = self.getModel()
-
-        albumsSelected = False
-        imagesSelected = False
-        for position in self.__objectSelection:
-            iterator = model.get_iter(position)
-            isAlbum = model.get_value(
-                iterator, self.COLUMN_IS_ALBUM)
-            if isAlbum:
-                albumsSelected = True
-            else:
-                imagesSelected = True
-
-        assert albumsSelected ^ imagesSelected
-
-        self._freezeViews()
-        if albumsSelected:
-            dialogId = "destroyAlbumsDialog"
-        else:
-            dialogId = "destroyImagesDialog"
-        widgets = gtk.glade.XML(env.gladeFile, dialogId)
-        dialog = widgets.get_widget(dialogId)
-        result = dialog.run()
-        if result == gtk.RESPONSE_OK:
-            if albumsSelected:
-                deleteFiles = False
-            else:
-                checkbutton = widgets.get_widget("deleteImageFilesCheckbutton")
-                deleteFiles = checkbutton.get_active()
-            for obj in self.__objectSelection.getSelectedObjects():
-                if deleteFiles:
-                    try:
-                        os.remove(obj.getLocation())
-                        # TODO: Delete from image cache too?
-                    except OSError:
-                        pass
-                env.shelf.deleteObject(obj.getId())
-            locations = list(self.getObjectSelection())
-            locations.sort()
-            locations.reverse()
-            for loc in locations:
-                del model[loc]
-            self.getObjectSelection().unselectAll()
-        dialog.destroy()
-        # TODO: If the removed objects are albums, update the album widget.
-        self._thawViews()
-
-    COLUMN_VALID_LOCATION = 0
-    COLUMN_VALID_CHECKSUM = 1
-    COLUMN_ROW_EDITABLE   = 2
-    COLUMN_IS_ALBUM       = 3
-
-    # Columns visible to user
-    COLUMN_OBJECT_ID      = 4
-    COLUMN_LOCATION       = 5
-    COLUMN_THUMBNAIL      = 6
-    COLUMN_ALBUM_TAG      = 7
-
-    # Content in objectMetadata fields
-    TYPE                 = 0
-    COLUMN_NR            = 1
-    EDITED_CALLBACK      = 2
-    EDITED_CALLBACK_DATA = 3
-
-
-
-######################################################################
-### Only for subbclasses
-
-    def _getRegisteredViews(self):
-        return self.__registeredViews
-
-    def _loadObjectList(self, objectList):
-        env.enter("Object collection loading objects.")
-        self._freezeViews()
-        self.clear(False)
-        self._insertObjectList(objectList)
-        self._thawViews()
-        env.exit("Object collection loading objects. (albums=" + str(self.__nrOfAlbums) + " images=" + str(self.__nrOfImages) + ")")
-
-    def _insertObjectList(self, objectList, location=None):
-        widgets = gtk.glade.XML(env.gladeFile, "loadingProgressDialog")
-        loadingProgressDialog = widgets.get_widget(
-            "loadingProgressDialog")
-        loadingProgressDialog.show()
-        while gtk.events_pending():
-            gtk.main_iteration()
-
-        # location = None means insert last, otherwise insert before
-        # location.
-        #
-        # Note that this methods does NOT update objectSelection.
-        if location == None:
-            location = len(self.__treeModel)
-        for obj in objectList:
-            iterator = self.__treeModel.insert(location)
-            self.__treeModel.set_value(iterator, self.COLUMN_OBJECT_ID, obj.getId())
-            if obj.isAlbum():
-                self.__treeModel.set_value(iterator, self.COLUMN_IS_ALBUM, True)
-                self.__treeModel.set_value(iterator, self.COLUMN_ALBUM_TAG, obj.getTag())
-                self.__treeModel.set_value(iterator, self.COLUMN_LOCATION, None)
-                self.__nrOfAlbums += 1
-            else:
-                self.__treeModel.set_value(iterator, self.COLUMN_IS_ALBUM, False)
-                self.__treeModel.set_value(iterator, self.COLUMN_ALBUM_TAG, None)
-                self.__treeModel.set_value(iterator, self.COLUMN_LOCATION, obj.getLocation())
-                self.__nrOfImages += 1
-                # TODO Set COLUMN_VALID_LOCATION and COLUMN_VALID_CHECKSUM
-            for attribute, value in obj.getAttributeMap().items():
-                if "@" + attribute in self.__objectMetadataMap:
-                    column = self.__objectMetadataMap["@" + attribute][self.COLUMN_NR]
-                    self.__treeModel.set_value(iterator, column, value)
-            self.__treeModel.set_value(iterator, self.COLUMN_ROW_EDITABLE, True)
-            self.__loadThumbnail(self.__treeModel, iterator)
-            location += 1
-        self._handleNrOfObjectsUpdate()
-
-        loadingProgressDialog.destroy()
-
-    def _handleNrOfObjectsUpdate(self):
-        updatedDisabledFields = Set()
-        if self.__nrOfAlbums == 0:
-            updatedDisabledFields.add(u"albumtag")
-        if self.__nrOfImages == 0:
-            updatedDisabledFields.add(u"location")
-        for view in self.__registeredViews:
-            view.fieldsDisabled(updatedDisabledFields - self.__disabledFields)
-            view.fieldsEnabled(self.__disabledFields - updatedDisabledFields)
-        self.__disabledFields = updatedDisabledFields
-        env.debug("The following fields are disabled: " + str(self.__disabledFields))
-
-    def _getTreeModel(self):
-        return self.__treeModel
-
-    def _freezeViews(self):
-        for view in self.__registeredViews:
-            view.freeze()
-
-    def _thawViews(self):
-        for view in self.__registeredViews:
-            view.thaw()
-
-
-###############################################################################
-### Callback functions
-
-    def _attributeEdited(self, renderer, path, value, column, attributeName):
-        model = self.getModel()
-        columnNumber = self.__objectMetadataMap["@" + attributeName][self.COLUMN_NR]
-        iterator = model.get_iter(path)
-        oldValue = model.get_value(iterator, columnNumber)
-        if not oldValue:
-            oldValue = u""
-        value = unicode(value, "utf-8")
-        if oldValue != value:
-            # TODO Show dialog and ask for confirmation?
-            objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
-            obj = env.shelf.getObject(objectId)
-            obj.setAttribute(attributeName, value)
-            model.set_value(iterator, columnNumber, value)
-            env.debug("Object attribute edited")
-
-    def _albumTagEdited(self, renderer, path, value, column, columnNumber):
-        model = self.getModel()
-        assert model.get_value(iterator, self.COLUMN_IS_ALBUM)
-        iterator = model.get_iter(path)
-        oldValue = model.get_value(iterator, columnNumber)
-        if not oldValue:
-            oldValue = u""
-        value = unicode(value, "utf-8")
-        if oldValue != value:
-            # TODO Show dialog and ask for confirmation?
-            objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
-            obj = env.shelf.getAlbum(objectId)
-            obj.setTag(value)
-            # TODO Handle invalid album tag?
-            model.set_value(iterator, columnNumber, value)
-            # TODO Update the album tree widget.
-            env.debug("Album tag edited")
-
-    def createAlbumChild(self, *unused):
-        dialog = AlbumDialog("Create album")
-        dialog.run(self._createAlbumChildHelper)
-
-    def _createAlbumChildHelper(self, tag, desc):
-        newAlbum = env.shelf.createAlbum(tag)
-        if len(desc) > 0:
-            newAlbum.setAttribute(u"title", desc)
-        selectedObjects = self.__objectSelection.getSelectedObjects()
-        selectedAlbum = selectedObjects[0]
-        children = list(selectedAlbum.getChildren())
-        children.append(newAlbum)
-        selectedAlbum.setChildren(children)
-        env.mainwindow.reloadAlbumTree()
-
-    def registerAndAddImages(self, *unused):
-        selectedObjects = self.__objectSelection.getSelectedObjects()
-        assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
-        selectedAlbum = selectedObjects[0]
-        dialog = RegisterImagesDialog(selectedAlbum)
-        if dialog.run() == gtk.RESPONSE_OK:
-            env.mainwindow.reload() # TODO: Don't reload everything.
-        dialog.destroy()
-
-    def generateHtml(self, *unused):
-        selectedObjects = self.__objectSelection.getSelectedObjects()
-        assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
-        selectedAlbum = selectedObjects[0]
-        env.mainwindow.generateHtml(selectedAlbum)
-
-    def albumProperties(self, widget, data):
-        selectedObjects = self.__objectSelection.getSelectedObjects()
-        assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
-        selectedAlbumId = selectedObjects[0].getId()
-        dialog = AlbumDialog("Edit album", selectedAlbumId)
-        dialog.run(self._albumPropertiesHelper)
-
-    def _albumPropertiesHelper(self, tag, desc):
-        selectedObjects = self.__objectSelection.getSelectedObjects()
-        selectedAlbum = selectedObjects[0]
-        selectedAlbum.setTag(tag)
-        if len(desc) > 0:
-            selectedAlbum.setAttribute(u"title", desc)
-        else:
-            selectedAlbum.deleteAttribute(u"title")
-        env.mainwindow.reloadAlbumTree()
-        # TODO: Update objectCollection.
-
-    def rotateImage(self, widget, angle):
-        for (rowNr, obj) in self.__objectSelection.getMap().items():
-            if not obj.isAlbum():
-                location = obj.getLocation().encode(env.codeset)
-                if angle == 90:
-                    commandString = env.rotateRightCommand
-                else:
-                    commandString = env.rotateLeftCommand
-                command = commandString.encode(env.codeset) % { "location":location }
-                result = os.system(command)
-                if result == 0:
-                    obj.contentChanged()
-                    model = self.getUnsortedModel()
-                    self.__loadThumbnail(model, model.get_iter(rowNr))
-                else:
-                    dialog = gtk.MessageDialog(
-                        type=gtk.MESSAGE_ERROR,
-                        buttons=gtk.BUTTONS_OK,
-                        message_format="Failed to execute command: \"%s\"" % command)
-                    dialog.run()
-                    dialog.destroy()
-
-    def openImage(self, widget, data):
-        locations = ""
-        for obj in self.__objectSelection.getSelectedObjects():
-            if not obj.isAlbum():
-                location = obj.getLocation()
-                locations += location + " "
-        if locations != "":
-            command = env.openCommand % { "locations":locations }
-            # GIMP does not seem to be able to open locations containing swedish
-            # characters. I tried latin-1 and utf-8 without success.
-            result = os.system(command + " &")
-            if result != 0:
-                dialog = gtk.MessageDialog(
-                    type=gtk.MESSAGE_ERROR,
-                    buttons=gtk.BUTTONS_OK,
-                    message_format="Failed to execute command: \"%s\"" % command)
-                dialog.run()
-                dialog.destroy()
-
-######################################################################
-### Private
-
-    def __addAttribute(self, name):
-        self.__objectMetadataMap["@" + name] = (gobject.TYPE_STRING,
-                                                len(self.__columnsType),
-                                                self._attributeEdited,
-                                                name)
-        self.__columnsType.append(gobject.TYPE_STRING)
-
-    def __loadThumbnail(self, model, iterator):
-        objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
-        obj = env.shelf.getObject(objectId)
-        if obj.isAlbum():
-            pixbuf = env.albumIconPixbuf
-        else:
-            try:
-                thumbnailLocation = env.imageCache.get(
-                    obj, env.thumbnailSize[0], env.thumbnailSize[1])[0]
-                pixbuf = gtk.gdk.pixbuf_new_from_file(thumbnailLocation.encode(env.codeset))
-                # TODO Set and use COLUMN_VALID_LOCATION and COLUMN_VALID_CHECKSUM
-            except IOError:
-                pixbuf = env.unknownImageIconPixbuf
-        model.set_value(iterator, self.COLUMN_THUMBNAIL, pixbuf)
diff --git a/src/gkofoto/objectcollectionfactory.py b/src/gkofoto/objectcollectionfactory.py
deleted file mode 100644 (file)
index ed9b4bd..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-import string
-from searchresult import *
-from albummembers import *
-from environment import env
-from kofoto.search import *
-from kofoto.shelf import *
-
-class ObjectCollectionFactory:
-
-######################################################################
-### Public functions and constants
-
-    def __init__(self):
-        env.debug("Init ObjectCollectionFactory")
-        self.__searchResult = SearchResult()
-        self.__albumMembers = AlbumMembers()
-
-    def getObjectCollection(self, query):
-        env.debug("Object collection factory loading query: " + query);
-        self.__clear()
-        validAlbumTag = False
-        if query and query[0] == "/":
-            try:
-                verifyValidAlbumTag(query[1:])
-                validAlbumTag = True
-            except BadAlbumTagError:
-                pass
-        try:
-            if validAlbumTag:
-                self.__albumMembers.loadAlbum(env.shelf.getAlbum(query[1:]))
-                return self.__albumMembers
-            else:
-                self.__searchResult.loadQuery(query)
-                return self.__searchResult
-        except AlbumDoesNotExistError, tag:
-            errorText = "No such album tag: \"%s\"." % tag
-        except CategoryDoesNotExistError, tag:
-            errorText = "No such category tag: \"%s\"." % tag
-        except BadTokenError, pos:
-            errorText = "Error parsing query: bad token starting at position %s: \"%s\"." % (
-                pos,
-                query[pos[0]:])
-        except UnterminatedStringError, e:
-            errorText = "Error parsing query: unterminated string starting at position %s: \"%s\"." % (
-                e.args[0],
-                query[e.args[0]:])
-        except ParseError, text:
-            errorText = "Error parsing query: %s." % text
-        dialog = gtk.MessageDialog(
-            type=gtk.MESSAGE_ERROR,
-            buttons=gtk.BUTTONS_OK,
-            message_format=errorText)
-        dialog.run()
-        dialog.destroy()
-        self.__searchResult = SearchResult()
-        return self.__searchResult
-
-
-######################################################################
-### Private functions
-
-    def __clear(self):
-        self.__searchResult.clear()
-        self.__albumMembers.clear()
diff --git a/src/gkofoto/objectcollectionview.py b/src/gkofoto/objectcollectionview.py
deleted file mode 100644 (file)
index 7f57d5c..0000000
+++ /dev/null
@@ -1,327 +0,0 @@
-import gtk
-from environment import env
-from menuhandler import *
-from objectcollection import *
-
-class ObjectCollectionView:
-
-###############################################################################
-### Public
-
-    def __init__(self, view):
-        self._viewWidget = view
-        self._objectCollection = None
-        self._contextMenu = None
-        self.__objectCollectionLoaded = False
-        self.__hidden = True
-        self.__connections = []
-        view.connect("button_press_event", self._mouse_button_pressed)
-
-    def show(self, objectCollection):
-        if self.__hidden:
-            self.__hidden = False
-            self.__connectObjectCollection(objectCollection)
-            self._showHelper()
-            self._connectMenubarImageItems()
-            self._updateMenubarSortMenu()
-        else:
-            self.setObjectCollection(objectCollection)
-
-    def _connectMenubarImageItems(self):
-        self._connect(
-            env.widgets["menubarOpenImage"],
-            "activate",
-            self._objectCollection.openImage)
-        self._connect(
-            env.widgets["menubarRotateLeft"],
-            "activate",
-            self._objectCollection.rotateImage,
-            270)
-        self._connect(
-            env.widgets["menubarRotateRight"],
-            "activate",
-            self._objectCollection.rotateImage,
-            90)
-
-    def _updateMenubarSortMenu(self):
-        sortMenuGroup = self.__createSortMenuGroup(self._objectCollection)
-        sortByItem = env.widgets["menubarSortBy"]
-        if self._objectCollection.isSortable():
-            sortByItem.set_sensitive(True)
-            sortByItem.set_submenu(sortMenuGroup.createGroupMenu())
-        else:
-            sortByItem.remove_submenu()
-            sortByItem.set_sensitive(False)
-
-    def hide(self):
-        if not self.__hidden:
-            self.__hidden = True
-            self._hideHelper()
-            self.__disconnectObjectCollection()
-
-    def setObjectCollection(self, objectCollection):
-        if not self.__hidden:
-            env.debug("ObjectCollectionView sets object collection")
-            self.__connectObjectCollection(objectCollection)
-
-    def freeze(self):
-        self._freezeHelper()
-        self._objectCollection.getObjectSelection().removeChangedCallback(self.importSelection)
-        env.clipboard.removeChangedCallback(self._updateContextMenu)
-
-    def thaw(self):
-        self._thawHelper()
-        self._objectCollection.getObjectSelection().addChangedCallback(self.importSelection)
-        env.clipboard.addChangedCallback(self._updateContextMenu)
-        self.importSelection(self._objectCollection.getObjectSelection())
-        # importSelection makes an implicit _updateContextMenu()
-
-    def sortOrderChanged(self, sortOrder):
-        env.debug("Sort order is " + str(sortOrder))
-        self.__sortMenuGroup[sortOrder].activate()
-
-    def sortColumnChanged(self, sortColumn):
-        env.debug("Sort column is " + str(sortColumn))
-        self.__sortMenuGroup[sortColumn].activate()
-
-    def fieldsDisabled(self, fields):
-        pass
-
-    def fieldsEnabled(self, fields):
-        pass
-
-    def _mouse_button_pressed(self, widget, event):
-        if event.button == 3:
-            self._contextMenu.popup(None, None, None, event.button, event.time)
-            return True
-        else:
-            return False
-
-##############################################################################
-### Methods used by and overloaded by subbclasses
-
-    def _connect(self, obj, signal, function, data=None):
-        oid = obj.connect(signal, function, data)
-        self.__connections.append((obj, oid))
-
-    def _disconnect(self, obj, oid):
-        obj.disconnect(oid)
-        self.__connections.remove((obj, oid))
-
-    def _clearAllConnections(self):
-        for (obj, oid) in self.__connections:
-            obj.disconnect(oid)
-        self.__connections = []
-
-    def _createContextMenu(self, objectCollection):
-        env.debug("Creating view context menu")
-        self._contextMenu = gtk.Menu()
-        self.__clipboardMenuGroup = self.__createClipboardMenuGroup(objectCollection)
-        for item in self.__clipboardMenuGroup:
-            self._contextMenu.add(item)
-        self.__objectMenuGroup = self.__createObjectMenuGroup(objectCollection)
-        for item in self.__objectMenuGroup:
-            self._contextMenu.add(item)
-        self.__albumMenuGroup = self.__createAlbumMenuGroup(objectCollection)
-        for item in self.__albumMenuGroup:
-            self._contextMenu.add(item)
-        self.__imageMenuGroup = self.__createImageMenuGroup(objectCollection)
-        for item in self.__imageMenuGroup:
-            self._contextMenu.add(item)
-        self.__sortMenuGroup = self.__createSortMenuGroup(objectCollection)
-        self._contextMenu.add(self.__sortMenuGroup.createGroupMenuItem())
-
-    def _clearContextMenu(self):
-        env.debug("Clearing view context menu")
-        self._contextMenu = None
-        self.__clipboardMenuGroup = None
-        self.__objectMenuGroup = None
-        self.__albumMenuGroup = None
-        self.__imageMenuGroup = None
-        self.__sortMenuGroup = None
-
-    def _updateContextMenu(self, *foo):
-        env.debug("Updating context menu")
-        self.__objectMenuGroup[self._objectCollection.getDestroyLabel()].set_sensitive(False)
-        env.widgets["menubarDestroy"].set_sensitive(False)
-        mutable = self._objectCollection.isMutable()
-        objectSelection = self._objectCollection.getObjectSelection()
-        if objectSelection:
-            model = self._objectCollection.getModel()
-            rootAlbumId = env.shelf.getRootAlbum().getId()
-
-            albumsSelected = 0
-            imagesSelected = 0
-            rootAlbumSelected = False
-            for position in objectSelection:
-                iterator = model.get_iter(position)
-                isAlbum = model.get_value(
-                    iterator, self._objectCollection.COLUMN_IS_ALBUM)
-                if isAlbum:
-                    albumsSelected += 1
-                    if rootAlbumId == model.get_value(
-                        iterator, self._objectCollection.COLUMN_OBJECT_ID):
-                        rootAlbumSelected = True
-                else:
-                    imagesSelected += 1
-
-            self.__clipboardMenuGroup[self._objectCollection.getCutLabel()].set_sensitive(mutable)
-            env.widgets["menubarCut"].set_sensitive(mutable)
-            self.__clipboardMenuGroup[self._objectCollection.getCopyLabel()].set_sensitive(True)
-            env.widgets["menubarCopy"].set_sensitive(True)
-            self.__clipboardMenuGroup[self._objectCollection.getDeleteLabel()].set_sensitive(mutable)
-            env.widgets["menubarDelete"].set_sensitive(mutable)
-            destroyActive = (imagesSelected == 0) ^ (albumsSelected == 0) and not rootAlbumSelected
-            self.__objectMenuGroup[self._objectCollection.getDestroyLabel()].set_sensitive(destroyActive)
-            env.widgets["menubarDestroy"].set_sensitive(destroyActive)
-            if albumsSelected == 1 and imagesSelected == 0:
-                selectedAlbumId = model.get_value(
-                    iterator, self._objectCollection.COLUMN_OBJECT_ID)
-                selectedAlbum = env.shelf.getAlbum(selectedAlbumId)
-                if selectedAlbum.isMutable():
-                    self.__albumMenuGroup.enable()
-                    env.widgets["menubarCreateAlbumChild"].set_sensitive(True)
-                    env.widgets["menubarRegisterAndAddImages"].set_sensitive(True)
-                    env.widgets["menubarGenerateHtml"].set_sensitive(True)
-                    env.widgets["menubarProperties"].set_sensitive(True)
-                else:
-                    self.__albumMenuGroup.disable()
-                    self.__albumMenuGroup[self._objectCollection.getAlbumPropertiesLabel()].set_sensitive(True)
-                    env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
-                    env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
-                    env.widgets["menubarGenerateHtml"].set_sensitive(True)
-                    env.widgets["menubarProperties"].set_sensitive(True)
-            else:
-                self.__albumMenuGroup.disable()
-                env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
-                env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
-                env.widgets["menubarGenerateHtml"].set_sensitive(False)
-                env.widgets["menubarProperties"].set_sensitive(False)
-            if albumsSelected == 0 and imagesSelected > 0:
-                self.__imageMenuGroup.enable()
-                env.widgets["menubarOpenImage"].set_sensitive(True)
-                env.widgets["menubarRotateLeft"].set_sensitive(True)
-                env.widgets["menubarRotateRight"].set_sensitive(True)
-            else:
-                self.__imageMenuGroup.disable()
-                env.widgets["menubarOpenImage"].set_sensitive(False)
-                env.widgets["menubarRotateLeft"].set_sensitive(False)
-                env.widgets["menubarRotateRight"].set_sensitive(False)
-        else:
-            self.__clipboardMenuGroup.disable()
-            env.widgets["menubarCut"].set_sensitive(False)
-            env.widgets["menubarCopy"].set_sensitive(False)
-            env.widgets["menubarDelete"].set_sensitive(False)
-
-            self.__objectMenuGroup.disable()
-            env.widgets["menubarDestroy"].set_sensitive(False)
-
-            self.__albumMenuGroup.disable()
-            env.widgets["menubarCreateAlbumChild"].set_sensitive(False)
-            env.widgets["menubarRegisterAndAddImages"].set_sensitive(False)
-            env.widgets["menubarGenerateHtml"].set_sensitive(False)
-            env.widgets["menubarProperties"].set_sensitive(False)
-
-            self.__imageMenuGroup.disable()
-            env.widgets["menubarOpenImage"].set_sensitive(False)
-            env.widgets["menubarRotateLeft"].set_sensitive(False)
-            env.widgets["menubarRotateRight"].set_sensitive(False)
-
-        if env.clipboard.hasObjects():
-            self.__clipboardMenuGroup[self._objectCollection.getPasteLabel()].set_sensitive(mutable)
-            env.widgets["menubarPaste"].set_sensitive(mutable)
-        else:
-            self.__clipboardMenuGroup[self._objectCollection.getPasteLabel()].set_sensitive(False)
-            env.widgets["menubarPaste"].set_sensitive(False)
-
-
-###############################################################################
-### Private
-
-    def __connectObjectCollection(self, objectCollection):
-        if self._objectCollection != None:
-            self.__disconnectObjectCollection()
-        self._objectCollection = objectCollection
-        self._createContextMenu(objectCollection)
-        self._connectObjectCollectionHelper()
-        self.thaw()
-        self._objectCollection.registerView(self)
-
-    def __disconnectObjectCollection(self):
-        if self._objectCollection is not None:
-            self._objectCollection.unRegisterView(self)
-            self.freeze()
-            self._disconnectObjectCollectionHelper()
-            self._clearContextMenu()
-            self._objectCollection = None
-
-    def __createSortMenuGroup(self, objectCollection):
-        menuGroup = MenuGroup("Sort by")
-        if objectCollection.isSortable():
-            env.debug("Creating sort menu group for sortable log collection")
-            menuGroup.addRadioMenuItem("Ascending",
-                                       objectCollection.setSortOrder,
-                                       gtk.SORT_ASCENDING)
-            menuGroup.addRadioMenuItem("Descending",
-                                       objectCollection.setSortOrder,
-                                       gtk.SORT_DESCENDING)
-            menuGroup.addSeparator()
-            objectMetadataMap = objectCollection.getObjectMetadataMap()
-            columnNames = list(objectMetadataMap.keys())
-            columnNames.sort()
-            for columnName in columnNames:
-                if objectMetadataMap[columnName][ObjectCollection.TYPE] != gtk.gdk.Pixbuf:
-                    menuGroup.addRadioMenuItem(columnName,
-                                               objectCollection.setSortColumnName,
-                                               columnName)
-        return menuGroup
-
-    def __createClipboardMenuGroup(self, oc):
-        menuGroup = MenuGroup()
-        env.debug("Creating clipboard menu")
-        menuGroup.addStockImageMenuItem(
-            oc.getCutLabel(), gtk.STOCK_CUT, oc.cut)
-        menuGroup.addStockImageMenuItem(
-            oc.getCopyLabel(), gtk.STOCK_COPY, oc.copy)
-        menuGroup.addStockImageMenuItem(
-            oc.getPasteLabel(), gtk.STOCK_PASTE, oc.paste)
-        menuGroup.addStockImageMenuItem(
-            oc.getDeleteLabel(), gtk.STOCK_DELETE, oc.delete)
-        menuGroup.addSeparator()
-        return menuGroup
-
-    def __createObjectMenuGroup(self, oc):
-        menuGroup = MenuGroup()
-        menuGroup.addStockImageMenuItem(
-            oc.getDestroyLabel(), gtk.STOCK_DELETE, oc.destroy)
-        menuGroup.addSeparator()
-        return menuGroup
-
-    def __createAlbumMenuGroup(self, oc):
-        menuGroup = MenuGroup()
-        menuGroup.addMenuItem(
-            oc.getCreateAlbumChildLabel(), oc.createAlbumChild)
-        menuGroup.addMenuItem(
-            oc.getRegisterImagesLabel(), oc.registerAndAddImages)
-        menuGroup.addMenuItem(
-            oc.getGenerateHtmlLabel(), oc.generateHtml)
-        menuGroup.addStockImageMenuItem(
-            oc.getAlbumPropertiesLabel(),
-            gtk.STOCK_PROPERTIES,
-            oc.albumProperties)
-        menuGroup.addSeparator()
-        return menuGroup
-
-    def __createImageMenuGroup(self, oc):
-        menuGroup = MenuGroup()
-        menuGroup.addMenuItem(oc.getOpenImageLabel(), oc.openImage)
-        menuGroup.addImageMenuItem(
-            oc.getRotateImageLeftLabel(),
-            os.path.join(env.iconDir, "rotateleft.png"),
-            oc.rotateImage, 270)
-        menuGroup.addImageMenuItem(
-            oc.getRotateImageRightLabel(),
-            os.path.join(env.iconDir, "rotateright.png"),
-            oc.rotateImage, 90)
-        menuGroup.addSeparator()
-        return menuGroup
diff --git a/src/gkofoto/objectselection.py b/src/gkofoto/objectselection.py
deleted file mode 100644 (file)
index d127847..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-from environment import env
-from sets import Set
-
-class ObjectSelection:
-    def __init__(self, objectCollection):
-        # Don't forget to update this class when the model is reordered or
-        # when rows are removed or added.
-        self.__selectedObjects = {}
-        # When objects are stored in self.__selectedObjects, the key MUST be
-        # the location in the UNSORTED model since this class is not
-        # notified when/if the model is re-sorted.
-        #
-        # This class must know about each object's row to be able to distinguish
-        # individual objects in an album that contains multiple instances
-        # of the same image or album.
-        self.__changedCallbacks = Set()
-        self.__objectCollection = objectCollection
-    def addChangedCallback(self, callback):
-        self.__changedCallbacks.add(callback)
-
-    def removeChangedCallback(self, callback):
-        self.__changedCallbacks.remove(callback)
-
-    def unselectAll(self, notify=True):
-        self.__selectedObjects.clear()
-        if notify:
-            self.__invokeChangedCallbacks()
-
-    def setSelection(self, rowNrs, notify=True):
-        self.__selectedObjects.clear()
-        for rowNr in rowNrs:
-            self.addSelection(rowNr, False)
-        if notify:
-            self.__invokeChangedCallbacks()
-
-    def addSelection(self, rowNr, notify=True):
-        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
-        self.__selectedObjects[unsortedRowNr] = self.__getObject(unsortedRowNr)
-        if notify:
-            self.__invokeChangedCallbacks()
-
-    def removeSelection(self, rowNr, notify=True):
-        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
-        del self.__selectedObjects[unsortedRowNr]
-        if notify:
-            self.__invokeChangedCallbacks()
-
-    def getSelectedObjects(self):
-        return self.__selectedObjects.values()
-
-    def getLowestSelectedRowNr(self):
-        rowNrs = list(self)
-        if (len(rowNrs) > 0):
-            rowNrs.sort()
-            return rowNrs[0]
-        else:
-            return None
-
-    def getMap(self):
-        return self.__selectedObjects
-
-    def __contains__(self, rowNr):
-        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
-        return unsortedRowNr in self.__selectedObjects.keys()
-
-    def __len__(self):
-        return len(self.__selectedObjects)
-
-    def __iter__(self):
-        for unsortedRowNr in self.__selectedObjects.keys():
-            rowNr = self.__objectCollection.convertFromUnsortedRowNr(unsortedRowNr)
-            yield rowNr
-
-    def __getitem__(self, rowNr):
-        unsortedRowNr = self.__objectCollection.convertToUnsortedRowNr(rowNr)
-        return self.__selectedObjects[unsortedRowNr]
-
-    def __invokeChangedCallbacks(self):
-        env.debug("Invoking selection changed callbacks: " + str(self.__selectedObjects.keys()))
-        for callback in self.__changedCallbacks:
-            callback(self)
-
-    def __getObject(self, unsortedRowNr):
-        objectId = self.__objectCollection.getUnsortedModel()[unsortedRowNr][self.__objectCollection.COLUMN_OBJECT_ID]
-        return env.shelf.getObject(objectId)
diff --git a/src/gkofoto/registerimagesdialog.py b/src/gkofoto/registerimagesdialog.py
deleted file mode 100644 (file)
index a86599c..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-import gtk
-import os
-from environment import env
-from kofoto.shelf import ImageExistsError, NotAnImageError, makeValidTag
-from kofoto.clientutils import walk_files
-
-class RegisterImagesDialog(gtk.FileSelection):
-    def __init__(self, albumToAddTo=None):
-        gtk.FileSelection.__init__(self, title="Register images")
-        self.__albumToAddTo = albumToAddTo
-        self.set_select_multiple(True)
-        self.ok_button.connect("clicked", self._ok)
-
-    def _ok(self, widget):
-        widgets = gtk.glade.XML(env.gladeFile, "registrationProgressDialog")
-        registrationProgressDialog = widgets.get_widget(
-            "registrationProgressDialog")
-        newImagesCount = widgets.get_widget(
-            "newImagesCount")
-        alreadyRegisteredImagesCount = widgets.get_widget(
-            "alreadyRegisteredImagesCount")
-        nonImagesCount = widgets.get_widget(
-            "nonImagesCount")
-        filesInvestigatedCount = widgets.get_widget(
-            "filesInvestigatedCount")
-        okButton = widgets.get_widget("okButton")
-        okButton.set_sensitive(False)
-
-        registrationProgressDialog.show()
-
-        newImages = 0
-        alreadyRegisteredImages = 0
-        nonImages = 0
-        filesInvestigated = 0
-        images = []
-        for filepath in walk_files(self.get_selections()):
-            try:
-                try:
-                    filepath = filepath.decode("utf-8")
-                except UnicodeDecodeError:
-                    filepath = filepath.decode("latin1")
-                image = env.shelf.createImage(filepath)
-                images.append(image)
-                newImages += 1
-                newImagesCount.set_text(str(newImages))
-            except ImageExistsError:
-                alreadyRegisteredImages += 1
-                alreadyRegisteredImagesCount.set_text(str(alreadyRegisteredImages))
-            except NotAnImageError:
-                nonImages += 1
-                nonImagesCount.set_text(str(nonImages))
-            filesInvestigated += 1
-            filesInvestigatedCount.set_text(str(filesInvestigated))
-            while gtk.events_pending():
-                gtk.main_iteration()
-        if self.__albumToAddTo:
-            children = list(self.__albumToAddTo.getChildren())
-            self.__albumToAddTo.setChildren(children + images)
-
-        okButton.set_sensitive(True)
-        registrationProgressDialog.run()
-        registrationProgressDialog.destroy()
diff --git a/src/gkofoto/searchresult.py b/src/gkofoto/searchresult.py
deleted file mode 100644 (file)
index b2aac8e..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-from kofoto.shelf import *
-from kofoto.search import *
-from sortableobjectcollection import *
-from environment import env
-
-class SearchResult(SortableObjectCollection):
-
-######################################################################
-### Public functions and constants
-
-    def __init__(self):
-        SortableObjectCollection.__init__(self)
-
-    def loadQuery(self, query):
-        parser = Parser(env.shelf)
-        self._loadObjectList(env.shelf.search(parser.parse(query)))
-
-######################################################################
-### Private functions and datastructures
diff --git a/src/gkofoto/singleobjectview.py b/src/gkofoto/singleobjectview.py
deleted file mode 100644 (file)
index 4cd427d..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-import gtk
-import sys
-from environment import env
-from gkofoto.imageview import *
-from gkofoto.objectcollectionview import *
-
-class SingleObjectView(ObjectCollectionView, ImageView):
-
-###############################################################################
-### Public
-
-    def __init__(self):
-        env.debug("Init SingleObjectView")
-        ImageView.__init__(self)
-        ObjectCollectionView.__init__(self, env.widgets["objectView"])
-        self._viewWidget.add(self)
-        self.show_all()
-        env.widgets["nextButton"].connect("clicked", self._goto, 1)
-        env.widgets["menubarNextImage"].connect("activate", self._goto, 1)
-        env.widgets["previousButton"].connect("clicked", self._goto, -1)
-        env.widgets["menubarPreviousImage"].connect("activate", self._goto, -1)
-        env.widgets["zoomToFit"].connect("clicked", self.fitToWindow)
-        env.widgets["menubarZoomToFit"].connect("activate", self.fitToWindow)
-        env.widgets["zoom100"].connect("clicked", self.zoom100)
-        env.widgets["menubarActualSize"].connect("activate", self.zoom100)
-        env.widgets["zoomIn"].connect("clicked", self.zoomIn)
-        env.widgets["menubarZoomIn"].connect("activate", self.zoomIn)
-        env.widgets["zoomOut"].connect("clicked", self.zoomOut)
-        env.widgets["menubarZoomOut"].connect("activate", self.zoomOut)
-        self.connect("button_press_event", self._mouse_button_pressed)
-        self.__selectionLocked = False
-
-    def importSelection(self, objectSelection):
-        if not self.__selectionLocked:
-            env.debug("SingleImageView is importing selection")
-            self.__selectionLocked = True
-            model = self._objectCollection.getModel()
-            if len(model) == 0:
-                # Model is empty. No rows can be selected.
-                self.__selectedRowNr = -1
-                self.clear()
-            else:
-                if len(objectSelection) == 0:
-                    # No objects is selected -> select first object
-                    self.__selectedRowNr = 0
-                    objectSelection.setSelection([self.__selectedRowNr])
-                elif len(objectSelection) > 1:
-                    # More than one object selected -> select first object
-                    self.__selectedRowNr = objectSelection.getLowestSelectedRowNr()
-                    objectSelection.setSelection([self.__selectedRowNr])
-                else:
-                    # Exactly one object selected
-                    self.__selectedRowNr = objectSelection.getLowestSelectedRowNr()
-                selectedObject = objectSelection[self.__selectedRowNr]
-                if selectedObject.isAlbum():
-                    self.loadFile(env.albumIconFileName, False)
-                else:
-                    self.loadFile(selectedObject.getLocation(), False)
-            enablePreviousButton = (self.__selectedRowNr > 0)
-            env.widgets["previousButton"].set_sensitive(enablePreviousButton)
-            env.widgets["menubarPreviousImage"].set_sensitive(enablePreviousButton)
-            enableNextButton = (self.__selectedRowNr != -1 and
-                                self.__selectedRowNr < len(model) - 1)
-            env.widgets["nextButton"].set_sensitive(enableNextButton)
-            env.widgets["menubarNextImage"].set_sensitive(enableNextButton)
-            self.__selectionLocked = False
-        self._updateContextMenu()
-
-        # Override sensitiveness set in _updateContextMenu.
-        for widgetName in [
-                "menubarCut",
-                "menubarCopy",
-                "menubarDelete",
-                "menubarDestroy",
-                "menubarProperties",
-                "menubarCreateAlbumChild",
-                "menubarRegisterAndAddImages",
-                "menubarGenerateHtml",
-                ]:
-            env.widgets[widgetName].set_sensitive(False)
-
-    def _showHelper(self):
-        env.enter("SingleObjectView.showHelper()")
-        env.widgets["objectView"].show()
-        env.widgets["objectView"].grab_focus()
-        for widgetName in [
-                "zoom100",
-                "zoomToFit",
-                "zoomIn",
-                "zoomOut",
-                "menubarZoom",
-                ]:
-            env.widgets[widgetName].set_sensitive(True)
-        env.exit("SingleObjectView.showHelper()")
-
-    def _hideHelper(self):
-        env.enter("SingleObjectView.hideHelper()")
-        env.widgets["objectView"].hide()
-        for widgetName in [
-                "previousButton",
-                "nextButton",
-                "menubarPreviousImage",
-                "menubarNextImage",
-                "zoom100",
-                "zoomToFit",
-                "zoomIn",
-                "zoomOut",
-                "menubarZoom",
-                ]:
-            env.widgets[widgetName].set_sensitive(False)
-        env.exit("SingleObjectView.hideHelper()")
-
-    def _connectObjectCollectionHelper(self):
-        env.enter("Connecting SingleObjectView to object collection")
-        env.exit("Connecting SingleObjectView to object collection")
-
-    def _disconnectObjectCollectionHelper(self):
-        env.enter("Disconnecting SingleObjectView from object collection")
-        env.exit("Disconnecting SingleObjectView from object collection")
-
-    def _freezeHelper(self):
-        env.enter("SingleObjectView.freezeHelper()")
-        self._clearAllConnections()
-        self.clear()
-        env.exit("SingleObjectView.freezeHelper()")
-
-    def _thawHelper(self):
-        env.enter("SingleObjectView.thawHelper()")
-        model = self._objectCollection.getModel()
-        # The row_changed event is needed when the location attribute of the image object is changed.
-        self._connect(model, "row_changed", self._rowChanged)
-        # The following events are needed to update the previous and next navigation buttons.
-        self._connect(model, "rows_reordered", self._modelUpdated)
-        self._connect(model, "row_inserted", self._modelUpdated)
-        self._connect(model, "row_deleted", self._modelUpdated)
-        self.importSelection(self._objectCollection.getObjectSelection())
-        env.exit("SingleObjectView.thawHelper()")
-
-    def _modelUpdated(self, *foo):
-        env.debug("SingleObjectView is handling model update")
-        self.importSelection(self._objectCollection.getObjectSelection())
-
-    def _rowChanged(self, model, path, iter, arg, *unused):
-        if path[0] == self.__selectedRowNr:
-            env.debug("selected object in SingleObjectView changed")
-            objectSelection = self._objectCollection.getObjectSelection()
-            obj = objectSelection[path[0]]
-            if not obj.isAlbum():
-                self.loadFile(obj.getLocation(), True)
-
-    def _goto(self, button, direction):
-        objectSelection = self._objectCollection.getObjectSelection()
-        objectSelection.setSelection([self.__selectedRowNr + direction])
-
-    def _viewWidgetFocusInEvent(self, widget, event):
-        ObjectCollectionView._viewWidgetFocusInEvent(self, widget, event)
-        for widgetName in [
-                "menubarClear",
-                "menubarSelectAll",
-                ]:
-            env.widgets[widgetName].set_sensitive(False)
diff --git a/src/gkofoto/sortableobjectcollection.py b/src/gkofoto/sortableobjectcollection.py
deleted file mode 100644 (file)
index 18bf3e3..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-import gtk
-from environment import env
-from mysortedmodel import *
-from objectcollection import *
-
-def attributeSortFunc(model, iterA, iterB, column):
-    valueA = model.get_value(iterA, column)
-    valueB = model.get_value(iterB, column)
-    try:
-        result = cmp(float(valueA), float(valueB))
-    except (ValueError, TypeError):
-        result = cmp(valueA, valueB)
-    if result == 0:
-        result = cmp(model.get_value(iterA, ObjectCollection.COLUMN_OBJECT_ID),
-                     model.get_value(iterB, ObjectCollection.COLUMN_OBJECT_ID))
-    return result
-
-class SortableObjectCollection(ObjectCollection):
-
-######################################################################
-### Public
-
-    def __init__(self):
-        ObjectCollection.__init__(self)
-        self.__sortOrder = None
-        self.__sortColumnName = None
-        self.__sortedTreeModel = MySortedModel(self.getUnsortedModel())
-        self.setSortOrder(order=gtk.SORT_ASCENDING)
-        self.setSortColumnName(columnName=env.defaultSortColumn)
-
-    def isSortable(self):
-        return True
-
-    def isReorderable(self):
-        return False
-
-    def getModel(self):
-        return self.__sortedTreeModel
-
-    def getUnsortedModel(self):
-        return ObjectCollection.getModel(self)
-
-    def convertToUnsortedRowNr(self, rowNr):
-        return self.__sortedTreeModel.convert_path_to_child_path(rowNr)[0]
-
-    def convertFromUnsortedRowNr(self, unsortedRowNr):
-        return self.__sortedTreeModel. convert_child_path_to_path(unsortedRowNr)[0]
-
-    def getSortOrder(self):
-        return self.__sortOrder
-
-    def getSortColumnName(self):
-        return self.__sortColumnName
-
-    def setSortOrder(self, widget=None, order=None):
-        if widget != None and not widget.get_active():
-            # ignore the callback when the radio menu item is unselected
-            return
-        if self.__sortOrder != order:
-            env.debug("Setting sort order to: " + str(order))
-            self.__sortOrder = order
-            self.__configureSortedModel(self.__sortColumnName, self.__sortOrder)
-            self.__emitSortOrderChanged()
-
-    def setSortColumnName(self, widget=None, columnName=None):
-        if widget != None and not widget.get_active():
-            # ignore the callback when the radio menu item is unselected
-            return
-        if self.__sortColumnName != columnName:
-            if not columnName in self.getObjectMetadataMap():
-                columnName = "id"
-            env.debug("Setting sort column to: " + columnName)
-            self.__sortColumnName = columnName
-            self.__configureSortedModel(self.__sortColumnName, self.__sortOrder)
-            self.__emitSortColumnChanged()
-
-    def registerView(self, view):
-        ObjectCollection.registerView(self, view)
-        self.__emitSortOrderChanged()
-        self.__emitSortColumnChanged()
-
-    def __emitSortOrderChanged(self):
-        for view in self._getRegisteredViews():
-            view.sortOrderChanged(self.__sortOrder)
-
-    def __emitSortColumnChanged(self):
-        for view in self._getRegisteredViews():
-            view.sortColumnChanged(self.__sortColumnName)
-
-    def __configureSortedModel(self, sortColumnName, sortOrder):
-        if (sortOrder != None and sortColumnName != None):
-            metaDataMap = self.getObjectMetadataMap()
-            if not metaDataMap.has_key(sortColumnName):
-                sortColumnName = u"id"
-            sortColumnNr = metaDataMap[sortColumnName][self.COLUMN_NR]
-            model = self.getModel()
-            model.set_sort_column_id(sortColumnNr, self.__sortOrder)
-            # It is important that the attributeSortFunc is not an class member method,
-            # otherwise we are leaking memmory.
-            model.set_sort_func(sortColumnNr,
-                                attributeSortFunc,
-                                sortColumnNr)
index ef0ef10..ef2de91 100755 (executable)
@@ -12,10 +12,8 @@ if os.path.islink(sys.argv[0]):
 else:
     bindir = os.path.dirname(sys.argv[0])
 
-# Find kofoto libraries (../lib) and gkofoto libraries (..) in the
-# source tree.
+# Find kofoto libraries (../lib) in the source tree.
 sys.path.insert(0, os.path.join(bindir, "..", "lib"))
-sys.path.insert(0, os.path.join(bindir, ".."))
 
 from gkofoto.main import main
 main(bindir, sys.argv)
diff --git a/src/gkofoto/tableview.py b/src/gkofoto/tableview.py
deleted file mode 100644 (file)
index 1710148..0000000
+++ /dev/null
@@ -1,363 +0,0 @@
-import gtk
-from environment import env
-from sets import Set
-from gkofoto.objectcollectionview import *
-from sets import Set
-from objectcollection import *
-from menuhandler import *
-
-class TableView(ObjectCollectionView):
-
-###############################################################################
-### Public
-
-    def __init__(self):
-        env.debug("Init TableView")
-        ObjectCollectionView.__init__(self, env.widgets["tableView"])
-        selection = self._viewWidget.get_selection()
-        selection.set_mode(gtk.SELECTION_MULTIPLE)
-        self.__selectionLocked = False
-        self._viewWidget.connect("drag_data_received", self._onDragDataReceived)
-        self._viewWidget.connect("drag-data-get", self._onDragDataGet)
-        self.__userChosenColumns = {}
-        self.__createdColumns = {}
-        self.__editedCallbacks = {}
-        self._connectedOids = []
-        # Import the users setting in the configuration file for
-        # which columns that shall be shown.
-        columnLocation = 0
-        for columnName in env.defaultTableViewColumns:
-            self.__userChosenColumns[columnName] = columnLocation
-            columnLocation += 1
-
-    def importSelection(self, objectSelection):
-        if not self.__selectionLocked:
-            env.debug("TableView is importing selection")
-            self.__selectionLocked = True
-            selection = self._viewWidget.get_selection()
-            selection.unselect_all()
-            for rowNr in objectSelection:
-                selection.select_path(rowNr)
-            rowNr = self._objectCollection.getObjectSelection().getLowestSelectedRowNr()
-            if rowNr is None:
-                if len(self._objectCollection.getModel()) > 0:
-                    # Scroll to first object in view
-                    self._viewWidget.scroll_to_cell((0,), None, False, 0, 0)
-            else:
-                # Scroll to first selected object in view
-                self._viewWidget.scroll_to_cell(rowNr, None, False, 0, 0)
-            self.__selectionLocked = False
-        self._updateContextMenu()
-        self._updateMenubarSortMenu()
-
-    def fieldsDisabled(self, fields):
-        env.debug("Table view disable fields: " + str(fields))
-        self.__removeColumnsAndUpdateLocation(fields)
-        for columnName in fields:
-            self.__viewGroup[columnName].set_sensitive(False)
-
-    def fieldsEnabled(self, fields):
-        env.debug("Table view enable fields: " + str(fields))
-        objectMetadataMap = self._objectCollection.getObjectMetadataMap()
-        for columnName in fields:
-            self.__viewGroup[columnName].set_sensitive(True)
-            if columnName not in self.__createdColumns:
-                if columnName in self.__userChosenColumns:
-                    self.__createColumn(columnName, objectMetadataMap, self.__userChosenColumns[columnName])
-
-    def _showHelper(self):
-        env.enter("TableView.showHelper()")
-        env.widgets["tableViewScroll"].show()
-        self._viewWidget.grab_focus()
-        env.exit("TableView.showHelper()")
-
-    def _hideHelper(self):
-        env.enter("TableView.hideHelper()")
-        env.widgets["tableViewScroll"].hide()
-        env.exit("TableView.hideHelper()")
-
-    def _connectObjectCollectionHelper(self):
-        env.enter("Connecting TableView to object collection")
-        # Set model
-        self._viewWidget.set_model(self._objectCollection.getModel())
-        # Create columns
-        objectMetadataMap = self._objectCollection.getObjectMetadataMap()
-        disabledFields = self._objectCollection.getDisabledFields()
-        columnLocationList = self.__userChosenColumns.items()
-        columnLocationList.sort(lambda x, y: cmp(x[1], y[1]))
-        env.debug("Column locations: " + str(columnLocationList))
-        for (columnName, columnLocation) in columnLocationList:
-            if (columnName in objectMetadataMap and
-                columnName not in disabledFields):
-                self.__createColumn(columnName, objectMetadataMap)
-                self.__viewGroup[columnName].activate()
-        self.fieldsDisabled(self._objectCollection.getDisabledFields())
-        env.exit("Connecting TableView to object collection")
-
-    def _initDragAndDrop(self):
-        # Init drag & drop
-        if self._objectCollection.isReorderable() and not self._objectCollection.isSortable():
-            targetEntries = [("STRING", gtk.TARGET_SAME_WIDGET, 0)]
-            self._viewWidget.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
-                                                      targetEntries,
-                                                      gtk.gdk.ACTION_MOVE)
-            self._viewWidget.enable_model_drag_dest(targetEntries, gtk.gdk.ACTION_COPY)
-        else:
-            self._viewWidget.unset_rows_drag_source()
-            self._viewWidget.unset_rows_drag_dest()
-
-    def _disconnectObjectCollectionHelper(self):
-        env.enter("Disconnecting TableView from object collection")
-        self.__removeColumnsAndUpdateLocation()
-        self._viewWidget.set_model(None)
-        env.exit("Disconnecting TableView from object collection")
-
-    def _freezeHelper(self):
-        env.enter("TableView.freezeHelper()")
-        self._clearAllConnections()
-        env.exit("TableView.freezeHelper()")
-
-    def _thawHelper(self):
-        env.enter("TableView.thawHelper()")
-        self._initDragAndDrop()
-        self._connect(self._viewWidget, "focus-in-event", self._treeViewFocusInEvent)
-        self._connect(self._viewWidget, "focus-out-event", self._treeViewFocusOutEvent)
-        self._connect(self._viewWidget.get_selection(), "changed", self._widgetSelectionChanged)
-        env.exit("TableView.thawHelper()")
-
-    def _createContextMenu(self, objectCollection):
-        ObjectCollectionView._createContextMenu(self, objectCollection)
-        self.__viewGroup = self.__createTableColumnsMenuGroup(objectCollection)
-        self._contextMenu.add(self.__viewGroup.createGroupMenuItem())
-
-    def __createTableColumnsMenuGroup(self, objectCollection):
-        menuGroup = MenuGroup("View columns")
-        columnNames = objectCollection.getObjectMetadataMap().keys()
-        columnNames.sort()
-        for columnName in columnNames:
-            menuGroup.addCheckedMenuItem(
-                columnName,
-                self._viewColumnToggled,
-                columnName)
-        return menuGroup
-
-    def _clearContextMenu(self):
-        ObjectCollectionView._clearContextMenu(self)
-        self.__viewGroup = None
-
-###############################################################################
-### Callback functions registered by this class but invoked from other classes.
-
-    def _treeViewFocusInEvent(self, widget, event, data):
-        oc = self._objectCollection
-        for widgetName, function in [
-                ("menubarCut", self._objectCollection.cut),
-                ("menubarCopy", self._objectCollection.copy),
-                ("menubarPaste", self._objectCollection.paste),
-                ("menubarDestroy", oc.destroy),
-                ("menubarClear", lambda x: widget.get_selection().unselect_all()),
-                ("menubarSelectAll", lambda x: widget.get_selection().select_all()),
-                ("menubarCreateAlbumChild", oc.createAlbumChild),
-                ("menubarRegisterAndAddImages", oc.registerAndAddImages),
-                ("menubarGenerateHtml", oc.generateHtml),
-                ("menubarProperties", oc.albumProperties),
-                ]:
-            w = env.widgets[widgetName]
-            oid = w.connect("activate", function)
-            self._connectedOids.append((w, oid))
-
-        self._updateContextMenu()
-
-        for widgetName in [
-                "menubarClear",
-                "menubarSelectAll"
-                ]:
-            env.widgets[widgetName].set_sensitive(True)
-
-    def _treeViewFocusOutEvent(self, widget, event, data):
-        for (widget, oid) in self._connectedOids:
-            widget.disconnect(oid)
-        self._connectedOids = []
-        for widgetName in [
-                "menubarCut",
-                "menubarCopy",
-                "menubarPaste",
-                "menubarDestroy",
-                "menubarClear",
-                "menubarSelectAll",
-                "menubarCreateAlbumChild",
-                "menubarRegisterAndAddImages",
-                "menubarGenerateHtml",
-                "menubarProperties",
-                ]:
-            env.widgets[widgetName].set_sensitive(False)
-
-    def _widgetSelectionChanged(self, selection, data):
-        if not self.__selectionLocked:
-            env.enter("TableView selection changed")
-            self.__selectionLocked = True
-            rowNrs = []
-            selection.selected_foreach(lambda model,
-                                       path,
-                                       iter:
-                                       rowNrs.append(path[0]))
-            self._objectCollection.getObjectSelection().setSelection(rowNrs)
-            self.__selectionLocked = False
-            env.exit("TableView selection changed")
-
-    def _onDragDataGet(self, widget, dragContext, selection, info, timestamp):
-        selectedRows = []
-        # TODO replace with "get_selected_rows()" when it is introduced in Pygtk 2.2 API
-        self._viewWidget.get_selection().selected_foreach(lambda model,
-                                                          path,
-                                                          iter:
-                                                          selectedRows.append(model[path]))
-        if len(selectedRows) == 1:
-            # Ignore drag & drop if zero or more then one row is selected
-            # Drag & drop of multiple rows will probably come in gtk 2.4.
-            # http://mail.gnome.org/archives/gtk-devel-list/2003-December/msg00160.html
-            sourceRowNumber = str(selectedRows[0].path[0])
-            selection.set_text(sourceRowNumber, len(sourceRowNumber))
-        else:
-            env.debug("Ignoring drag&drop when only one row is selected")
-
-
-    def _onDragDataReceived(self, treeview, dragContext, x, y, selection, info, eventtime):
-        targetData = treeview.get_dest_row_at_pos(x, y)
-        if selection.get_text() == None:
-            dragContext.finish(False, False, eventtime)
-        else:
-            model = self._objectCollection.getModel()
-            if targetData == None:
-                targetPath = (len(model) - 1,)
-                dropPosition = gtk.TREE_VIEW_DROP_AFTER
-            else:
-                targetPath, dropPosition = targetData
-            sourceRowNumber = int(selection.get_text())
-            if sourceRowNumber == targetPath[0]:
-                # dropped on itself
-                dragContext.finish(False, False, eventtime)
-            else:
-                # The continer must have a getChildren() and a setChildren()
-                # method as for example the album class has.
-                container = self._objectCollection.getContainer()
-                children = list(container.getChildren())
-                sourceRow = model[sourceRowNumber]
-                targetIter = model.get_iter(targetPath)
-                objectSelection = self._objectCollection.getObjectSelection()
-                if (dropPosition == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE
-                    or dropPosition == gtk.TREE_VIEW_DROP_BEFORE):
-                    container.setChildren(self.__moveListItem(children,
-                                                              sourceRowNumber,
-                                                              targetPath[0]))
-                    model.insert_before(sibling=targetIter, row=sourceRow)
-                    model.remove(sourceRow.iter)
-                    # TODO update the album tree widget?
-                elif (dropPosition == gtk.TREE_VIEW_DROP_INTO_OR_AFTER
-                      or dropPosition == gtk.TREE_VIEW_DROP_AFTER):
-                    container.setChildren(self.__moveListItem(children,
-                                                              sourceRowNumber,
-                                                              targetPath[0] + 1))
-                    model.insert_after(sibling=targetIter, row=sourceRow)
-                    model.remove(sourceRow.iter)
-                    # TODO update the album tree widget?
-                objectSelection.setSelection([targetPath[0]])
-                # I've experienced that the drag-data-delete signal isn't
-                # always emitted when I drag & drop rapidly in the TreeView.
-                # And when it is missing the source row is not removed as is
-                # should. It is probably an bug in gtk+ (or maybe in pygtk).
-                # It only happens sometimes and I have not managed to reproduce
-                # it with a simpler example. Hence we remove the row ourself
-                # and are not relying on the drag-data-delete-signal.
-                # http://bugzilla.gnome.org/show_bug.cgi?id=134997
-                removeSourceRowAutomatically = False
-                dragContext.finish(True, removeSourceRowAutomatically, eventtime)
-
-    def _viewColumnToggled(self, checkMenuItem, columnName):
-        if checkMenuItem.get_active():
-            if columnName not in self.__createdColumns:
-                self.__createColumn(columnName,
-                                    self._objectCollection.getObjectMetadataMap())
-                # The correct columnLocation is stored when the column is removed
-                # there is no need to store the location when it is created
-                # since the column order may be reordered later before it is removed.
-        else:
-            # Since the column has been removed explicitly by the user
-            # we dont store the column's relative location.
-            try:
-                del self.__userChosenColumns[columnName]
-            except KeyError:
-                pass
-            if columnName in self.__createdColumns:
-                self.__removeColumn(columnName)
-
-###############################################################################
-### Private
-
-    def __createColumn(self, columnName, objectMetadataMap, location=-1):
-        (objtype, column, editedCallback, editedCallbackData) = objectMetadataMap[columnName]
-        if objtype == gtk.gdk.Pixbuf:
-            renderer = gtk.CellRendererPixbuf()
-            column = gtk.TreeViewColumn(columnName, renderer, pixbuf=column)
-            env.debug("Created a PixBuf column for " + columnName)
-        elif objtype == gobject.TYPE_STRING or objtype == gobject.TYPE_INT:
-            renderer = gtk.CellRendererText()
-            column = gtk.TreeViewColumn(columnName,
-                                        renderer,
-                                        text=column,
-                                        editable=ObjectCollection.COLUMN_ROW_EDITABLE)
-            column.set_resizable(True)
-            if editedCallback:
-                cid = renderer.connect("edited",
-                                       editedCallback,
-                                       column,
-                                       editedCallbackData)
-                self.__editedCallbacks[columnName] = (cid, renderer)
-                env.debug("Created a Text column with editing callback for " + columnName)
-            else:
-                env.debug("Created a Text column without editing callback for " + columnName)
-        else:
-            print "Warning, unsupported type for column ", columnName
-            return
-        column.set_reorderable(True)
-        self._viewWidget.insert_column(column, location)
-        self.__createdColumns[columnName] = column
-        return column
-
-    def __removeColumn(self, columnName):
-        column = self.__createdColumns[columnName]
-        self._viewWidget.remove_column(column)
-        if columnName in self.__editedCallbacks:
-            (cid, renderer) = self.__editedCallbacks[columnName]
-            renderer.disconnect(cid)
-            del self.__editedCallbacks[columnName]
-        del self.__createdColumns[columnName]
-        column.destroy()
-        env.debug("Removed column " + columnName)
-
-    def __removeColumnsAndUpdateLocation(self, columnNames=None):
-       # Remove columns and store their relative locations for next time
-       # they are re-created.
-       columnLocation = 0
-       for column in self._viewWidget.get_columns():
-           columnName = column.get_title()
-           # TODO Store the column width and reuse it when the column is
-           #      recreated. I don't know how to store the width since
-           #      column.get_width() return correct values for columns
-           #      containing a gtk.CellRendererPixbuf but only 0 for all
-           #      columns containing a gtk.CellRendererText. It is probably
-           #      a bug in gtk och pygtk. I have not yet reported the bug.
-           if columnNames is None or columnName in columnNames:
-               if columnName in self.__createdColumns:
-                   self.__removeColumn(columnName)
-                   self.__userChosenColumns[columnName] = columnLocation
-           columnLocation += 1
-
-    def __moveListItem(self, list, currentIndex, newIndex):
-        if currentIndex == newIndex:
-            return list
-        if currentIndex < newIndex:
-            newIndex -= 1
-        movingChild = list[currentIndex]
-        del list[currentIndex]
-        return list[:newIndex] + [movingChild] + list[newIndex:]
diff --git a/src/gkofoto/taganddescriptiondialog.py b/src/gkofoto/taganddescriptiondialog.py
deleted file mode 100644 (file)
index e5c88d5..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-import gtk
-import string
-import re
-from environment import env
-from kofoto.shelf import *
-
-class TagAndDescriptionDialog:
-    def __init__(self, title, tagText=u"", descText=u""):
-        env.assertUnicode(tagText)
-        env.assertUnicode(descText)
-        self._widgets = gtk.glade.XML(env.gladeFile, "tagAndDescriptionDialog")
-        self._dialog = self._widgets.get_widget("tagAndDescriptionDialog")
-        self._dialog.set_title(title)
-        self._tagWidget = self._widgets.get_widget("tag")
-        self._tagWidget.set_text(tagText)
-        self._descWidget = self._widgets.get_widget("description")
-        self._descWidget.set_text(descText)
-        self._descWidget.connect("changed", self._descriptionChanged, self._tagWidget)
-        okbutton = self._widgets.get_widget("okbutton")
-        self._tagWidget.connect("changed", self._tagChanged, okbutton)
-        self.__descText = descText
-        okbutton.set_sensitive(self._isTagOkay(tagText))
-
-    def run(self, ok=None, data=None):
-        result = self._dialog.run()
-        tag = self._tagWidget.get_text().decode("utf-8")
-        desc = self._descWidget.get_text().decode("utf-8")
-        self._dialog.destroy()
-        if result == gtk.RESPONSE_OK:
-            if ok == None:
-                return None
-            else:
-                if data:
-                    return ok(tag, desc, data)
-                else:
-                    return ok(tag, desc)
-        else:
-            return None
-
-    def __generateTagName(self, descText):
-        env.assertUnicode(descText)
-        return re.sub(r"(?Lu)\W", "", descText).lower()
-
-    def __generateTagNameDeprecated1(self, descText):
-        # An algoritm for generating tag names used in previous gkofoto
-        # versions (2004-04-26 -- 2004-05-15). This algoritm
-        # must always remove all swedish characters, regardles of LOCAL
-        # or UNICODE setting, to be backward compatible with the old version.
-        env.assertUnicode(descText)
-        return re.sub("\W", "", descText)
-
-    def __generateTagNameDeprecated2(self, descText):
-        # An algoritm for generating tag names used in previous gkofoto
-        # versions (< 2004-04-26)
-        env.assertUnicode(descText)
-        return string.translate(descText.encode(env.codeset),
-                                string.maketrans("", ""),
-                                string.whitespace)
-
-    def _descriptionChanged(self, description, tag):
-        newDescText = description.get_text().decode("utf-8")
-        currentTagText = self._tagWidget.get_text()
-        if (currentTagText == self.__generateTagName(self.__descText) or
-            currentTagText == self.__generateTagNameDeprecated1(self.__descText) or
-            currentTagText == self.__generateTagNameDeprecated2(self.__descText)):
-            tag.set_text(self.__generateTagName(newDescText))
-        self.__descText = newDescText
-
-    def _tagChanged(self, tag, button):
-        tagString = tag.get_text().decode("utf-8")
-        button.set_sensitive(self._isTagOkay(tagString))
diff --git a/src/gkofoto/thumbnailview.py b/src/gkofoto/thumbnailview.py
deleted file mode 100644 (file)
index 16f7105..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-import gtk
-from gkofoto.objectcollectionview import *
-from gkofoto.objectcollection import *
-from environment import env
-
-class ThumbnailView(ObjectCollectionView):
-
-###############################################################################
-### Public
-
-    def __init__(self):
-        env.debug("Init ThumbnailView")
-##        ObjectCollectionView.__init__(self,
-##                                      env.widgets["thumbnailList"])
-        ObjectCollectionView.__init__(self,
-                                      env.widgets["thumbnailView"])
-        self.__currentMaxWidth = env.thumbnailSize[0]
-        self.__selectionLocked = False
-        return
-        self._viewWidget.connect("select_icon", self._widgetIconSelected)
-        self._viewWidget.connect("unselect_icon", self._widgetIconUnselected)
-
-    def importSelection(self, objectSelection):
-        if not self.__selectionLocked:
-            env.debug("ThumbnailView is importing selection.")
-            self.__selectionLocked = True
-  &