Use list.sort's keyword parameters
[joel/kofoto.git] / src / packages / kofoto / gkofoto / imageversionslist.py
1 import os
2 import gtk
3 from kofoto.alternative import Alternative
4 from kofoto.shelf import ImageVersionType
5 from kofoto.gkofoto.environment import env
6 from kofoto.gkofoto.menuhandler import MenuGroup
7 from kofoto.gkofoto.imageversionsdialog import ImageVersionsDialog
8 from kofoto.gkofoto.duplicateandopenimagedialog import DuplicateAndOpenImageDialog
9 from kofoto.gkofoto.fullscreenwindow import FullScreenWindow
10
11 _imageVersionTypeToStringMap = {
12     ImageVersionType.Important: "Important",
13     ImageVersionType.Original: "Original",
14     ImageVersionType.Other: "Other",
15 }
16
17 _rotationDirection = Alternative("Left", "Right")
18
19 class ImageVersionsList(gtk.ScrolledWindow):
20     def __init__(self, singleObjectView):
21         gtk.ScrolledWindow.__init__(self)
22         self.__menuGroup = None
23         self.__imageWidgetToImageVersion = None
24         self.__imageWidgetList = None
25         self.__selectedImageWidgets = None
26         self.__singleObjectView = singleObjectView
27         self.__vbox = gtk.VBox()
28         self.__vbox.set_border_width(5)
29         self.__vbox.set_spacing(10)
30         self.__vbox.show()
31         self.add_with_viewport(self.__vbox)
32         self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
33         self.__image = None
34         self.__tooltips = gtk.Tooltips()
35         self.__recentlySelectedImageWidget = None
36         self.connect("focus-in-event", self.__focusInEventHandler_cb)
37         self.connect("focus-out-event", self.__focusOutEventHandler_cb)
38         self.__contextMenu = self.__createContextMenu()
39         self.clear()
40
41         callbacks = [
42             ("menubarViewImageVersion", self.__view_cb),
43             ("menubarViewImageVersionsFullScreen", self.__view_full_screen_cb),
44             ("menubarCopyImageVersionLocations", self.__copyImageLocation_cb),
45             ("menubarOpenImageVersions", self.__open_cb),
46             ("menubarDuplicateAndOpenImageVersion", self.__duplicateAndOpen_cb),
47             ("menubarRotateImageVersionLeft", self.__rotateLeft_cb),
48             ("menubarRotateImageVersionRight", self.__rotateRight_cb),
49             ("menubarSplitToIndependentImages", self.__split_cb),
50             ("menubarDestroyImageVersion", self.__destroy_cb),
51             ("menubarEditImageVersionProperties", self.__editProperties_cb),
52             ]
53         for widgetName, callback in callbacks:
54             env.widgets[widgetName].connect("activate", callback)
55
56     def clear(self):
57         for widget in self.__vbox.get_children():
58             self.__vbox.remove(widget)
59         self.__imageWidgetList = []
60         self.__imageWidgetToImageVersion = {}
61         self.__selectedImageWidgets = set()
62         self.__updateMenus()
63
64     def loadImage(self, image):
65         self.clear()
66         self.__image = image
67         self.__tooltips.enable()
68         for iv in image.getImageVersions():
69             vbox = gtk.VBox()
70             vbox.set_border_width(3)
71             self.__vbox.pack_start(vbox, expand=False, fill=False)
72             thumbnail = gtk.Image()
73             try:
74                 thumbnailLocation = env.imageCache.get(iv, 128, 128)[0]
75                 thumbnail.set_from_file(thumbnailLocation)
76             except (IOError, OSError):
77                 thumbnail.set_from_pixbuf(env.unknownImageIconPixbuf)
78             alignment = gtk.Alignment(0.5, 0.5, 0.5, 0.5)
79             alignment.add(thumbnail)
80             alignment.set_padding(5, 5, 5, 5)
81             eventbox = gtk.EventBox()
82             eventbox.add(alignment)
83             eventbox.connect(
84                 "button-press-event", self.__mouseButtonPressed_cb)
85             eventbox.connect_after(
86                 "expose-event", self.__imageWidgetExposed_cb)
87             tooltipText = "Location: " + iv.getLocation()
88             if iv.getComment():
89                 tooltipText += "\nComment: " + iv.getComment()
90             self.__tooltips.set_tip(eventbox, tooltipText)
91             vbox.add(eventbox)
92             if iv.isPrimary():
93                 vbox.add(gtk.Label("Primary"))
94             vbox.add(gtk.Label(_imageVersionTypeToStringMap[iv.getType()]))
95             self.__imageWidgetList.append(eventbox)
96             self.__imageWidgetToImageVersion[eventbox] = iv
97         self.__vbox.show_all()
98
99     def reload(self):
100         self.loadImage(self.__image)
101
102     def __createContextMenu(self):
103         menu = gtk.Menu()
104         menugroup = MenuGroup()
105         menugroup.addMenuItem(
106             "View",
107             self.__view_cb)
108         menugroup.addMenuItem(
109             "View in full screen mode",
110             self.__view_full_screen_cb)
111         menugroup.addMenuItem(
112             "Copy image version location(s)",
113             self.__copyImageLocation_cb)
114         menugroup.addStockImageMenuItem(
115             "Open image version(s) in external program...",
116             gtk.STOCK_OPEN,
117             self.__open_cb)
118         menugroup.addStockImageMenuItem(
119             "Duplicate and open image version(s) in external program...",
120             gtk.STOCK_OPEN,
121             self.__duplicateAndOpen_cb)
122         menugroup.addImageMenuItem(
123             "Rotate left",
124             os.path.join(env.iconDir, "rotateleft.png"),
125             self.__rotateLeft_cb)
126         menugroup.addImageMenuItem(
127             "Rotate right",
128             os.path.join(env.iconDir, "rotateright.png"),
129             self.__rotateRight_cb)
130         menugroup.addSeparator()
131         menugroup.addMenuItem(
132             "Split to independent image(s)",
133             self.__split_cb)
134         menugroup.addSeparator()
135         menugroup.addStockImageMenuItem(
136             "Destroy...",
137             gtk.STOCK_DELETE,
138             self.__destroy_cb)
139         menugroup.addStockImageMenuItem(
140             "Edit properties...",
141             gtk.STOCK_PROPERTIES,
142             self.__editProperties_cb)
143         for item in menugroup:
144             menu.add(item)
145         self.__menuGroup = menugroup
146         return menu
147
148     def __updateMenus(self):
149         zeroSelected = len(self.__selectedImageWidgets) == 0
150         oneSelected = len(self.__selectedImageWidgets) == 1
151         allSelected = (
152             len(self.__selectedImageWidgets) == len(self.__imageWidgetList))
153
154         env.widgets["menubarViewImageVersion"].set_sensitive(
155             oneSelected)
156         env.widgets["menubarViewImageVersionsFullScreen"].set_sensitive(
157             not zeroSelected)
158         env.widgets["menubarCopyImageVersionLocations"].set_sensitive(
159             not zeroSelected)
160         env.widgets["menubarOpenImageVersions"].set_sensitive(
161             not zeroSelected)
162         env.widgets["menubarDuplicateAndOpenImageVersion"].set_sensitive(
163             oneSelected)
164         env.widgets["menubarRotateImageVersionLeft"].set_sensitive(
165             not zeroSelected)
166         env.widgets["menubarRotateImageVersionRight"].set_sensitive(
167             not zeroSelected)
168         env.widgets["menubarSplitToIndependentImages"].set_sensitive(
169             not zeroSelected and not allSelected)
170         env.widgets["menubarDestroyImageVersion"].set_sensitive(
171             not zeroSelected)
172         env.widgets["menubarEditImageVersionProperties"].set_sensitive(
173             not zeroSelected)
174
175         if zeroSelected:
176             self.__menuGroup.disable()
177         else:
178             self.__menuGroup.enable()
179             if not oneSelected:
180                 self.__menuGroup["View"].set_sensitive(False)
181                 self.__menuGroup[
182                     ("Duplicate and open image version(s) in external"
183                      " program...")
184                     ].set_sensitive(False)
185             if allSelected:
186                 self.__menuGroup["Split to independent image(s)"].set_sensitive(False)
187
188     def __mouseButtonPressed_cb(self, widget, event):
189         def selectWidget(widget):
190             widget.set_state(gtk.STATE_SELECTED)
191             self.__selectedImageWidgets.add(widget)
192         def unselectWidget(widget):
193             widget.set_state(gtk.STATE_NORMAL)
194             self.__selectedImageWidgets.remove(widget)
195         def selectOnlyThis():
196             for x in list(self.__selectedImageWidgets):
197                 unselectWidget(x)
198             selectWidget(widget)
199         def selectThisToo():
200             selectWidget(widget)
201         def flipThis():
202             if widget in self.__selectedImageWidgets:
203                 unselectWidget(widget)
204             else:
205                 selectWidget(widget)
206         def extendSelection():
207             otherIndex = self.__imageWidgetList.index(
208                 self.__recentlySelectedImageWidget)
209             thisIndex = self.__imageWidgetList.index(widget)
210             for x in xrange(
211                 min(otherIndex, thisIndex), max(otherIndex, thisIndex) + 1):
212                 selectWidget(self.__imageWidgetList[x])
213
214         self.grab_focus()
215         if event.button == 1:
216             if event.type == gtk.gdk.BUTTON_PRESS:
217                 if event.state & gtk.gdk.CONTROL_MASK:
218                     flipThis()
219                 elif event.state & gtk.gdk.SHIFT_MASK:
220                     extendSelection()
221                 else:
222                     selectOnlyThis()
223             elif event.type == gtk.gdk._2BUTTON_PRESS:
224                 if (event.state & (
225                         gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK) == 0):
226                     self.__view_cb()
227         elif event.button == 3:
228             if widget in self.__selectedImageWidgets:
229                 selectThisToo()
230             else:
231                 selectOnlyThis()
232             self.__contextMenu.popup(
233                 None, None, None, event.button, event.time)
234         if self.__recentlySelectedImageWidget:
235             self.__recentlySelectedImageWidget.queue_draw()
236         self.__recentlySelectedImageWidget = widget
237         self.__updateMenus()
238
239     def __imageWidgetExposed_cb(self, widget, event):
240         if widget == self.__recentlySelectedImageWidget:
241             state = gtk.STATE_SELECTED
242             allocation = widget.get_allocation()
243             widget.style.paint_focus(
244                 widget.window, state, None, widget, "",
245                 2, 2, allocation.width - 4, allocation.height - 4)
246         else:
247             state = gtk.STATE_NORMAL
248
249     def __view_cb(self, *args):
250         assert len(self.__selectedImageWidgets) == 1
251         widget = list(self.__selectedImageWidgets)[0]
252         imageVersion = self.__imageWidgetToImageVersion[widget]
253         location = imageVersion.getLocation()
254         self.__singleObjectView._loadImageAtLocation(location)
255
256     def __view_full_screen_cb(self, *args):
257         assert len(self.__selectedImageWidgets) > 0
258         widgets = self.__getSelectedImageVersionsInOrder()
259         if len(widgets) > 1:
260             imageVersions = [
261                 self.__imageWidgetToImageVersion[x] for x in widgets]
262             window = FullScreenWindow(imageVersions)
263         else:
264             imageVersions = [
265                 self.__imageWidgetToImageVersion[x]
266                 for x in self.__imageWidgetList]
267             index = self.__imageWidgetList.index(widgets[0])
268             window = FullScreenWindow(imageVersions, index)
269         window.show_all()
270
271     def __copyImageLocation_cb(self, widget, param):
272         assert len(self.__selectedImageWidgets) > 0
273         clipboard = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD)
274         primary = gtk.clipboard_get(gtk.gdk.SELECTION_PRIMARY)
275         location = "\n".join(
276             [self.__imageWidgetToImageVersion[x].getLocation()
277              for x in self.__getSelectedImageVersionsInOrder()])
278         clipboard.set_text(location)
279         primary.set_text(location)
280
281     def __open_cb(self, *args):
282         assert len(self.__selectedImageWidgets) > 0
283         locations = [
284             self.__imageWidgetToImageVersion[x].getLocation()
285             for x in self.__getSelectedImageVersionsInOrder()]
286         command = env.openCommand % {"locations": " ".join(locations)}
287         result = os.system(command.encode(env.localeEncoding) + " &")
288         if result != 0:
289             dialog = gtk.MessageDialog(
290                 type=gtk.MESSAGE_ERROR,
291                 buttons=gtk.BUTTONS_OK,
292                 message_format="Failed to execute command: \"%s\"" % command)
293             dialog.run()
294             dialog.destroy()
295
296     def __duplicateAndOpen_cb(self, *args):
297         assert len(self.__selectedImageWidgets) == 1
298         imageWidget = list(self.__selectedImageWidgets)[0]
299         imageVersion = self.__imageWidgetToImageVersion[imageWidget]
300         dialog = DuplicateAndOpenImageDialog()
301         dialog.run(imageVersion)
302
303     def __split_cb(self, *args):
304         assert len(self.__selectedImageWidgets) > 0
305         assert len(self.__selectedImageWidgets) < len(self.__imageWidgetList)
306         for widget in self.__selectedImageWidgets:
307             imageVersion = self.__imageWidgetToImageVersion[widget]
308             image = env.shelf.createImage()
309             imageVersion.setImage(image)
310             for key, value in self.__image.getAttributeMap().items():
311                 image.setAttribute(key, value)
312             for category in  self.__image.getCategories():
313                 image.addCategory(category)
314         self.__singleObjectView.reload()
315
316     def __rotateLeft_cb(self, *args):
317         assert len(self.__selectedImageWidgets) > 0
318         self.__rotate(_rotationDirection.Left)
319
320     def __rotateRight_cb(self, *args):
321         assert len(self.__selectedImageWidgets) > 0
322         self.__rotate(_rotationDirection.Right)
323
324     def __destroy_cb(self, *args):
325         assert len(self.__selectedImageWidgets) > 0
326         widgets = gtk.glade.XML(env.gladeFile, "destroyImageVersionsDialog")
327         dialog = widgets.get_widget("destroyImageVersionsDialog")
328         result = dialog.run()
329         if result == gtk.RESPONSE_OK:
330             checkbutton = widgets.get_widget("deleteImageFilesCheckbutton")
331             deleteFiles = checkbutton.get_active()
332             for widget in self.__selectedImageWidgets:
333                 imageVersion = self.__imageWidgetToImageVersion[widget]
334                 if deleteFiles:
335                     try:
336                         os.remove(imageVersion.getLocation())
337                         # TODO: Delete from image cache too?
338                     except OSError:
339                         pass
340                 env.shelf.deleteImageVersion(imageVersion.getId())
341             self.__singleObjectView.reload()
342         dialog.destroy()
343
344     def __editProperties_cb(self, *args):
345         assert len(self.__selectedImageWidgets) > 0
346         dialog = ImageVersionsDialog(self.__singleObjectView._objectCollection)
347         dialog.runViewImageVersions(self.__image)
348         self.__singleObjectView.reload()
349
350     def __focusInEventHandler_cb(self, widget, event):
351         for x in self.__selectedImageWidgets:
352             x.set_state(gtk.STATE_SELECTED)
353
354     def __focusOutEventHandler_cb(self, widget, event):
355         for x in self.__selectedImageWidgets:
356             x.set_state(gtk.STATE_ACTIVE)
357
358     def __rotate(self, rotationDirection):
359         for widget in self.__selectedImageWidgets:
360             imageVersion = self.__imageWidgetToImageVersion[widget]
361             if rotationDirection == _rotationDirection.Left:
362                 rotateCommand = env.rotateLeftCommand
363             elif rotationDirection == _rotationDirection.Right:
364                 rotateCommand = env.rotateRightCommand
365             else:
366                 # Can't happen.
367                 assert False
368             location = imageVersion.getLocation()
369             env.pixbufLoader.unload_all(location)
370             command = rotateCommand % {"location": location}
371             result = os.system(command.encode(env.localeEncoding))
372             if result == 0:
373                 imageVersion.contentChanged()
374             else:
375                 dialog = gtk.MessageDialog(
376                     type=gtk.MESSAGE_ERROR,
377                     buttons=gtk.BUTTONS_OK,
378                     message_format="Failed to execute command: \"%s\"" % command)
379                 dialog.run()
380                 dialog.destroy()
381         self.__singleObjectView.reload()
382
383     def __getSelectedImageVersionsInOrder(self):
384         return [
385             x
386             for x in self.__imageWidgetList
387             if x in self.__selectedImageWidgets]