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