Use @property for properties
[joel/kofoto.git] / src / packages / kofoto / gkofoto / objectcollection.py
1 import os
2 import gtk
3 import gobject
4 import gc
5 import subprocess
6 from kofoto.shelfexceptions import BadAlbumTagError
7 from kofoto.timer import Timer
8 from kofoto.gkofoto.environment import env
9 from kofoto.gkofoto.objectselection import ObjectSelection
10 from kofoto.gkofoto.albumdialog import AlbumDialog
11 from kofoto.gkofoto.registerimagesdialog import RegisterImagesDialog
12 from kofoto.gkofoto.imageversionsdialog import ImageVersionsDialog
13 from kofoto.gkofoto.registerimageversionsdialog import \
14     RegisterImageVersionsDialog
15 from kofoto.gkofoto.duplicateandopenimagedialog import \
16     DuplicateAndOpenImageDialog
17 from kofoto.gkofoto.fullscreenwindow import FullScreenWindow
18 from kofoto.gkofoto.pseudothread import PseudoThread
19
20 class ObjectCollection(object):
21
22 ######################################################################
23 ### Public
24
25     def __init__(self):
26         env.debug("Init ObjectCollection")
27         self.__objectSelection = ObjectSelection(self)
28         self.__insertionPseudoThread = None
29         self.__registeredViews = []
30         self.__disabledFields = set()
31         self.__rowInsertedCallbacks = []
32         self.__columnsType = [ gobject.TYPE_BOOLEAN,  # COLUMN_VALID_LOCATION
33                                gobject.TYPE_BOOLEAN,  # COLUMN_VALID_CHECKSUM
34                                gobject.TYPE_BOOLEAN,  # COLUMN_ROW_EDITABLE
35                                gobject.TYPE_BOOLEAN,  # COLUMN_IS_ALBUM
36                                gobject.TYPE_INT,      # COLUMN_OBJECT_ID
37                                gobject.TYPE_STRING,   # COLUMN_LOCATION
38                                gtk.gdk.Pixbuf,        # COLUMN_THUMBNAIL
39                                gobject.TYPE_STRING,   # COLUMN_IMAGE_VERSIONS
40                                gobject.TYPE_STRING ]  # COLUMN_ALBUM_TAG
41         self.__objectMetadataMap = {
42             u"id"       :(gobject.TYPE_INT,    self.COLUMN_OBJECT_ID, None,                 None),
43             u"location" :(gobject.TYPE_STRING, self.COLUMN_LOCATION,  None,                 None),
44             u"thumbnail":(gtk.gdk.Pixbuf,      self.COLUMN_THUMBNAIL, None,                 None),
45             u"albumtag" :(gobject.TYPE_STRING, self.COLUMN_ALBUM_TAG, self._albumTagEdited, self.COLUMN_ALBUM_TAG),
46             u"versions" :(gobject.TYPE_STRING, self.COLUMN_IMAGE_VERSIONS, None,            None),
47             }
48         for name in env.shelf.getAllAttributeNames():
49             self.__addAttribute(name)
50         self.__treeModel = gtk.ListStore(*self.__columnsType)
51         self.__frozen = False
52         self.__nrOfAlbums = 0
53         self.__nrOfImages = 0
54
55     # Return true if the objects has a defined order and may
56     # be reordered. An object that is reorderable is not
57     # allowed to also be sortable.
58     def isReorderable(self):
59         return False
60
61     # Return true if the objects may be sorted.
62     def isSortable(self):
63         return False
64
65     # Return true if objects may be added and removed from the collection.
66     def isMutable(self):
67         return not self.isLoading()
68
69     # Return true if object collection has not finished loading.
70     def isLoading(self):
71         ipt = self.__insertionPseudoThread
72         return ipt and ipt.is_running()
73
74     def getCutLabel(self):
75         return "Cut reference"
76
77     def getCopyLabel(self):
78         return "Copy reference"
79
80     def getPasteLabel(self):
81         return "Paste reference"
82
83     def getDeleteLabel(self):
84         return "Delete reference"
85
86     def getDestroyLabel(self):
87         return "Destroy..."
88
89     def getCreateAlbumChildLabel(self):
90         return "Create album child..."
91
92     def getRegisterImagesLabel(self):
93         return "Register and add images..."
94
95     def getGenerateHtmlLabel(self):
96         return "Generate HTML..."
97
98     def getAlbumPropertiesLabel(self):
99         return "Album properties..."
100
101     def getOpenImageLabel(self):
102         return "Open image in external program..."
103
104     def getDuplicateAndOpenImageLabel(self):
105         return "Duplicate and open image in external program..."
106
107     def getRotateImageLeftLabel(self):
108         return "Rotate image left"
109
110     def getRotateImageRightLabel(self):
111         return "Rotate image right"
112
113     def getImageVersionsLabel(self):
114         return "Edit image versions..."
115
116     def getRegisterImageVersionsLabel(self):
117         return "Register image versions..."
118
119     def getMergeImagesLabel(self):
120         return "Merge images..."
121
122     def getDestroyNonPrimaryImageVersionsLabel(self):
123         return "Destroy non-primary image versions..."
124
125     def getObjectMetadataMap(self):
126         return self.__objectMetadataMap
127
128     def getModel(self):
129         return self.__treeModel
130
131     def getUnsortedModel(self):
132         return self.__treeModel
133
134     def addInsertedRowCallback(self, callback, data=None):
135         self.__rowInsertedCallbacks.append((callback, data))
136
137     def removeInsertedRowCallback(self, callback, data=None):
138         self.__rowInsertedCallbacks.remove((callback, data))
139
140     def signalRowInserted(self):
141         for callback, data in self.__rowInsertedCallbacks:
142             callback(data)
143
144     def convertToUnsortedRowNr(self, rowNr):
145         return rowNr
146
147     def convertFromUnsortedRowNr(self, unsortedRowNr):
148         return unsortedRowNr
149
150     def getObjectSelection(self):
151         return self.__objectSelection
152
153     def getDisabledFields(self):
154         return self.__disabledFields
155
156     def registerView(self, view):
157         env.debug("Register view to object collection")
158         self.__registeredViews.append(view)
159
160     def unRegisterView(self, view):
161         env.debug("Unregister view from object collection")
162         self.__registeredViews.remove(view)
163
164     def reloadSingleObjectView(self):
165         for view in self.__registeredViews:
166             view._reloadSingleObjectView()
167
168     def clear(self, freeze=True):
169         env.debug("Clearing object collection")
170         if freeze:
171             self._freezeViews()
172         if self.isLoading():
173             self.__loadingFinished()
174         self.__treeModel.clear()
175         gc.collect()
176         self.__nrOfAlbums = 0
177         self.__nrOfImages = 0
178         self._handleNrOfObjectsUpdate()
179         self.__objectSelection.unselectAll()
180         if freeze:
181             self._thawViews()
182
183     def cut(self, *unused):
184         raise Exception("Error. Not allowed to cut objects into objectCollection.") # TODO
185
186     def copy(self, *unused):
187         env.clipboard.setObjects(self.__objectSelection.getSelectedObjects())
188
189     def paste(self, *unused):
190         raise Exception("Error. Not allowed to paste objects into objectCollection.") # TODO
191
192     def delete(self, *unused):
193         raise Exception("Error. Not allowed to delete objects from objectCollection.") # TODO
194
195     def destroy(self, *unused):
196         model = self.getModel()
197
198         albumsSelected = False
199         imagesSelected = False
200         for position in self.__objectSelection:
201             iterator = model.get_iter(position)
202             isAlbum = model.get_value(
203                 iterator, self.COLUMN_IS_ALBUM)
204             if isAlbum:
205                 albumsSelected = True
206             else:
207                 imagesSelected = True
208
209         assert albumsSelected ^ imagesSelected
210
211         self._freezeViews()
212         if albumsSelected:
213             dialogId = "destroyAlbumsDialog"
214         else:
215             dialogId = "destroyImagesDialog"
216         widgets = gtk.glade.XML(env.gladeFile, dialogId)
217         dialog = widgets.get_widget(dialogId)
218         result = dialog.run()
219         albumDestroyed = False
220         if result == gtk.RESPONSE_OK:
221             if albumsSelected:
222                 deleteFiles = False
223             else:
224                 checkbutton = widgets.get_widget("deleteImageFilesCheckbutton")
225                 deleteFiles = checkbutton.get_active()
226             objectIds = set()
227             # Create a Set to avoid duplicated objects.
228             for obj in set(self.__objectSelection.getSelectedObjects()):
229                 if deleteFiles and not obj.isAlbum():
230                     for iv in obj.getImageVersions():
231                         try:
232                             os.remove(iv.getLocation())
233                             # TODO: Delete from image cache too?
234                         except OSError:
235                             pass
236                 env.clipboard.removeObjects(obj)
237                 env.shelf.deleteObject(obj.getId())
238                 objectIds.add(obj.getId())
239                 if obj.isAlbum():
240                     albumDestroyed = True
241             self.getObjectSelection().unselectAll()
242             unsortedModel = self.getUnsortedModel()
243             locations = [row.path for row in unsortedModel
244                          if row[ObjectCollection.COLUMN_OBJECT_ID] in objectIds]
245             for loc in sorted(locations, reverse=True):
246                 del unsortedModel[loc]
247         dialog.destroy()
248         if albumDestroyed:
249             env.mainwindow.reloadAlbumTree()
250         self._thawViews()
251
252     COLUMN_VALID_LOCATION = 0
253     COLUMN_VALID_CHECKSUM = 1
254     COLUMN_ROW_EDITABLE   = 2
255     COLUMN_IS_ALBUM       = 3
256
257     # Columns visible to user
258     COLUMN_OBJECT_ID      = 4
259     COLUMN_LOCATION       = 5
260     COLUMN_THUMBNAIL      = 6
261     COLUMN_IMAGE_VERSIONS = 7
262     COLUMN_ALBUM_TAG      = 8
263
264     # Content in objectMetadata fields
265     TYPE                 = 0
266     COLUMN_NR            = 1
267     EDITED_CALLBACK      = 2
268     EDITED_CALLBACK_DATA = 3
269
270
271
272 ######################################################################
273 ### Only for subbclasses
274
275     def _getRegisteredViews(self):
276         return self.__registeredViews
277
278     def _loadObjectList(self, objectList):
279         env.enter("Object collection loading objects.")
280         self._freezeViews()
281         self.clear(False)
282         self._insertObjectList(objectList)
283         self._thawViews()
284         env.exit("Object collection loading objects. (albums=" + str(self.__nrOfAlbums) + " images=" + str(self.__nrOfImages) + ")")
285
286     def _insertObjectList(self, objectList, location=None):
287         # location = None means insert last, otherwise insert before
288         # location.
289         #
290         # Note that this method does NOT update objectSelection.
291
292         if location == None:
293             location = len(self.__treeModel)
294         self.__insertionPseudoThread = PseudoThread(
295             self.__insertionWorker(objectList, location))
296         self.__insertionPseudoThread.start()
297
298     def __insertionWorker(self, objectList, location):
299         timer = Timer()
300         for obj in objectList:
301             self._freezeViews()
302
303 #            self.__treeModel.insert(location)
304 # Work-around for bug 171027 in PyGTK 2.6.1:
305             if location >= len(self.__treeModel):
306                 iterator = self.__treeModel.append()
307             else:
308                 iterator = self.__treeModel.insert_before(
309                     self.__treeModel[location].iter)
310 # End work-around.
311
312             self.__treeModel.set_value(iterator, self.COLUMN_OBJECT_ID, obj.getId())
313             if obj.isAlbum():
314                 self.__treeModel.set_value(iterator, self.COLUMN_IS_ALBUM, True)
315                 self.__treeModel.set_value(iterator, self.COLUMN_ALBUM_TAG, obj.getTag())
316                 self.__treeModel.set_value(iterator, self.COLUMN_LOCATION, None)
317                 self.__treeModel.set_value(iterator, self.COLUMN_IMAGE_VERSIONS, "")
318                 self.__nrOfAlbums += 1
319             else:
320                 if obj.getPrimaryVersion():
321                     ivlocation = obj.getPrimaryVersion().getLocation()
322                 else:
323                     ivlocation = None
324                 imageVersions = list(obj.getImageVersions())
325                 if len(imageVersions) > 1:
326                     imageVersionsText = str(len(imageVersions))
327                 else:
328                     imageVersionsText = ""
329                 self.__treeModel.set_value(iterator, self.COLUMN_IS_ALBUM, False)
330                 self.__treeModel.set_value(iterator, self.COLUMN_ALBUM_TAG, None)
331                 self.__treeModel.set_value(iterator, self.COLUMN_LOCATION, ivlocation)
332                 self.__treeModel.set_value(iterator, self.COLUMN_IMAGE_VERSIONS, imageVersionsText)
333                 self.__nrOfImages += 1
334                 # TODO Set COLUMN_VALID_LOCATION and COLUMN_VALID_CHECKSUM
335             for attribute, value in obj.getAttributeMap().items():
336                 if "@" + attribute in self.__objectMetadataMap:
337                     column = self.__objectMetadataMap["@" + attribute][self.COLUMN_NR]
338                     self.__treeModel.set_value(iterator, column, value)
339             self.__treeModel.set_value(iterator, self.COLUMN_ROW_EDITABLE, True)
340             self._thawViews()
341             self.signalRowInserted()
342             self.__loadThumbnail(self.__treeModel, iterator)
343             location += 1
344             self.__updateObjectCount(True)
345             if timer.get() > 0.05:
346                 yield True
347                 timer.reset()
348
349         self._handleNrOfObjectsUpdate()
350         self.__loadingFinished()
351         yield False
352
353     def __loadingFinished(self):
354         self.__updateObjectCount(False)
355         for view in self.__registeredViews:
356             view.loadingFinished()
357         self.__insertionPseudoThread.stop()
358         self.__insertionPseudoThread = None
359
360     def __updateObjectCount(self, loadingInProgress):
361         env.widgets["statusbarLoadedObjects"].pop(1)
362         if loadingInProgress:
363             text = "%d objects (and counting...)" % len(self.__treeModel)
364         else:
365             text = "%d objects" % len(self.__treeModel)
366         env.widgets["statusbarLoadedObjects"].push(1, text)
367
368     def _handleNrOfObjectsUpdate(self):
369         updatedDisabledFields = set()
370         if self.__nrOfAlbums == 0:
371             updatedDisabledFields.add(u"albumtag")
372         if self.__nrOfImages == 0:
373             updatedDisabledFields.add(u"location")
374         for view in self.__registeredViews:
375             view.fieldsDisabled(updatedDisabledFields - self.__disabledFields)
376             view.fieldsEnabled(self.__disabledFields - updatedDisabledFields)
377         self.__disabledFields = updatedDisabledFields
378         env.debug("The following fields are disabled: " + str(self.__disabledFields))
379
380     def _getTreeModel(self):
381         return self.__treeModel
382
383     def _freezeViews(self):
384         if self.__frozen:
385             return
386         for view in self.__registeredViews:
387             view.freeze()
388         self.__frozen = True
389
390     def _thawViews(self):
391         if not self.__frozen:
392             return
393         for view in self.__registeredViews:
394             view.thaw()
395         self.__frozen = False
396
397
398 ###############################################################################
399 ### Callback functions
400
401     def _attributeEdited(self, unused1, path, value, unused2, attributeName):
402         model = self.getModel()
403         columnNumber = self.__objectMetadataMap["@" + attributeName][self.COLUMN_NR]
404         iterator = model.get_iter(path)
405         oldValue = model.get_value(iterator, columnNumber)
406         if not oldValue:
407             oldValue = u""
408         value = unicode(value, "utf-8")
409         if oldValue != value:
410             # TODO Show dialog and ask for confirmation?
411             objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
412             obj = env.shelf.getObject(objectId)
413             obj.setAttribute(attributeName, value)
414             model.set_value(iterator, columnNumber, value)
415             env.debug("Object attribute edited")
416
417     def _albumTagEdited(self, unused1, path, value, unused2, columnNumber):
418         model = self.getModel()
419         iterator = model.get_iter(path)
420         assert model.get_value(iterator, self.COLUMN_IS_ALBUM)
421         oldValue = model.get_value(iterator, columnNumber)
422         if not oldValue:
423             oldValue = u""
424         value = unicode(value, "utf-8")
425         if oldValue != value:
426             # TODO Show dialog and ask for confirmation?
427             objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
428             obj = env.shelf.getAlbum(objectId)
429             try:
430                 obj.setTag(value)
431             except BadAlbumTagError:
432                 dialog = gtk.MessageDialog(
433                     type=gtk.MESSAGE_ERROR,
434                     buttons=gtk.BUTTONS_OK,
435                     message_format="Bad album tag: \"%s\"" % value)
436                 dialog.run()
437                 dialog.destroy()
438                 value = oldValue
439             model.set_value(iterator, columnNumber, value)
440             # TODO Update the album tree widget.
441             env.debug("Album tag edited")
442
443     def reloadSelectedRows(self):
444         model = self.getUnsortedModel()
445         for (rowNr, obj) in self.__objectSelection.getMap().items():
446             if not obj.isAlbum():
447                 self.__loadThumbnail(model, model.get_iter(rowNr))
448                 imageVersions = list(obj.getImageVersions())
449                 if len(imageVersions) > 1:
450                     imageVersionsText = str(len(imageVersions))
451                 else:
452                     imageVersionsText = ""
453                 model.set_value(model.get_iter(rowNr), self.COLUMN_IMAGE_VERSIONS, imageVersionsText)
454
455     def createAlbumChild(self, *unused):
456         dialog = AlbumDialog("Create album")
457         dialog.run(self._createAlbumChildHelper)
458
459     def _createAlbumChildHelper(self, tag, desc):
460         newAlbum = env.shelf.createAlbum(tag)
461         if len(desc) > 0:
462             newAlbum.setAttribute(u"title", desc)
463         selectedObjects = self.__objectSelection.getSelectedObjects()
464         selectedAlbum = selectedObjects[0]
465         children = list(selectedAlbum.getChildren())
466         children.append(newAlbum)
467         selectedAlbum.setChildren(children)
468         env.mainwindow.reloadAlbumTree()
469
470     def registerAndAddImages(self, *unused):
471         selectedObjects = self.__objectSelection.getSelectedObjects()
472         assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
473         selectedAlbum = selectedObjects[0]
474         dialog = RegisterImagesDialog(selectedAlbum)
475         if dialog.run() == gtk.RESPONSE_OK:
476             env.mainwindow.reload() # TODO: Don't reload everything.
477         dialog.destroy()
478
479     def generateHtml(self, *unused):
480         selectedObjects = self.__objectSelection.getSelectedObjects()
481         assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
482         selectedAlbum = selectedObjects[0]
483         env.mainwindow.generateHtml(selectedAlbum)
484
485     def albumProperties(self, *unused):
486         selectedObjects = self.__objectSelection.getSelectedObjects()
487         assert len(selectedObjects) == 1 and selectedObjects[0].isAlbum()
488         selectedAlbumId = selectedObjects[0].getId()
489         dialog = AlbumDialog("Edit album", selectedAlbumId)
490         dialog.run(self._albumPropertiesHelper)
491
492     def _albumPropertiesHelper(self, tag, desc):
493         selectedObjects = self.__objectSelection.getSelectedObjects()
494         selectedAlbum = selectedObjects[0]
495         selectedAlbum.setTag(tag)
496         if len(desc) > 0:
497             selectedAlbum.setAttribute(u"title", desc)
498         else:
499             selectedAlbum.deleteAttribute(u"title")
500         env.mainwindow.reloadAlbumTree()
501         # TODO: Update objectCollection.
502
503     def imageVersions(self, *unused):
504         selectedObjects = self.__objectSelection.getSelectedObjects()
505         assert len(selectedObjects) == 1
506         dialog = ImageVersionsDialog(self)
507         dialog.runViewImageVersions(selectedObjects[0])
508         self.reloadSingleObjectView()
509
510     def registerImageVersions(self, *unused):
511         selectedObjects = self.__objectSelection.getSelectedObjects()
512         assert len(selectedObjects) == 1
513         dialog = RegisterImageVersionsDialog(self)
514         dialog.run(selectedObjects[0])
515         self.reloadSingleObjectView()
516
517     def mergeImages(self, *unused):
518         selectedObjects = self.__objectSelection.getSelectedObjects()
519         assert len(selectedObjects) > 1
520         dialog = ImageVersionsDialog(self)
521         dialog.runMergeImages(selectedObjects)
522
523     def destroyNonPrimaryImageVersions(self, *unused):
524         selectedImages = [
525             x
526             for x in self.__objectSelection.getSelectedObjects()
527             if not x.isAlbum()]
528         assert len(selectedImages) > 0
529         dialogId = "destroyNonPrimaryImageVersionsDialog"
530         widgets = gtk.glade.XML(env.gladeFile, dialogId)
531         dialog = widgets.get_widget(dialogId)
532         result = dialog.run()
533         if result == gtk.RESPONSE_OK:
534             checkbutton = widgets.get_widget("deleteImageFilesCheckbutton")
535             deleteFiles = checkbutton.get_active()
536             for image in selectedImages:
537                 for iv in image.getImageVersions():
538                     if not iv.isPrimary():
539                         if deleteFiles:
540                             try:
541                                 os.remove(iv.getLocation())
542                                 # TODO: Delete from image cache too?
543                             except OSError:
544                                 pass
545                         env.shelf.deleteImageVersion(iv.getId())
546             self.reloadSingleObjectView()
547             self.reloadSelectedRows()
548         dialog.destroy()
549
550     def rotateImage(self, unused, angle):
551         for (rowNr, obj) in self.__objectSelection.getMap().items():
552             if not obj.isAlbum():
553                 imageversion = obj.getPrimaryVersion()
554                 if not imageversion:
555                     # Image has no versions. Skip it for now.
556                     continue
557                 location = imageversion.getLocation()
558                 env.pixbufLoader.unload_all(location)
559                 if angle == 90:
560                     commandString = env.rotateRightCommand
561                 else:
562                     commandString = env.rotateLeftCommand
563                 command = commandString % { "location":location }
564                 try:
565                     result = subprocess.call(command, shell=True)
566                 except OSError:
567                     result = 1
568                 if result != 0:
569                     dialog = gtk.MessageDialog(
570                         type=gtk.MESSAGE_ERROR,
571                         buttons=gtk.BUTTONS_OK,
572                         message_format="Failed to execute command: \"%s\"" % command)
573                     dialog.run()
574                     dialog.destroy()
575                 else:
576                     imageversion.contentChanged()
577                     model = self.getUnsortedModel()
578                     self.__loadThumbnail(model, model.get_iter(rowNr))
579         self.reloadSingleObjectView()
580
581     def rotateImageLeft(self, widget, *unused):
582         self.rotateImage(widget, 270)
583
584     def rotateImageRight(self, widget, *unused):
585         self.rotateImage(widget, 90)
586
587     def openImage(self, *unused):
588         locations = ""
589         for obj in self.__objectSelection.getSelectedObjects():
590             if not obj.isAlbum():
591                 imageversion = obj.getPrimaryVersion()
592                 if not imageversion:
593                     # Image has no versions. Skip it for now.
594                     continue
595                 location = imageversion.getLocation()
596                 locations += location + " "
597         if locations != "":
598             command = env.openCommand % { "locations":locations }
599             try:
600                 result = subprocess.call(command, shell=True)
601             except OSError:
602                 result = 1
603             if result != 0:
604                 dialog = gtk.MessageDialog(
605                     type=gtk.MESSAGE_ERROR,
606                     buttons=gtk.BUTTONS_OK,
607                     message_format="Failed to execute command: \"%s\"" % command)
608                 dialog.run()
609                 dialog.destroy()
610
611     def duplicateAndOpenImage(self, *unused):
612         selectedObjects = self.__objectSelection.getSelectedObjects()
613         assert len(selectedObjects) == 1
614         assert not selectedObjects[0].isAlbum()
615         dialog = DuplicateAndOpenImageDialog()
616         dialog.run(selectedObjects[0].getPrimaryVersion())
617
618     def fullScreen(self):
619         imageVersions = []
620         if len(self.__objectSelection) > 1:
621             for obj in self.__objectSelection.getSelectedObjects():
622                 if not obj.isAlbum():
623                     imageVersions.append(obj.getPrimaryVersion())
624             window = FullScreenWindow(imageVersions)
625         else:
626             index = self.__objectSelection.getLowestSelectedRowNr()
627             if index is None:
628                 index = 0
629             current_row = 0
630             nr_of_albums_before_index = 0
631             for row in self.getModel():
632                 objectId = row[self.COLUMN_OBJECT_ID]
633                 obj = env.shelf.getObject(objectId)
634                 if not obj.isAlbum():
635                     imageVersions.append(obj.getPrimaryVersion())
636                 elif index > current_row:
637                     nr_of_albums_before_index += 1
638                 current_row += 1
639             window = FullScreenWindow(imageVersions,
640                                       index - nr_of_albums_before_index)
641         window.show_all()
642
643 ######################################################################
644 ### Private
645
646     def __addAttribute(self, name):
647         self.__objectMetadataMap["@" + name] = (gobject.TYPE_STRING,
648                                                 len(self.__columnsType),
649                                                 self._attributeEdited,
650                                                 name)
651         self.__columnsType.append(gobject.TYPE_STRING)
652
653     def __loadThumbnail(self, model, iterator):
654         objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
655         obj = env.shelf.getObject(objectId)
656         if obj.isAlbum():
657             pixbuf = env.albumIconPixbuf
658         elif not obj.getPrimaryVersion():
659             pixbuf = env.unknownImageIconPixbuf
660         else:
661             try:
662                 thumbnailLocation = env.imageCache.get(
663                     obj.getPrimaryVersion(),
664                     env.thumbnailSize[0],
665                     env.thumbnailSize[1])[0]
666                 pixbuf = gtk.gdk.pixbuf_new_from_file(thumbnailLocation)
667                 # TODO Set and use COLUMN_VALID_LOCATION and COLUMN_VALID_CHECKSUM
668             except IOError:
669                 pixbuf = env.unknownImageIconPixbuf
670         model.set_value(iterator, self.COLUMN_THUMBNAIL, pixbuf)