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