Refactor field and function names
[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 from kofoto.gkofoto.imageview import ImageView
9 from kofoto.gkofoto.environment import env
10
11 class FullScreenWindow(gtk.Window):
12     """A fullscreen window widget."""
13
14     def __init__(self, image_versions, current_index=0):
15         """Constructor.
16
17         Arguments:
18
19         image_versions -- A list of ImageVersion instance to display.
20         current_index  -- Where to start in image_versions.
21         """
22
23         self.__gobject_init__() # TODO: Use gtk.Window.__init__ in PyGTK 2.8.
24
25         self._last_allocated_size = None
26         self._image_versions = image_versions
27         self._current_index = current_index
28         self._latest_handle = None
29         self._latest_size = (0, 0)
30         self._selected_category = None
31
32         bg_color = gtk.gdk.color_parse("#000000")
33         fg_color = gtk.gdk.color_parse("#999999")
34
35         eventbox = gtk.EventBox()
36         vbox = gtk.VBox()
37         eventbox.add(vbox)
38
39         # Add the image widget.
40         self._image_view = ImageView()
41         self._image_view.set_error_pixbuf(
42             gtk.gdk.pixbuf_new_from_file(env.unknownImageIconFileName))
43         self._image_view.modify_bg(gtk.STATE_NORMAL, bg_color)
44         vbox.pack_start(self._image_view)
45
46         # Add end screen label.
47         label = gtk.Label()
48         label.set_text(
49             "No more images.\nPress escape to get back to GKofoto.")
50         label.set_justify(gtk.JUSTIFY_CENTER)
51         label.modify_fg(gtk.STATE_NORMAL, fg_color)
52         vbox.pack_start(label)
53         self._end_screen_label = label
54
55         # Add image categories label.
56         label = gtk.Label()
57         label.modify_fg(gtk.STATE_NORMAL, fg_color)
58         vbox.pack_start(label, False)
59         self._image_categories_label = label
60
61         # Add categorization form.
62         self._categorization_hbox = gtk.HBox()
63         self._categorization_hbox.set_spacing(5)
64         vbox.pack_start(self._categorization_hbox, False)
65         label = gtk.Label("Category:")
66         label.modify_fg(gtk.STATE_NORMAL, fg_color)
67         self._categorization_hbox.pack_start(label, False)
68         self._category_entry = gtk.Entry()
69         self._category_entry.connect(
70             "activate", self._category_entry_activate_cb)
71         self._category_entry.connect(
72             "changed", self._category_entry_changed_cb)
73         self._categorization_hbox.pack_start(self._category_entry, False)
74         self._category_indicator_image = gtk.Image()
75         self._category_indicator_image.set_from_stock(
76             gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU)
77         self._categorization_hbox.pack_start(self._category_indicator_image, False)
78         self._category_info_label = gtk.Label()
79         self._category_info_label.set_line_wrap(True)
80         self._category_info_label.modify_fg(gtk.STATE_NORMAL, fg_color)
81         self._categorization_hbox.pack_start(self._category_info_label, True, True)
82
83         self.modify_bg(gtk.STATE_NORMAL, bg_color)
84         eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
85         self.add(eventbox)
86         self.set_modal(True)
87         self.set_default_size(400, 400)
88         self.fullscreen()
89         self.connect_after("map-event", self._after_map_event_cb)
90         self.connect_after("size-allocate", self._after_size_allocate_cb)
91         eventbox.connect("button-press-event", self._button_press_event_cb)
92         self.connect("key-press-event", self._key_press_event_cb)
93
94     def destroy(self):
95         """Destroy the widget."""
96
97         self._maybe_cancel_load()
98         gtk.Window.destroy(self)
99
100     # ----------------------------------------
101
102     def _after_map_event_cb(self, *unused):
103         self._hide_cursor()
104
105     def _after_size_allocate_cb(self, widget, rect):
106         allocated_size = (rect.width, rect.height)
107         if allocated_size == self._last_allocated_size:
108             return
109         self._last_allocated_size = allocated_size
110         self._goto(self._current_index)
111
112     def _button_press_event_cb(self, widget, event):
113         iv = self._image_view
114         if event.button == 1 and iv.get_zoom_mode() == iv.ZoomMode.BestFit:
115             self._goto(self._current_index + 1)
116
117     def _category_entry_activate_cb(self, widget):
118         if self._selected_category is not None:
119             image = self._image_versions[self._current_index].getImage()
120             if self._selected_category in image.getCategories():
121                 image.removeCategory(self._selected_category)
122             else:
123                 image.addCategory(self._selected_category)
124             self._categorization_hbox.hide_all()
125             self._category_entry.set_text("")
126             # self._selected_category is set to None implicitly by set_text.
127
128     def _category_entry_changed_cb(self, widget):
129         text = self._category_entry.get_text().decode("utf-8")
130         regexp = re.compile(".*%s.*" % re.escape(text.lower()))
131         if text != "":
132             categories = list(env.shelf.getMatchingCategories(regexp))
133         else:
134             categories = []
135         exact_match = None
136         for category in categories:
137             if category.getTag().lower() == text.lower() \
138                    or category.getDescription().lower() == text.lower():
139                 exact_match = category
140                 break
141         if len(categories) == 1 or exact_match is not None:
142             image_stock_id = gtk.STOCK_OK
143             if len(categories) == 1:
144                 self._selected_category = categories[0]
145             else:
146                 self._selected_category = exact_match
147             current_image = \
148                 self._image_versions[self._current_index].getImage()
149             image_categories = current_image.getCategories()
150             category_set = self._selected_category in image_categories
151             self._category_info_label.set_markup(
152                 u"Press enter to <b>%s</b> category <b>%s</b> [<b>%s</b>]" % (
153                     ["set", "unset"][category_set],
154                     self._selected_category.getDescription(),
155                     self._selected_category.getTag()))
156         else:
157             image_stock_id = gtk.STOCK_CANCEL
158             self._selected_category = None
159             self._category_info_label.set_text("No matching category")
160         self._category_indicator_image.set_from_stock(
161             image_stock_id, gtk.ICON_SIZE_MENU)
162
163     def _display_end_screen(self):
164         self._maybe_cancel_load()
165         self._image_view.hide()
166         self._end_screen_label.show()
167         self._categorization_hbox.hide_all()
168         self._image_categories_label.hide()
169
170     def _display_image(self):
171         self._image_view.set_image(self._get_image_async_cb)
172         self._image_view.show()
173         self._end_screen_label.hide()
174         self._categorization_hbox.hide_all()
175         self._image_categories_label.show()
176         self._update_image_categories_label()
177
178     def _get_image_async_cb(self, size):
179         path = self._image_versions[self._current_index].getLocation()
180         self._maybe_cancel_load()
181         self._latest_handle = env.pixbufLoader.load(
182             path,
183             size,
184             self._image_view.set_from_pixbuf,
185             self._image_view.set_error)
186         if size != self._latest_size:
187             self._unload(self._latest_size)
188         self._preload(size)
189         self._latest_size = size
190
191     def _goto(self, new_index):
192         if new_index < 0:
193             self._current_index = -1
194             self._display_end_screen()
195         elif new_index >= len(self._image_versions):
196             self._current_index = len(self._image_versions)
197             self._display_end_screen()
198         else:
199             self._current_index = new_index
200             self._display_image()
201
202     def _hide_cursor(self):
203         pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
204         color = gtk.gdk.Color()
205         invisible_cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
206         self.window.set_cursor(invisible_cursor)
207
208     def _is_valid_index(self, index):
209         return 0 <= index < len(self._image_versions)
210
211     def _key_press_event_cb(self, unused, event):
212         # GIMP: 1 --> 100%, C-S-e --> fit
213         # EOG: [1,C-0,C-1] --> 100%
214         # f-spot: [0,1,C-0,C-1] --> fit
215
216         k = gtk.keysyms
217         CTRL = gtk.gdk.CONTROL_MASK
218         e = (event.keyval, event.state & CTRL)
219
220         if self._categorization_hbox.props.visible:
221             #
222             # Showing category entry -- disable bindings except escape.
223             #
224             if e in [(k.Escape, 0), (k.t, CTRL)]:
225                 self._toggle_categorization_form()
226                 return True
227             else:
228                 return False
229
230         #
231         # Normal case.
232         #
233         if e in [(k.space, 0), (k.Right, 0), (k.Down, 0), (k.Page_Down, 0)]:
234             self._goto(self._current_index + 1)
235             return True
236         if e in [(k.BackSpace, 0), (k.Left, 0), (k.Up, 0), (k.Page_Up, 0)]:
237             self._goto(self._current_index - 1)
238             return True
239         if e == (k.Home, 0):
240             self._goto(0)
241             return True
242         if e == (k.End, 0):
243             self._goto(len(self._image_versions) - 1)
244             return True
245         if e == (k.Escape, 0):
246             self.destroy()
247             return True
248         if e == (k.plus, 0):
249             self._image_view.zoom_in()
250             return True
251         if e == (k.minus, 0):
252             self._image_view.zoom_out()
253             return True
254         if e == (k._1, 0):
255             self._image_view.zoom_to_actual()
256             return True
257         if e in [(k.equal, 0), (k._0, 0)]:
258             self._image_view.zoom_to_fit()
259             return True
260         if e == (k.c, CTRL):
261             self._toggle_image_categories()
262             return True
263         if e == (k.t, CTRL):
264             self._toggle_categorization_form()
265             return True
266         return False
267
268     def _maybe_cancel_load(self):
269         if self._latest_handle is None:
270             # Nothing to cancel.
271             return
272         env.pixbufLoader.cancel_load(self._latest_handle)
273         self._latest_handle = None
274
275     def _preload(self, size):
276         self._preload_or_unload(size, True)
277
278     def _preload_or_unload(self, size, preload):
279         index = self._current_index
280         for x in [index + 2, index - 1, index + 1]:
281             if self._is_valid_index(x):
282                 location = self._image_versions[x].getLocation()
283                 if preload:
284                     env.pixbufLoader.preload(location, size)
285                 else:
286                     env.pixbufLoader.unload(location, size)
287
288     def _toggle_categorization_form(self):
289         if not self._image_view.props.visible:
290             # Display end screen.
291             return
292         if self._categorization_hbox.props.visible:
293             self._categorization_hbox.hide_all()
294         else:
295             self._categorization_hbox.show_all()
296             self._category_entry.grab_focus()
297
298     def _toggle_image_categories(self):
299         cl = self._image_categories_label
300         if cl.props.visible:
301             cl.hide()
302         else:
303             cl.show()
304
305     def _unload(self, size):
306         self._preload_or_unload(size, False)
307
308     def _update_image_categories_label(self):
309         image = self._image_versions[self._current_index].getImage()
310         categories = image.getCategories()
311         texts = sorted(x.getTag() for x in categories)
312         markup = u" | ".join(u"<b>%s</b>" % x for x in texts)
313         self._image_categories_label.set_markup(markup)
314
315 gobject.type_register(FullScreenWindow) # TODO: Not needed in PyGTK 2.8.