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
20 class ObjectCollection(object):
22 ######################################################################
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),
48 for name in env.shelf.getAllAttributeNames():
49 self.__addAttribute(name)
50 self.__treeModel = gtk.ListStore(*self.__columnsType)
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):
61 # Return true if the objects may be sorted.
65 # Return true if objects may be added and removed from the collection.
67 return not self.isLoading()
69 # Return true if object collection has not finished loading.
71 ipt = self.__insertionPseudoThread
72 return ipt and ipt.is_running()
74 def getCutLabel(self):
75 return "Cut reference"
77 def getCopyLabel(self):
78 return "Copy reference"
80 def getPasteLabel(self):
81 return "Paste reference"
83 def getDeleteLabel(self):
84 return "Delete reference"
86 def getDestroyLabel(self):
89 def getCreateAlbumChildLabel(self):
90 return "Create album child..."
92 def getRegisterImagesLabel(self):
93 return "Register and add images..."
95 def getGenerateHtmlLabel(self):
96 return "Generate HTML..."
98 def getAlbumPropertiesLabel(self):
99 return "Album properties..."
101 def getOpenImageLabel(self):
102 return "Open image in external program..."
104 def getDuplicateAndOpenImageLabel(self):
105 return "Duplicate and open image in external program..."
107 def getRotateImageLeftLabel(self):
108 return "Rotate image left"
110 def getRotateImageRightLabel(self):
111 return "Rotate image right"
113 def getImageVersionsLabel(self):
114 return "Edit image versions..."
116 def getRegisterImageVersionsLabel(self):
117 return "Register image versions..."
119 def getMergeImagesLabel(self):
120 return "Merge images..."
122 def getDestroyNonPrimaryImageVersionsLabel(self):
123 return "Destroy non-primary image versions..."
125 def getObjectMetadataMap(self):
126 return self.__objectMetadataMap
129 return self.__treeModel
131 def getUnsortedModel(self):
132 return self.__treeModel
134 def addInsertedRowCallback(self, callback, data=None):
135 self.__rowInsertedCallbacks.append((callback, data))
137 def removeInsertedRowCallback(self, callback, data=None):
138 self.__rowInsertedCallbacks.remove((callback, data))
140 def signalRowInserted(self):
141 for callback, data in self.__rowInsertedCallbacks:
144 def convertToUnsortedRowNr(self, rowNr):
147 def convertFromUnsortedRowNr(self, unsortedRowNr):
150 def getObjectSelection(self):
151 return self.__objectSelection
153 def getDisabledFields(self):
154 return self.__disabledFields
156 def registerView(self, view):
157 env.debug("Register view to object collection")
158 self.__registeredViews.append(view)
160 def unRegisterView(self, view):
161 env.debug("Unregister view from object collection")
162 self.__registeredViews.remove(view)
164 def reloadSingleObjectView(self):
165 for view in self.__registeredViews:
166 view._reloadSingleObjectView()
168 def clear(self, freeze=True):
169 env.debug("Clearing object collection")
173 self.__loadingFinished()
174 self.__treeModel.clear()
176 self.__nrOfAlbums = 0
177 self.__nrOfImages = 0
178 self._handleNrOfObjectsUpdate()
179 self.__objectSelection.unselectAll()
183 def cut(self, *unused):
184 raise Exception("Error. Not allowed to cut objects into objectCollection.") # TODO
186 def copy(self, *unused):
187 env.clipboard.setObjects(self.__objectSelection.getSelectedObjects())
189 def paste(self, *unused):
190 raise Exception("Error. Not allowed to paste objects into objectCollection.") # TODO
192 def delete(self, *unused):
193 raise Exception("Error. Not allowed to delete objects from objectCollection.") # TODO
195 def destroy(self, *unused):
196 model = self.getModel()
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)
205 albumsSelected = True
207 imagesSelected = True
209 assert albumsSelected ^ imagesSelected
213 dialogId = "destroyAlbumsDialog"
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:
224 checkbutton = widgets.get_widget("deleteImageFilesCheckbutton")
225 deleteFiles = checkbutton.get_active()
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():
232 os.remove(iv.getLocation())
233 # TODO: Delete from image cache too?
236 env.clipboard.removeObjects(obj)
237 env.shelf.deleteObject(obj.getId())
238 objectIds.add(obj.getId())
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]
249 env.mainwindow.reloadAlbumTree()
252 COLUMN_VALID_LOCATION = 0
253 COLUMN_VALID_CHECKSUM = 1
254 COLUMN_ROW_EDITABLE = 2
257 # Columns visible to user
261 COLUMN_IMAGE_VERSIONS = 7
264 # Content in objectMetadata fields
268 EDITED_CALLBACK_DATA = 3
272 ######################################################################
273 ### Only for subbclasses
275 def _getRegisteredViews(self):
276 return self.__registeredViews
278 def _loadObjectList(self, objectList):
279 env.enter("Object collection loading objects.")
282 self._insertObjectList(objectList)
284 env.exit("Object collection loading objects. (albums=" + str(self.__nrOfAlbums) + " images=" + str(self.__nrOfImages) + ")")
286 def _insertObjectList(self, objectList, location=None):
287 # location = None means insert last, otherwise insert before
290 # Note that this method does NOT update objectSelection.
293 location = len(self.__treeModel)
294 self.__insertionPseudoThread = PseudoThread(
295 self.__insertionWorker(objectList, location))
296 self.__insertionPseudoThread.start()
298 def __insertionWorker(self, objectList, location):
300 for obj in objectList:
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()
308 iterator = self.__treeModel.insert_before(
309 self.__treeModel[location].iter)
312 self.__treeModel.set_value(iterator, self.COLUMN_OBJECT_ID, obj.getId())
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
320 if obj.getPrimaryVersion():
321 ivlocation = obj.getPrimaryVersion().getLocation()
324 imageVersions = list(obj.getImageVersions())
325 if len(imageVersions) > 1:
326 imageVersionsText = str(len(imageVersions))
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)
341 self.signalRowInserted()
342 self.__loadThumbnail(self.__treeModel, iterator)
344 self.__updateObjectCount(True)
345 if timer.get() > 0.05:
349 self._handleNrOfObjectsUpdate()
350 self.__loadingFinished()
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
360 def __updateObjectCount(self, loadingInProgress):
361 env.widgets["statusbarLoadedObjects"].pop(1)
362 if loadingInProgress:
363 text = "%d objects (and counting...)" % len(self.__treeModel)
365 text = "%d objects" % len(self.__treeModel)
366 env.widgets["statusbarLoadedObjects"].push(1, text)
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))
380 def _getTreeModel(self):
381 return self.__treeModel
383 def _freezeViews(self):
386 for view in self.__registeredViews:
390 def _thawViews(self):
391 if not self.__frozen:
393 for view in self.__registeredViews:
395 self.__frozen = False
398 ###############################################################################
399 ### Callback functions
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)
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")
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)
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)
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)
439 model.set_value(iterator, columnNumber, value)
440 # TODO Update the album tree widget.
441 env.debug("Album tag edited")
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))
452 imageVersionsText = ""
453 model.set_value(model.get_iter(rowNr), self.COLUMN_IMAGE_VERSIONS, imageVersionsText)
455 def createAlbumChild(self, *unused):
456 dialog = AlbumDialog("Create album")
457 dialog.run(self._createAlbumChildHelper)
459 def _createAlbumChildHelper(self, tag, desc):
460 newAlbum = env.shelf.createAlbum(tag)
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()
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.
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)
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)
492 def _albumPropertiesHelper(self, tag, desc):
493 selectedObjects = self.__objectSelection.getSelectedObjects()
494 selectedAlbum = selectedObjects[0]
495 selectedAlbum.setTag(tag)
497 selectedAlbum.setAttribute(u"title", desc)
499 selectedAlbum.deleteAttribute(u"title")
500 env.mainwindow.reloadAlbumTree()
501 # TODO: Update objectCollection.
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()
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()
517 def mergeImages(self, *unused):
518 selectedObjects = self.__objectSelection.getSelectedObjects()
519 assert len(selectedObjects) > 1
520 dialog = ImageVersionsDialog(self)
521 dialog.runMergeImages(selectedObjects)
523 def destroyNonPrimaryImageVersions(self, *unused):
526 for x in self.__objectSelection.getSelectedObjects()
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():
541 os.remove(iv.getLocation())
542 # TODO: Delete from image cache too?
545 env.shelf.deleteImageVersion(iv.getId())
546 self.reloadSingleObjectView()
547 self.reloadSelectedRows()
550 def rotateImage(self, unused, angle):
551 for (rowNr, obj) in self.__objectSelection.getMap().items():
552 if not obj.isAlbum():
553 imageversion = obj.getPrimaryVersion()
555 # Image has no versions. Skip it for now.
557 location = imageversion.getLocation()
558 env.pixbufLoader.unload_all(location)
560 commandString = env.rotateRightCommand
562 commandString = env.rotateLeftCommand
563 command = commandString % { "location":location }
565 result = subprocess.call(command, shell=True)
569 dialog = gtk.MessageDialog(
570 type=gtk.MESSAGE_ERROR,
571 buttons=gtk.BUTTONS_OK,
572 message_format="Failed to execute command: \"%s\"" % command)
576 imageversion.contentChanged()
577 model = self.getUnsortedModel()
578 self.__loadThumbnail(model, model.get_iter(rowNr))
579 self.reloadSingleObjectView()
581 def rotateImageLeft(self, widget, *unused):
582 self.rotateImage(widget, 270)
584 def rotateImageRight(self, widget, *unused):
585 self.rotateImage(widget, 90)
587 def openImage(self, *unused):
589 for obj in self.__objectSelection.getSelectedObjects():
590 if not obj.isAlbum():
591 imageversion = obj.getPrimaryVersion()
593 # Image has no versions. Skip it for now.
595 location = imageversion.getLocation()
596 locations += location + " "
598 command = env.openCommand % { "locations":locations }
600 result = subprocess.call(command, shell=True)
604 dialog = gtk.MessageDialog(
605 type=gtk.MESSAGE_ERROR,
606 buttons=gtk.BUTTONS_OK,
607 message_format="Failed to execute command: \"%s\"" % command)
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())
618 def fullScreen(self):
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)
626 index = self.__objectSelection.getLowestSelectedRowNr()
630 nr_of_albums_before_index = 0
631 for row in self.getModel():
632 objectId = row[self.COLUMN_OBJECT_ID]
633 obj = env.getShelf().getObject(objectId)
634 if not obj.isAlbum():
635 imageVersions.append(obj.getPrimaryVersion())
636 elif index > current_row:
637 nr_of_albums_before_index += 1
639 window = FullScreenWindow(imageVersions,
640 index - nr_of_albums_before_index)
643 ######################################################################
646 def __addAttribute(self, name):
647 self.__objectMetadataMap["@" + name] = (gobject.TYPE_STRING,
648 len(self.__columnsType),
649 self._attributeEdited,
651 self.__columnsType.append(gobject.TYPE_STRING)
653 def __loadThumbnail(self, model, iterator):
654 objectId = model.get_value(iterator, self.COLUMN_OBJECT_ID)
655 obj = env.shelf.getObject(objectId)
657 pixbuf = env.albumIconPixbuf
658 elif not obj.getPrimaryVersion():
659 pixbuf = env.unknownImageIconPixbuf
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
669 pixbuf = env.unknownImageIconPixbuf
670 model.set_value(iterator, self.COLUMN_THUMBNAIL, pixbuf)