3528395e05e3c1be75e70a9c697d6075a353fa50
[joel/kofoto.git] / src / packages / kofoto / gkofoto / fullscreenwindow.py
1 """This module contains the FullScreenWindow class."""
2
3 __all__ = ["FullScreenWindow"]
4
5 import gtk
6 import gobject
7 import re
8 import string
9 from kofoto.gkofoto.imageview import ImageView
10 from kofoto.gkofoto.environment import env
11 from kofoto.shelf import CategoryDoesNotExistError
12
13 class FullScreenWindow(gtk.Window):
14     """A fullscreen window widget."""
15
16     def __init__(self, image_versions, current_index=0):
17         """Constructor.
18
19         Arguments:
20
21         image_versions -- A list of ImageVersion instance to display.
22         current_index  -- Where to start in image_versions.
23         """
24
25         self.__gobject_init__() # TODO: Use gtk.Window.__init__ in PyGTK 2.8.
26
27         self._last_allocated_size = None
28         self._image_versions = image_versions
29         self._current_index = current_index
30         self._latest_handle = None
31         self._latest_size = (0, 0)
32         self._selected_category_tag = None
33         self._last_selected_category_tag = None
34         self._show_image_categories = True
35         self._show_category_keys_info = False
36
37         bg_color = gtk.gdk.color_parse("#000000")
38         fg_color = gtk.gdk.color_parse("#999999")
39
40         eventbox = gtk.EventBox()
41         vbox = gtk.VBox()
42         eventbox.add(vbox)
43
44         # Add the image widget.
45         self._image_view = ImageView()
46         self._image_view.set_error_pixbuf(
47             gtk.gdk.pixbuf_new_from_file(env.unknownImageIconFileName))
48         self._image_view.modify_bg(gtk.STATE_NORMAL, bg_color)
49         vbox.pack_start(self._image_view)
50
51         # Add end screen label.
52         label = gtk.Label()
53         label.set_text(
54             "No more images.\nPress escape to get back to GKofoto.")
55         label.set_justify(gtk.JUSTIFY_CENTER)
56         label.modify_fg(gtk.STATE_NORMAL, fg_color)
57         vbox.pack_start(label)
58         self._end_screen_label = label
59
60         # Add image categories label.
61         label = gtk.Label()
62         label.modify_fg(gtk.STATE_NORMAL, fg_color)
63         vbox.pack_start(label, False)
64         self._image_categories_label = label
65
66         # Add category keys info label.
67         label = gtk.Label()
68         label.modify_fg(gtk.STATE_NORMAL, fg_color)
69         vbox.pack_start(label, False)
70         label.set_text(u"Assigned keys:")
71         self._category_keys_info_label = label
72         self._update_key_assignment_info()
73
74         # Add key assignment form.
75         self._key_assignment_hbox = gtk.HBox()
76         self._key_assignment_hbox.set_spacing(5)
77         vbox.pack_start(self._key_assignment_hbox, False)
78         label = gtk.Label("Assign last entered category to key (a-z):")
79         label.modify_fg(gtk.STATE_NORMAL, fg_color)
80         self._key_assignment_hbox.pack_start(label, False)
81         self._key_assignment_entry = gtk.Entry()
82         self._key_assignment_entry.connect(
83             "activate", self._key_assignment_entry_activate_cb)
84         self._key_assignment_hbox.pack_start(self._key_assignment_entry, False)
85
86         # Add categorization form.
87         self._categorization_hbox = gtk.HBox()
88         self._categorization_hbox.set_spacing(5)
89         vbox.pack_start(self._categorization_hbox, False)
90         label = gtk.Label("Category:")
91         label.modify_fg(gtk.STATE_NORMAL, fg_color)
92         self._categorization_hbox.pack_start(label, False)
93         self._category_entry = gtk.Entry()
94         self._category_entry.connect(
95             "activate", self._category_entry_activate_cb)
96         self._category_entry.connect(
97             "changed", self._category_entry_changed_cb)
98         self._categorization_hbox.pack_start(self._category_entry, False)
99         self._category_indicator_image = gtk.Image()
100         self._category_indicator_image.set_from_stock(
101             gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU)
102         self._categorization_hbox.pack_start(self._category_indicator_image, False)
103         self._matching_category_label = gtk.Label()
104         self._matching_category_label.set_line_wrap(True)
105         self._matching_category_label.modify_fg(gtk.STATE_NORMAL, fg_color)
106         self._categorization_hbox.pack_start(self._matching_category_label, True, True)
107
108         self.modify_bg(gtk.STATE_NORMAL, bg_color)
109         eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
110         self.add(eventbox)
111         self.set_modal(True)
112         self.set_default_size(400, 400)
113         self.fullscreen()
114         self.connect_after("map-event", self._after_map_event_cb)
115         self.connect_after("size-allocate", self._after_size_allocate_cb)
116         eventbox.connect("button-press-event", self._button_press_event_cb)
117         self.connect("key-press-event", self._key_press_event_cb)
118
119     def destroy(self):
120         """Destroy the widget."""
121
122         self._maybe_cancel_load()
123         gtk.Window.destroy(self)
124
125     # ----------------------------------------
126
127     def _add_or_remove_image_category(self, category_tag):
128         try:
129             category = env.shelf.getCategoryByTag(category_tag)
130         except CategoryDoesNotExistError:
131             # Category was removed. Ugly fix for now: ignore.
132             pass
133         else:
134             image = self._image_versions[self._current_index].getImage()
135             if category in image.getCategories():
136                 image.removeCategory(category)
137             else:
138                 image.addCategory(category)
139         self._update_image_categories_label()
140
141     def _after_map_event_cb(self, *unused):
142         self._hide_cursor()
143
144     def _after_size_allocate_cb(self, widget, rect):
145         allocated_size = (rect.width, rect.height)
146         if allocated_size == self._last_allocated_size:
147             return
148         self._last_allocated_size = allocated_size
149         self._goto(self._current_index)
150
151     def _button_press_event_cb(self, widget, event):
152         iv = self._image_view
153         if event.button == 1 and iv.get_zoom_mode() == iv.ZoomMode.BestFit:
154             self._goto(self._current_index + 1)
155
156     def _category_entry_activate_cb(self, widget):
157         if self._selected_category_tag is not None:
158             self._add_or_remove_image_category(self._selected_category_tag)
159             self._categorization_hbox.hide_all()
160             self._last_selected_category_tag = self._selected_category_tag
161             self._category_entry.set_text("")
162             # self._selected_category_tag is set to None implicitly by set_text.
163
164     def _category_entry_changed_cb(self, widget):
165         text = self._category_entry.get_text().decode("utf-8")
166         regexp = re.compile(".*%s.*" % re.escape(text.lower()))
167         if text != "":
168             categories = list(env.shelf.getMatchingCategories(regexp))
169         else:
170             categories = []
171         exact_match = None
172         for category in categories:
173             if category.getTag().lower() == text.lower() \
174                    or category.getDescription().lower() == text.lower():
175                 exact_match = category
176                 break
177         if len(categories) == 1 or exact_match is not None:
178             image_stock_id = gtk.STOCK_OK
179             if len(categories) == 1:
180                 selected_category = categories[0]
181             else:
182                 selected_category = exact_match
183             current_image = \
184                 self._image_versions[self._current_index].getImage()
185             image_categories = current_image.getCategories()
186             category_set = selected_category in image_categories
187             self._matching_category_label.set_markup(
188                 u"Press enter to <b>%s</b> category <b>%s</b> [<b>%s</b>]" % (
189                     ["set", "unset"][category_set],
190                     selected_category.getDescription(),
191                     selected_category.getTag()))
192             self._selected_category_tag = selected_category.getTag()
193         else:
194             image_stock_id = gtk.STOCK_CANCEL
195             self._selected_category_tag = None
196             self._matching_category_label.set_text("No matching category")
197         self._category_indicator_image.set_from_stock(
198             image_stock_id, gtk.ICON_SIZE_MENU)
199
200     def _display_end_screen(self):
201         self._maybe_cancel_load()
202
203         self._image_view.hide()
204         self._end_screen_label.show()
205         self._image_categories_label.hide()
206         self._category_keys_info_label.hide()
207         self._key_assignment_hbox.hide_all()
208         self._categorization_hbox.hide_all()
209
210     def _display_image(self):
211         self._image_view.set_image(self._get_image_async_cb)
212         self._update_image_categories_label()
213
214         self._image_view.show()
215         self._end_screen_label.hide()
216         if self._show_image_categories:
217             self._image_categories_label.show()
218         else:
219             self._image_categories_label.hide()
220         if self._show_category_keys_info:
221             self._category_keys_info_label.show()
222         else:
223             self._category_keys_info_label.hide()
224         self._key_assignment_hbox.hide_all()
225         self._categorization_hbox.hide_all()
226
227     def _get_image_async_cb(self, size):
228         path = self._image_versions[self._current_index].getLocation()
229         self._maybe_cancel_load()
230         self._latest_handle = env.pixbufLoader.load(
231             path,
232             size,
233             self._image_view.set_from_pixbuf,
234             self._image_view.set_error)
235         if size != self._latest_size:
236             self._unload(self._latest_size)
237         self._preload(size)
238         self._latest_size = size
239
240     def _goto(self, new_index):
241         if new_index < 0:
242             self._current_index = -1
243             self._display_end_screen()
244         elif new_index >= len(self._image_versions):
245             self._current_index = len(self._image_versions)
246             self._display_end_screen()
247         else:
248             self._current_index = new_index
249             self._display_image()
250
251     def _hide_cursor(self):
252         pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
253         color = gtk.gdk.Color()
254         invisible_cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
255         self.window.set_cursor(invisible_cursor)
256
257     def _is_valid_index(self, index):
258         return 0 <= index < len(self._image_versions)
259
260     def _key_assignment_entry_activate_cb(self, widget):
261         key = self._key_assignment_entry.get_text()
262         if (self._last_selected_category_tag is None
263             or len(key) != 1
264             or key not in string.lowercase):
265             return
266
267         keysym = getattr(gtk.keysyms, key)
268         env.fullScreenKeyAssignmentMap[keysym] = \
269             self._last_selected_category_tag
270         self._key_assignment_hbox.hide_all()
271         self._key_assignment_entry.set_text("")
272         self._update_key_assignment_info()
273
274     def _key_press_event_cb(self, unused, event):
275         # GIMP: 1 --> 100%, C-S-e --> fit
276         # EOG: [1,C-0,C-1] --> 100%
277         # f-spot: [0,1,C-0,C-1] --> fit
278
279         k = gtk.keysyms
280         CTRL = gtk.gdk.CONTROL_MASK
281         e = (event.keyval, event.state & CTRL)
282
283         if self._key_assignment_hbox.props.visible:
284             #
285             # Showing key assignment form -- disable bindings except escape.
286             #
287             if e in [(k.Escape, 0), (k.a, CTRL)]:
288                 self._toggle_key_assignment_form()
289                 return True
290             else:
291                 return False
292
293         if self._categorization_hbox.props.visible:
294             #
295             # Showing categorization form -- disable bindings except escape.
296             #
297             if e in [(k.Escape, 0), (k.t, CTRL)]:
298                 self._toggle_categorization_form()
299                 return True
300             else:
301                 return False
302
303         #
304         # Normal case.
305         #
306         if not (event.state & CTRL) \
307                and event.keyval in env.fullScreenKeyAssignmentMap:
308             category_tag = env.fullScreenKeyAssignmentMap[event.keyval]
309             self._add_or_remove_image_category(category_tag)
310             return True
311         if e in [(k.space, 0), (k.Right, 0), (k.Down, 0), (k.Page_Down, 0)]:
312             self._goto(self._current_index + 1)
313             return True
314         if e in [(k.BackSpace, 0), (k.Left, 0), (k.Up, 0), (k.Page_Up, 0)]:
315             self._goto(self._current_index - 1)
316             return True
317         if e == (k.Home, 0):
318             self._goto(0)
319             return True
320         if e == (k.End, 0):
321             self._goto(len(self._image_versions) - 1)
322             return True
323         if e == (k.Escape, 0):
324             self.destroy()
325             return True
326         if e == (k.plus, 0):
327             self._image_view.zoom_in()
328             return True
329         if e == (k.minus, 0):
330             self._image_view.zoom_out()
331             return True
332         if e == (k._1, 0):
333             self._image_view.zoom_to_actual()
334             return True
335         if e in [(k.equal, 0), (k._0, 0)]:
336             self._image_view.zoom_to_fit()
337             return True
338         if e == (k.a, CTRL):
339             self._toggle_key_assignment_form()
340             return True
341         if e == (k.c, CTRL):
342             self._toggle_image_categories()
343             return True
344         if e == (k.s, CTRL):
345             self._toggle_category_keys_info()
346             return True
347         if e == (k.t, CTRL):
348             self._toggle_categorization_form()
349             return True
350         return False
351
352     def _maybe_cancel_load(self):
353         if self._latest_handle is None:
354             # Nothing to cancel.
355             return
356         env.pixbufLoader.cancel_load(self._latest_handle)
357         self._latest_handle = None
358
359     def _preload(self, size):
360         self._preload_or_unload(size, True)
361
362     def _preload_or_unload(self, size, preload):
363         index = self._current_index
364         for x in [index + 2, index - 1, index + 1]:
365             if self._is_valid_index(x):
366                 location = self._image_versions[x].getLocation()
367                 if preload:
368                     env.pixbufLoader.preload(location, size)
369                 else:
370                     env.pixbufLoader.unload(location, size)
371
372     def _toggle_categorization_form(self):
373         if not self._image_view.props.visible:
374             # Displaying end screen.
375             return
376         if self._categorization_hbox.props.visible:
377             self._categorization_hbox.hide_all()
378         else:
379             self._categorization_hbox.show_all()
380             self._category_entry.grab_focus()
381
382     def _toggle_category_keys_info(self):
383         self._show_category_keys_info = not self._show_category_keys_info
384         if self._show_category_keys_info:
385             self._category_keys_info_label.show()
386         else:
387             self._category_keys_info_label.hide()
388
389     def _toggle_image_categories(self):
390         self._show_image_categories = not self._show_image_categories
391         if self._show_image_categories:
392             self._image_categories_label.show()
393         else:
394             self._image_categories_label.hide()
395
396     def _toggle_key_assignment_form(self):
397         if not self._image_view.props.visible:
398             # Displaying end screen.
399             return
400         if self._key_assignment_hbox.props.visible:
401             self._key_assignment_hbox.hide_all()
402         else:
403             self._key_assignment_hbox.show_all()
404             self._key_assignment_entry.grab_focus()
405
406     def _unload(self, size):
407         self._preload_or_unload(size, False)
408
409     def _update_image_categories_label(self):
410         image = self._image_versions[self._current_index].getImage()
411         categories = image.getCategories()
412         texts = sorted(x.getTag() for x in categories)
413         markup = u" | ".join(u"<b>%s</b>" % x for x in texts)
414         self._image_categories_label.set_markup(markup)
415
416     def _update_key_assignment_info(self):
417         text = u"\n".join(
418             u"<b>%s</b>: <b>%s</b>" % (chr(k), v)
419             for (k, v) in env.fullScreenKeyAssignmentMap.iteritems())
420         self._category_keys_info_label.set_markup(u"Assigned keys:\n" + text)
421
422 gobject.type_register(FullScreenWindow) # TODO: Not needed in PyGTK 2.8.