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