Use list.sort's keyword parameters
[joel/kofoto.git] / src / packages / kofoto / gkofoto / imageview.py
1 """
2 This module contains the ImageView class.
3 """
4
5 __all__ = ["ImageView"]
6
7 if __name__ == "__main__":
8     import pygtk
9     pygtk.require("2.0")
10 import gc
11 import gtk
12 import gobject
13 import operator
14 import os
15 from kofoto.alternative import Alternative
16 from kofoto.gkofoto.environment import env
17 from kofoto.rectangle import Rectangle
18
19 def _gdk_rectangle_size(gdk_rectangle):
20     return Rectangle(gdk_rectangle.width, gdk_rectangle.height)
21
22 def _pixbuf_size(pixbuf):
23     return Rectangle(pixbuf.get_width(), pixbuf.get_height())
24
25 def _safely_downscale_pixbuf(pixbuf, limit):
26     size = _pixbuf_size(pixbuf)
27     return _safely_scale_pixbuf(pixbuf, size.downscaled_to(limit))
28
29 def _safely_rescale_pixbuf(pixbuf, limit):
30     size = _pixbuf_size(pixbuf)
31     return _safely_scale_pixbuf(pixbuf, size.rescaled_to(limit))
32
33 def _safely_scale_pixbuf(pixbuf, size):
34     # The scaling with a factor not more than 30 is a work-around for
35     # a scaling bug in GTK+.
36
37     def scale_pixbuf(pixbuf, size):
38         if not Rectangle(1, 1).fits_within(size):
39             size = Rectangle(1, 1)
40         return pixbuf.scale_simple(
41             int(size.width), int(size.height), gtk.gdk.INTERP_BILINEAR)
42
43     psize = _pixbuf_size(pixbuf)
44     if psize.fits_within(size):
45         # Scale up.
46         factor = 1/30.0 # Somewhat arbitrary.
47         cmpfn = operator.lt
48     else:
49         # Scale down.
50         factor = 30 # Somewhat arbitrary.
51         cmpfn = operator.gt
52     while cmpfn(float(psize.max()) / size.max(), factor):
53         psize /= factor
54         pixbuf = scale_pixbuf(pixbuf, psize)
55     return scale_pixbuf(pixbuf, size)
56
57 class ImageView(gtk.Table):
58     """A quick image view widget supporting zooming."""
59
60     # Possible values of self._zoom_mode.
61     ZoomMode = Alternative("BestFit", "ActualSize", "Zoom")
62
63     # Hard-coded for now.
64     _MOUSE_WHEEL_ZOOM_FACTOR = 1.2
65
66     def __init__(self):
67         self.__gobject_init__() # TODO: Use gtk.Table.__init__ in PyGTK 2.8.
68
69         # Displayed pixbuf; None if not available.
70         self._displayed_pixbuf = None
71
72         # Zoom factor to use for one zoom step.
73         self._zoom_factor = 1.5
74
75         # Zoom size in pixels (a float, so that self._zoom_size *
76         # self._zoom_factor * 1/self._zoom_factor == self._zoom_size).
77         self._zoom_size = 0.0
78
79         # Zoom mode.
80         self._zoom_mode = ImageView.ZoomMode.BestFit
81
82         # Whether the widget should prescale resized images while
83         # waiting for the real thing.
84         self._prescale_mode = True
85
86         # Function to call when a new pixbuf size is wanted. None if
87         # ImageView.set_image has not yet been called.
88         self._request_pixbuf_func = None
89
90         # The size of the original image as a Rectangle instance. None
91         # if not yet known.
92         self._available_size = None
93
94         # Remember requested pixbuf size (a Rectangle instance or
95         # None) to avoid unnecessary reloading.
96         self._previously_requested_pixbuf_size = None
97
98         # Whether scrolling by mouse dragging is enabled, i.e.,
99         # whether there is at least one visible scroll bar.
100         self._mouse_dragging_enabled = False
101
102         # Used to remember reference coordinates for mouse dragging.
103         self._last_mouse_drag_reference_x = None
104         self._last_mouse_drag_reference_y = None
105
106         # Pixbuf to display on errors.
107         self._error_pixbuf = self.render_icon(
108             gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_DIALOG, "kofoto")
109
110         # Mouse cursors.
111         display = gtk.gdk.display_get_default()
112         pixbuf = gtk.gdk.pixbuf_new_from_file(
113             os.path.join(env.iconDir, "hand-open.png"))
114         self._open_hand_cursor = gtk.gdk.Cursor(display, pixbuf, 13, 13)
115         pixbuf = gtk.gdk.pixbuf_new_from_file(
116             os.path.join(env.iconDir, "hand-closed.png"))
117         self._closed_hand_cursor = gtk.gdk.Cursor(display, pixbuf, 13, 13)
118
119         # Subwidgets.
120         self.resize(2, 2)
121         self._eventbox_widget = gtk.EventBox()
122         self._image_widget = gtk.DrawingArea()
123         ew = self._eventbox_widget
124         iw = self._image_widget
125         options = gtk.FILL | gtk.EXPAND | gtk.SHRINK
126         self.attach(ew, 0, 1, 0, 1, options, options)
127         ew.add(iw)
128         iw.connect_after("realize", self._image_realize_cb)
129         iw.connect_after("unrealize", self._image_unrealize_cb)
130         self.connect_after("size-allocate", self._after_size_allocate_cb)
131         iw.connect("expose-event", self._image_expose_event_cb)
132         ew.connect("button-press-event", self._image_button_press_event_cb)
133         ew.connect("button-release-event", self._image_button_release_event_cb)
134         ew.connect("motion-notify-event", self._image_motion_notify_event_cb)
135         ew.connect("scroll-event", self._image_scroll_event_cb)
136
137         self._width_adjustment = gtk.Adjustment()
138         wadj = self._width_adjustment
139         self._width_scrollbar = gtk.HScrollbar(wadj)
140         wscrollbar = self._width_scrollbar
141         self.attach(wscrollbar, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
142
143         self._height_adjustment = gtk.Adjustment()
144         hadj = self._height_adjustment
145         self._height_scrollbar = gtk.VScrollbar(hadj)
146         hscrollbar = self._height_scrollbar
147         self.attach(hscrollbar, 1, 2, 0, 1, gtk.FILL, gtk.FILL)
148
149         # Listen for adjustment changes so that we can make
150         # adjustments behave nicely when zooming and resizing.
151         wadj.connect("value-changed", self._width_adjustment_value_changed)
152         hadj.connect("value-changed", self._height_adjustment_value_changed)
153
154         self._image_widget.show()
155         self._eventbox_widget.show()
156
157     def clear(self):
158         """Make the widget display nothing."""
159
160         assert self._is_realized()
161         self._available_size = None
162         self._request_pixbuf_func = None
163         self._displayed_pixbuf = None
164         self._update_scroll_bars()
165         self._image_widget.queue_draw()
166         gc.collect() # Help GTK to get rid of the old pixbuf.
167
168     def get_image_widget(self):
169         """Get the wrapped image widget.
170
171         Returns the wrapped image widget. This widget is the widget to
172         which mouse event handlers should be connected. (The return
173         value is actually an eventbox in which the image widget is
174         wrapped.)
175         """
176         return self._eventbox_widget
177
178     def get_prescale_mode(self):
179         """Whether the widget should prescale a resized image."""
180
181         return self._prescale_mode
182
183     def get_wanted_image_size(self):
184         """Get the currently wanted image size.
185
186         Returns a tuple (width_limit, height_limit) or None. None
187         indicates that the full-size image is wanted. The size is the
188         same that will be passed to the load_pixbuf_func passed to
189         set_image.
190         """
191
192         if self._zoom_mode == ImageView.ZoomMode.ActualSize:
193             return None
194         else:
195             return tuple(self._calculate_wanted_image_size())
196
197     def get_zoom_level(self):
198         """Get current zoom level."""
199
200         if self._displayed_pixbuf is None:
201             # Just return something.
202             return 1.0
203         else:
204             # We know that the proportions of self._displayed_pixbuf
205             # and the full-size image are the same since they are both
206             # set in set_from_pixbuf/set_error.
207             return (
208                 float(self._displayed_pixbuf.get_width()) /
209                 self._available_size.width)
210
211     def get_zoom_mode(self):
212         """Get current zoom mode."""
213
214         return self._zoom_mode
215
216     def modify_bg(self, state, color):
217         """Set the background color.
218
219         See gtk.Widget.modify_bg.
220         """
221
222         gtk.Table.modify_bg(self, state, color)
223         self._image_widget.modify_bg(state, color)
224
225     def set_error(self):
226         """Indicate that loading of the image failed.
227
228         This method should be called by the pixbuf request function
229         passed to ImageView.set_image if there was an error loading
230         the pixbuf.
231         """
232
233         assert self._is_realized() and self._image_is_set()
234         self._available_size = _pixbuf_size(self._error_pixbuf)
235         self._displayed_pixbuf = self._error_pixbuf
236         self._update_scroll_bars()
237         self._image_widget.queue_draw()
238         gc.collect() # Help GTK to get rid of the old pixbuf.
239
240     def set_error_pixbuf(self, pixbuf):
241         """Set the pixbuf displayed on errors."""
242
243         self._error_pixbuf = pixbuf
244
245     def set_from_pixbuf(self, pixbuf, available_size):
246         """Set displayed pixbuf.
247
248         This method should be called by the pixbuf request function
249         passed to ImageView.set_image if the load was successful.
250
251         Arguments:
252
253         pixbuf           -- The pixbuf.
254         available_size   -- Tuple (width, height) of the full-size image.
255         """
256
257         if not (self._is_realized() and self._image_is_set()):
258             return
259         if self._displayed_pixbuf_is_current():
260             if not Rectangle(*available_size).fits_within(
261                     self._available_size):
262                 # The original has grown on disk, so we might want a
263                 # larger pixbuf if available.
264                 self._request_pixbuf_and_prescale()
265                 return
266         self._available_size = Rectangle(*available_size)
267         self._create_displayed_pixbuf(pixbuf)
268         self._update_scroll_bars()
269         self._image_widget.queue_draw()
270         gc.collect() # Help GTK to get rid of the old pixbuf.
271
272     def set_image(self, load_pixbuf_func):
273         """Set image to display in the view.
274
275         This method indirectly sets the image to be displayed in the
276         widget. The argument function will be called (possibly
277         multiple times) to request a new pixbuf. Here are the
278         requirements on the function:
279
280         1. The function must accept one parameter: a tuple of width
281            limit and height limit in pixels, or None.
282         2. If the limit parameter is None, a full-size pixbuf should
283            be loaded.
284         3. The function must
285            a) call set_from_pixbuf with a the largest possible pixbuf
286               that fits within the limit; or
287            b) call set_error on failure.
288         4. The image proportions must be retained when scaling down
289            the image down to fit the limit.
290         5. The loaded pixbuf must not be larger than the original
291            image.
292
293         Arguments:
294
295         load_pixbuf_func -- The function.
296         """
297
298         self._request_pixbuf_func = load_pixbuf_func
299         self._available_size = None
300         if self._is_realized():
301             self._request_pixbuf_and_prescale()
302
303     def set_prescale_mode(self, mode):
304         """Set whether the widget should prescale a resized image."""
305
306         self._prescale_mode = mode
307
308     def set_zoom_factor(self, factor):
309         """Set the zoom factor."""
310
311         self._zoom_factor = factor
312
313     def zoom_in(self):
314         """Zoom in one step."""
315
316         assert self._is_realized() and self._image_is_set()
317         self._zoom_inout(self._zoom_factor)
318
319     def zoom_out(self):
320         """Zoom out one step."""
321
322         assert self._is_realized() and self._image_is_set()
323         self._zoom_inout(1 / self._zoom_factor)
324
325     def zoom_to(self, level):
326         """Zoom to a given zoom level.
327
328         Arguments:
329
330         level -- The zoom level (a number). 1.0 means the full-size
331                  image, 0.5 means a half-size image and so on.
332         """
333
334         assert self._is_realized() and self._image_is_set()
335         self._zoom_mode = ImageView.ZoomMode.Zoom
336         level = min(level, 1.0)
337         max_avail = max(
338             self._available_size.width, self._available_size.height)
339         self._zoom_size = level * max_avail
340         self._zoom_changed()
341
342     def zoom_to_actual(self):
343         """Zoom to actual (pixel-wise) image size."""
344
345         assert self._is_realized() and self._image_is_set()
346         self._zoom_mode = ImageView.ZoomMode.ActualSize
347         self._zoom_changed()
348
349     def zoom_to_fit(self):
350         """Zoom image to fit the current widget size."""
351
352         assert self._is_realized() and self._image_is_set()
353         self._zoom_mode = ImageView.ZoomMode.BestFit
354         self._zoom_changed()
355
356     def _after_size_allocate_cb(self, widget, rect):
357         if not (self._is_realized() and self._image_is_set()):
358             return
359         self._maybe_request_new_pixbuf_and_prescale()
360         self._update_scroll_bars()
361         self._image_widget.queue_draw()
362
363     def _calculate_best_fit_image_size(self):
364         allocation = self._image_widget.allocation
365         return Rectangle(allocation.width, allocation.height)
366
367     def _calculate_wanted_image_size(self):
368         if self._zoom_mode == ImageView.ZoomMode.ActualSize:
369             return self._available_size
370         else:
371             if self._zoom_mode == ImageView.ZoomMode.BestFit:
372                 return self._calculate_best_fit_image_size()
373             else:
374                 return self._calculate_zoomed_image_boundary()
375
376     def _calculate_zoomed_image_boundary(self):
377         zsize = int(round(self._zoom_size))
378         return Rectangle(zsize, zsize)
379
380     def _create_displayed_pixbuf(self, pixbuf):
381         size = self._calculate_wanted_image_size()
382         if self._zoom_mode == ImageView.ZoomMode.ActualSize:
383             self._displayed_pixbuf = pixbuf
384         elif size == _pixbuf_size(pixbuf):
385             self._displayed_pixbuf = pixbuf
386         else:
387             self._displayed_pixbuf = _safely_downscale_pixbuf(pixbuf, size)
388
389     def _disable_mouse_dragging(self):
390         self._mouse_dragging_enabled = False
391         self._image_widget.window.set_cursor(None)
392
393     def _displayed_pixbuf_is_current(self):
394         return self._available_size is not None
395
396     def _draw_pixbuf(self):
397         if not self._displayed_pixbuf:
398             return
399
400         # The rectangle <source_x, source_y> to <source_x + width,
401         # source_y + height> defines which part of the pixbuf to draw
402         # and the rectangle <target_x, target_y> to <target_x + width,
403         # target_y + height> defines where to draw it on the image
404         # widget.
405         source_x = int(round(self._width_adjustment.value))
406         width = int(round(self._width_adjustment.page_size))
407         source_y = int(round(self._height_adjustment.value))
408         height = int(round(self._height_adjustment.page_size))
409
410         # Draw the pixbuf in the center if it is smaller than the
411         # image widget.
412         allocation = self._image_widget.allocation
413         target_x = max(allocation.width - width, 0) / 2
414         target_y = max(allocation.height - height, 0) / 2
415
416         dp = self._displayed_pixbuf
417         gcontext = self._image_widget.style.fg_gc[gtk.STATE_NORMAL]
418         self._image_widget.window.draw_pixbuf(
419             gcontext, dp,
420             source_x, source_y,
421             target_x, target_y,
422             width, height)
423
424     def _enable_mouse_dragging(self):
425         self._mouse_dragging_enabled = True
426         self._image_widget.window.set_cursor(self._open_hand_cursor)
427
428     def _height_adjustment_value_changed(self, adj):
429         self._image_widget.queue_draw()
430
431     def _image_button_press_event_cb(self, widget, event):
432         if self._mouse_dragging_enabled and event.button == 1:
433             self._image_widget.window.set_cursor(self._closed_hand_cursor)
434             self._last_mouse_drag_reference_x = event.x
435             self._last_mouse_drag_reference_y = event.y
436
437     def _image_button_release_event_cb(self, widget, event):
438         if self._mouse_dragging_enabled and event.button == 1:
439             self._image_widget.window.set_cursor(self._open_hand_cursor)
440
441     def _image_expose_event_cb(self, widget, event):
442         self._draw_pixbuf()
443
444     def _image_is_set(self):
445         return self._request_pixbuf_func is not None
446
447     def _image_motion_notify_event_cb(self, widget, event):
448         if self._mouse_dragging_enabled:
449             wa = self._width_adjustment
450             ha = self._height_adjustment
451
452             dx = event.x - self._last_mouse_drag_reference_x
453             dy = event.y - self._last_mouse_drag_reference_y
454             new_w_value = max(0, min(wa.upper - wa.page_size, wa.value - dx))
455             new_h_value = max(0, min(ha.upper - ha.page_size, ha.value - dy))
456             self._last_mouse_drag_reference_x -= new_w_value - wa.value
457             self._last_mouse_drag_reference_y -= new_h_value - ha.value
458             wa.value = new_w_value
459             ha.value = new_h_value
460
461     def _image_realize_cb(self, widget):
462         if self._image_is_set():
463             self._request_pixbuf_and_prescale()
464
465     def _image_scroll_event_cb(self, widget, event):
466         allocation = self._image_widget.allocation
467         center_x = float(event.x) / allocation.width
468         center_y = float(event.y) / allocation.height
469         if event.direction == gtk.gdk.SCROLL_DOWN:
470             self._zoom_inout(
471                 1 / ImageView._MOUSE_WHEEL_ZOOM_FACTOR, center_x, center_y)
472         elif event.direction == gtk.gdk.SCROLL_UP:
473             self._zoom_inout(
474                 ImageView._MOUSE_WHEEL_ZOOM_FACTOR, center_x, center_y)
475
476     def _image_unrealize_cb(self, widget):
477         self._displayed_pixbuf = None
478         gc.collect() # Help GTK to get rid of the old pixbuf.
479
480     def _is_realized(self):
481         return self.flags() & gtk.REALIZED
482
483     def _limit_zoom_size_to_available_size(self):
484         if self._displayed_pixbuf_is_current():
485             self._zoom_size = float(min(
486                 self._zoom_size,
487                 max(self._available_size.width, self._available_size.height)))
488
489     def _maybe_request_new_pixbuf_and_prescale(self):
490         if self._zoom_mode != ImageView.ZoomMode.BestFit:
491             # We already have (requested) a pixbuf of the correct
492             # size.
493             return
494         wanted_size = self._calculate_best_fit_image_size()
495         if self._displayed_pixbuf_is_current():
496             # See comment in _request_pixbuf_and_prescale.
497             limited_wanted_size = wanted_size.downscaled_to(
498                 self._available_size)
499         else:
500             limited_wanted_size = wanted_size
501         psize = self._previously_requested_pixbuf_size
502         if psize is not None and limited_wanted_size != psize:
503             self._request_pixbuf_and_prescale()
504
505     def _request_pixbuf_and_prescale(self, center_x=0.5, center_y=0.5):
506         # Remember/guess the actual size of the pixbuf to be
507         # loaded by _request_pixbuf_func. This is done to
508         # avoid _request_pixbuf_and_prescale being called when
509         # we expect that the currently displayed image already
510         # is of the correct (full) size.
511         size = self._calculate_wanted_image_size()
512         self._previously_requested_pixbuf_size = size
513
514         if self._prescale_mode and self._displayed_pixbuf_is_current():
515             self._displayed_pixbuf = _safely_rescale_pixbuf(
516                 self._displayed_pixbuf, size)
517             if self._zoom_mode == ImageView.ZoomMode.Zoom:
518                 self._update_scroll_bars(center_x, center_y)
519                 self._draw_pixbuf()
520         self._request_pixbuf_func(size)
521
522     def _update_scroll_bars(self, center_w=0.5, center_h=0.5):
523         if self._displayed_pixbuf is None:
524             self._width_scrollbar.hide()
525             self._height_scrollbar.hide()
526             self._disable_mouse_dragging()
527             return
528
529         old_pb_width = self._width_adjustment.upper
530         old_pb_height = self._height_adjustment.upper
531         new_pb_width = self._displayed_pixbuf.get_width()
532         new_pb_height = self._displayed_pixbuf.get_height()
533
534         if self._zoom_mode == ImageView.ZoomMode.BestFit:
535             self._width_scrollbar.hide()
536             self._height_scrollbar.hide()
537             self._width_adjustment.set_all(
538                 value=0, page_size=new_pb_width, upper=new_pb_width)
539             self._height_adjustment.set_all(
540                 value=0, page_size=new_pb_height, upper=new_pb_height)
541             self._disable_mouse_dragging()
542             return
543
544         allocated_table_size = _gdk_rectangle_size(self.get_allocation())
545
546         # The size to allocate to the image widget. Start out with an
547         # image size equal to the whole table, i.e., no scroll bars.
548         size = Rectangle(*allocated_table_size)
549
550         h_sb_height = self._width_scrollbar.size_request()[1]
551         v_sb_width = self._height_scrollbar.size_request()[0]
552         show_w_scroll_bar = False
553         show_h_scroll_bar = False
554         for i in range(2): # Two loops are enough to stabilize the result.
555             if not show_w_scroll_bar and \
556                     new_pb_width > size.width:
557                 show_w_scroll_bar = True
558                 size.height -= h_sb_height
559             if not show_h_scroll_bar and \
560                     new_pb_height > size.height:
561                 show_h_scroll_bar = True
562                 size.width -= v_sb_width
563
564         # If the image widget allocation is larger than the displayed
565         # pixbuf, make it not larger than the pixbuf.
566         size.width = min(size.width, new_pb_width)
567         size.height = min(size.height, new_pb_height)
568
569         old_w_page_size = self._width_adjustment.page_size
570         old_h_page_size = self._height_adjustment.page_size
571
572         self._width_adjustment.set_all(
573             upper=new_pb_width,
574             page_size=size.width,
575             page_increment=size.width,
576             step_increment=size.width / 10.0)
577         self._height_adjustment.set_all(
578             upper=new_pb_height,
579             page_size=size.height,
580             page_increment=size.height,
581             step_increment=size.height / 10.0)
582
583         w_value = self._width_adjustment.value
584         h_value = self._height_adjustment.value
585
586         # Adjust adjustment values so that we zoom relative to the
587         # chosen center point.
588         w_value = \
589             (new_pb_width / old_pb_width) \
590             * (w_value + center_w * old_w_page_size) \
591             - center_w * size.width
592         h_value = \
593             (new_pb_height / old_pb_height) \
594             * (h_value + center_h * old_h_page_size) \
595             - center_h * size.height
596
597         # Adjust adjustment values so that they aren't smaller or
598         # larger than allowed (which they may be after a zoom).
599         w_value = min(
600             max(w_value, 0),
601             self._width_adjustment.upper - self._width_adjustment.page_size)
602         h_value = min(
603             max(h_value, 0),
604             self._height_adjustment.upper - self._height_adjustment.page_size)
605
606         self._width_adjustment.value = w_value
607         self._height_adjustment.value = h_value
608
609         if show_w_scroll_bar:
610             self._width_scrollbar.show()
611         else:
612             self._width_scrollbar.hide()
613         if show_h_scroll_bar:
614             self._height_scrollbar.show()
615         else:
616             self._height_scrollbar.hide()
617
618         if show_w_scroll_bar or show_h_scroll_bar:
619             self._enable_mouse_dragging()
620         else:
621             self._disable_mouse_dragging()
622
623     def _update_zoom_size_to_current(self):
624         if self._zoom_mode == ImageView.ZoomMode.BestFit:
625             allocation = self._image_widget.allocation
626             self._zoom_size = float(max(allocation.width, allocation.height))
627         elif self._zoom_mode == ImageView.ZoomMode.ActualSize:
628             self._zoom_size = max(
629                 self._available_size.width, self._available_size.height)
630
631     def _width_adjustment_value_changed(self, adj):
632         self._image_widget.queue_draw()
633
634     def _zoom_changed(self, center_x=0.5, center_y=0.5):
635         self._request_pixbuf_and_prescale(center_x, center_y)
636         self._update_scroll_bars(center_x, center_y)
637         self._image_widget.queue_draw()
638
639     def _zoom_inout(self, factor, center_x=0.5, center_y=0.5):
640         self._update_zoom_size_to_current()
641         self._zoom_mode = ImageView.ZoomMode.Zoom
642         # The two calls to _limit_zoom_size_to_available_size below
643         # make the zooming feel right in the following two cases (A
644         # and B):
645         #
646         # 1. Both A & B: Enter zoom mode on a large image with a
647         #    sufficiently large zoom size.
648         # 2. Both A & B: Change to an image that is smaller than the
649         #    current zoom size.
650         # 3. Case A: Zoom in and then zoom out. Case B: Zoom out.
651         self._limit_zoom_size_to_available_size()
652         self._zoom_size *= factor
653         self._limit_zoom_size_to_available_size()
654         self._zoom_changed(center_x, center_y)
655
656 gobject.type_register(ImageView) # TODO: Not needed in PyGTK 2.8.
657
658 ######################################################################
659
660 if __name__ == "__main__":
661     from kofoto.gkofoto.cachingpixbufloader import CachingPixbufLoader
662     import sys
663
664     env.iconDir = "%s/../../../gkofoto/icons" % os.path.dirname(sys.argv[0])
665
666     caching_pixbuf_loader = CachingPixbufLoader()
667
668     class State:
669         def __init__(self):
670             self._latest_handle = None
671             self._latest_key = None
672             self._current_pic_index = -1
673             self._image_paths = sys.argv[1:]
674
675         def get_image_async_cb(self, size):
676             path = self._image_paths[self._current_pic_index]
677             if self._latest_handle is not None:
678                 caching_pixbuf_loader.cancel_load(self._latest_handle)
679                 if path == self._latest_key[0]:
680                     caching_pixbuf_loader.unload(*self._latest_key)
681             self._latest_handle = caching_pixbuf_loader.load(
682                 path,
683                 size,
684                 imageview.set_from_pixbuf,
685                 imageview.set_error)
686             self._latest_key = (path, size)
687
688         def next_image(self, *unused):
689             self._current_pic_index = \
690                 (self._current_pic_index + 1) % len(self._image_paths)
691             imageview.set_image(self.get_image_async_cb)
692
693     def callback_wrapper(fn):
694         def _f(*unused):
695             fn()
696         return _f
697
698     def toggle_prescale_mode(widget):
699         imageview.set_prescale_mode(widget.get_active())
700
701     appstate = State()
702
703     window = gtk.Window()
704     window.set_default_size(300, 200)
705
706     imageview = ImageView()
707     window.add(imageview)
708
709     control_window = gtk.Window()
710     control_window.set_transient_for(window)
711
712     control_box = gtk.VBox()
713     control_window.add(control_box)
714
715     clear_button = gtk.Button(stock=gtk.STOCK_CLEAR)
716     control_box.add(clear_button)
717     clear_button.connect("clicked", callback_wrapper(imageview.clear))
718
719     ztf_button = gtk.Button(stock=gtk.STOCK_ZOOM_FIT)
720     control_box.add(ztf_button)
721     ztf_button.connect("clicked", callback_wrapper(imageview.zoom_to_fit))
722
723     za_button = gtk.Button(stock=gtk.STOCK_ZOOM_100)
724     control_box.add(za_button)
725     za_button.connect("clicked", callback_wrapper(imageview.zoom_to_actual))
726
727     zi_button = gtk.Button(stock=gtk.STOCK_ZOOM_IN)
728     control_box.add(zi_button)
729     zi_button.connect("clicked", callback_wrapper(imageview.zoom_in))
730
731     zo_button = gtk.Button(stock=gtk.STOCK_ZOOM_OUT)
732     control_box.add(zo_button)
733     zo_button.connect("clicked", callback_wrapper(imageview.zoom_out))
734
735     next_button = gtk.Button(stock=gtk.STOCK_GO_FORWARD)
736     control_box.add(next_button)
737     next_button.connect("clicked", appstate.next_image)
738
739     prescale_checkbutton = gtk.CheckButton("Prescale mode")
740     prescale_checkbutton.set_active(True)
741     control_box.add(prescale_checkbutton)
742     prescale_checkbutton.connect("toggled", toggle_prescale_mode)
743
744     window.show_all()
745     window.connect("destroy", gtk.main_quit)
746
747     control_window.show_all()
748
749     appstate.next_image()
750     gtk.main()