OSDN Git Service

version bump
[mypaint-anime/master.git] / gui / tileddrawwidget.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2008 by Martin Renold <martinxyz@gmx.ch>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8
9 import gtk, gobject, cairo, random
10 gdk = gtk.gdk
11 from math import floor, ceil, pi, log
12 from numpy import isfinite
13 from warnings import warn
14
15 from lib import helpers, tiledsurface, pixbufsurface
16 import cursor
17
18
19
20 def _make_testbed_model():
21     warn("Creating standalone model for testing", RuntimeWarning, 2)
22     import lib.brush, lib.document
23     brush = lib.brush.BrushInfo()
24     brush.load_defaults()
25     return lib.document.Document(brush)
26
27
28
29 class TiledDrawWidget(gtk.DrawingArea):
30     """
31     This widget displays a document (../lib/document*.py).
32     
33     It can show the document translated, rotated or zoomed. It does
34     not respond to user input except for painting. Painting events are
35     passed to the document after applying the inverse transformation.
36     """
37
38     # Register a GType name for Glade, GtkBuilder etc.
39     __gtype_name__ = "TiledDrawWidget"
40
41     CANNOT_DRAW_CURSOR = gdk.Cursor(gdk.CIRCLE)
42
43     def __init__(self, app=None, document=None):
44         gtk.DrawingArea.__init__(self)
45         self.connect("expose-event", self.expose_cb)
46         self.connect("enter-notify-event", self.enter_notify_cb)
47         self.connect("leave-notify-event", self.leave_notify_cb)
48         self.connect("size-allocate", self.size_allocate_cb)
49         self.connect("state-changed", self.state_changed_cb)
50
51         # workaround for https://gna.org/bugs/?14372 ([Windows] crash when moving the pen during startup)
52         def at_application_start(*junk):
53             self.connect("motion-notify-event", self.motion_notify_cb)
54             self.connect("button-press-event", self.button_press_cb)
55             self.connect("button-release-event", self.button_release_cb)
56         gobject.idle_add(at_application_start)
57
58         self.set_events(gdk.EXPOSURE_MASK
59                         | gdk.POINTER_MOTION_MASK
60                         | gdk.ENTER_NOTIFY_MASK
61                         | gdk.LEAVE_NOTIFY_MASK
62                         # Workaround for https://gna.org/bugs/index.php?16253
63                         # Mypaint doesn't use proximity-*-event for anything
64                         # yet, but this seems to be needed for scrollwheels
65                         # etc. to keep working.
66                         | gdk.PROXIMITY_OUT_MASK
67                         | gdk.PROXIMITY_IN_MASK
68                         # for some reason we also need to specify events handled in drawwindow.py:
69                         | gdk.BUTTON_PRESS_MASK
70                         | gdk.BUTTON_RELEASE_MASK
71                         )
72
73         self.set_extension_events (gdk.EXTENSION_EVENTS_ALL)
74
75         self.app = app
76         if document is None:
77             document = _make_testbed_model()
78         self.doc = document
79         self.doc.canvas_observers.append(self.canvas_modified_cb)
80         self.doc.brush.brushinfo.observers.append(self.brush_modified_cb)
81
82         self.cursor_info = None
83
84         self.last_event_time = None
85         self.last_event_x = None
86         self.last_event_y = None
87         self.last_event_device = None
88         self.last_event_had_pressure_info = False
89         self.last_painting_pos = None
90         self.device_observers = [] #: Notified during drawing when input devices change
91         self._input_stroke_ended_observers = [] #: Access via gui.document
92
93         self.visualize_rendering = False
94
95         self.translation_x = 0.0
96         self.translation_y = 0.0
97         self.scale = 1.0
98         self.rotation = 0.0
99         self.mirrored = False
100
101         self.has_pointer = False
102         self.dragfunc = None
103
104         self.current_layer_solo = False
105         self.show_layers_above = True
106
107         self.overlay_layer = None
108
109         # gets overwritten for the main window
110         self.zoom_max = 5.0
111         self.zoom_min = 1/5.0
112
113         #self.scroll_at_edges = False
114         self.pressure_mapping = None
115         self.bad_devices = []
116         self.motions = []
117
118         # Sensitivity; we draw via a cached snapshot while the widget is
119         # insensitive. tdws are generally only insensitive during loading and
120         # saving, and because we now process the GTK main loop during loading
121         # and saving, we need to avoid drawing partially-loaded files.
122
123         self.is_sensitive = True    # just mirrors gtk.STATE_INSENSITIVE
124         self.snapshot_pixmap = None
125
126         self.override_cursor = None
127
128     #def set_scroll_at_edges(self, choice):
129     #    self.scroll_at_edges = choice
130
131     def state_changed_cb(self, widget, oldstate):
132         # Keeps track of the sensitivity state, and regenerates
133         # the snapshot pixbuf on entering it.
134         sensitive = self.get_state() != gtk.STATE_INSENSITIVE
135         if sensitive:
136             self.snapshot_pixmap = None
137         else:
138             if self.snapshot_pixmap is None:
139                 self.snapshot_pixmap = self.get_snapshot()
140         self.is_sensitive = sensitive
141
142     def enter_notify_cb(self, widget, event):
143         self.has_pointer = True
144     def leave_notify_cb(self, widget, event):
145         self.has_pointer = False
146
147     def size_allocate_cb(self, widget, allocation):
148         old_alloc = getattr(self, 'stored_allocation', allocation)
149         if old_alloc != allocation:
150             dx = allocation.x - old_alloc.x
151             dy = allocation.y - old_alloc.y
152             self.scroll(dx, dy)
153         self.stored_allocation = allocation
154
155     def device_used(self, device):
156         """Tell the TDW about a device being used."""
157         if device == self.last_event_device:
158             return True
159         for func in self.device_observers:
160             func(self.last_event_device, device)
161         self.last_event_device = device
162         return False
163
164         # Do not interpolate between motion events from different
165         # devices.  If the final pressure value from the previous
166         # device was not 0.0, the motion event of the new device could
167         # cause a visible stroke, even if pressure is 0.0.
168         self.doc.brush.reset()
169
170     def motion_notify_cb(self, widget, event, button1_pressed=None):
171         if not self.is_sensitive:
172             return
173         
174         if self.last_event_time:
175             dtime = (event.time - self.last_event_time)/1000.0
176             dx = event.x - self.last_event_x
177             dy = event.y - self.last_event_y
178         else:
179             dtime = None
180         self.last_event_x = event.x
181         self.last_event_y = event.y
182         self.last_event_time = event.time
183         if dtime is None:
184             return
185         
186         same_device = self.device_used(event.device)
187
188         if self.dragfunc:
189             self.dragfunc(dx, dy, event.x, event.y)
190             return
191
192         # Refuse drawing if the layer is locked or hidden
193         if self.doc.layer.locked or not self.doc.layer.visible:
194             return
195             # TODO: some feedback, maybe
196
197         cr = self.get_model_coordinates_cairo_context()
198         x, y = cr.device_to_user(event.x, event.y)
199         
200         pressure = event.get_axis(gdk.AXIS_PRESSURE)
201
202         if pressure is not None and (pressure > 1.0 or pressure < 0.0 or not isfinite(pressure)):
203             if event.device.name not in self.bad_devices:
204                 print 'WARNING: device "%s" is reporting bad pressure %+f' % (event.device.name, pressure)
205                 self.bad_devices.append(event.device.name)
206             if not isfinite(pressure):
207                 # infinity/nan: use button state (instead of clamping in brush.hpp)
208                 # https://gna.org/bugs/?14709
209                 pressure = None
210
211         if pressure is None:
212             self.last_event_had_pressure_info = False
213             if button1_pressed is None:
214                 button1_pressed = event.state & gdk.BUTTON1_MASK
215             if button1_pressed:
216                 pressure = 0.5
217             else:
218                 pressure = 0.0
219         else:
220             self.last_event_had_pressure_info = True
221
222         xtilt = event.get_axis(gdk.AXIS_XTILT)
223         ytilt = event.get_axis(gdk.AXIS_YTILT)
224         # Check whether tilt is present.  For some tablets without
225         # tilt support GTK reports a tilt axis with value nan, instead
226         # of None.  https://gna.org/bugs/?17084
227         if xtilt is None or ytilt is None or not isfinite(xtilt+ytilt):
228             xtilt = 0.0
229             ytilt = 0.0
230         
231         if event.state & gdk.CONTROL_MASK or event.state & gdk.MOD1_MASK:
232             # color picking, do not paint
233             # Don't simply return; this is a workaround for unwanted lines in https://gna.org/bugs/?16169
234             pressure = 0.0
235             
236         ### CSS experimental - scroll when touching the edge of the screen in fullscreen mode
237         #
238         # Disabled for the following reasons:
239         # - causes irritation when doing fast strokes near the edge
240         # - scrolling speed depends on the number of events received (can be a huge difference between tablets/mouse)
241         # - also, mouse button scrolling is usually enough
242         #
243         #if self.scroll_at_edges and pressure <= 0.0:
244         #  screen_w = gdk.screen_width()
245         #  screen_h = gdk.screen_height()
246         #  trigger_area = 10
247         #  if (event.x <= trigger_area):
248         #    self.scroll(-10,0)
249         #  if (event.x >= (screen_w-1)-trigger_area):
250         #    self.scroll(10,0)
251         #  if (event.y <= trigger_area):
252         #    self.scroll(0,-10)
253         #  if (event.y >= (screen_h-1)-trigger_area):
254         #    self.scroll(0,10)
255
256         if self.pressure_mapping:
257             pressure = self.pressure_mapping(pressure)
258         if event.state & gdk.SHIFT_MASK:
259             pressure = 0.0
260
261         if pressure:
262             self.last_painting_pos = x, y
263
264         # If the device has changed and the last pressure value from the previous device
265         # is not equal to 0.0, this can leave a visible stroke on the layer even if the 'new'
266         # device is not pressed on the tablet and has a pressure axis == 0.0.
267         # Reseting the brush when the device changes fixes this issue, but there may be a
268         # much more elegant solution that only resets the brush on this edge-case.
269         if not same_device:
270             self.doc.brush.reset()
271
272         # On Windows, GTK timestamps have a resolution around
273         # 15ms, but tablet events arrive every 8ms.
274         # https://gna.org/bugs/index.php?16569
275         # TODO: proper fix in the brush engine, using only smooth,
276         #       filtered speed inputs, will make this unneccessary
277         if dtime < 0.0:
278             print 'Time is running backwards, dtime=%f' % dtime
279             dtime = 0.0
280         data = (x, y, pressure, xtilt, ytilt)
281         if dtime == 0.0:
282             self.motions.append(data)
283         elif dtime > 0.0:
284             if self.motions:
285                 # replay previous events that had identical timestamp
286                 if dtime > 0.1:
287                     # really old events, don't associate them with the new one
288                     step = 0.1
289                 else:
290                     step = dtime
291                 step /= len(self.motions)+1
292                 for data_old in self.motions:
293                     self.doc.stroke_to(step, *data_old)
294                     dtime -= step
295                 self.motions = []
296             self.doc.stroke_to(dtime, *data)
297
298     def button_press_cb(self, win, event):
299         if event.type != gdk.BUTTON_PRESS:
300             # ignore the extra double-click event
301             return
302
303         if event.button == 1:
304             # mouse button pressed (while painting without pressure information)
305             if not self.last_event_had_pressure_info:
306                 # For the mouse we don't get a motion event for "pressure"
307                 # changes, so we simulate it. (Note: we can't use the
308                 # event's button state because it carries the old state.)
309                 self.motion_notify_cb(win, event, button1_pressed=True)
310
311     def button_release_cb(self, win, event):
312         # (see comment above in button_press_cb)
313         if event.button == 1 and not self.last_event_had_pressure_info:
314             self.motion_notify_cb(win, event, button1_pressed=False)
315         # Outsiders can access this via gui.document
316         for func in self._input_stroke_ended_observers:
317             func(event)
318
319     def straight_line_from_last_pos(self, is_sequence=False):
320         if not self.last_painting_pos:
321             return
322         dst = self.get_cursor_in_model_coordinates()
323         self.doc.straight_line(self.last_painting_pos, dst)
324         if is_sequence:
325             self.last_painting_pos = dst
326
327     def canvas_modified_cb(self, x1, y1, w, h):
328         if not self.window:
329             return
330         
331         if w == 0 and h == 0:
332             # full redraw (used when background has changed)
333             #print 'full redraw'
334             self.queue_draw()
335             return
336
337         cr = self.get_model_coordinates_cairo_context()
338
339         if self.is_translation_only():
340             x, y = cr.user_to_device(x1, y1)
341             self.queue_draw_area(int(x), int(y), w, h)
342         else:
343             # create an expose event with the event bbox rotated/zoomed
344             # OPTIMIZE: this is estimated to cause at least twice more rendering work than neccessary
345             # transform 4 bbox corners to screen coordinates
346             corners = [(x1, y1), (x1+w-1, y1), (x1, y1+h-1), (x1+w-1, y1+h-1)]
347             corners = [cr.user_to_device(x, y) for (x, y) in corners]
348             self.queue_draw_area(*helpers.rotated_rectangle_bbox(corners))
349
350     def expose_cb(self, widget, event):
351         self.update_cursor() # hack to get the initial cursor right
352         if self.snapshot_pixmap:
353             gc = self.get_style().fg_gc[self.get_state()]
354             area = event.area
355             x,y,w,h = area.x, area.y, area.width, area.height
356             self.window.draw_drawable(gc, self.snapshot_pixmap, x,y, x,y, w,h)
357         else:
358             self.repaint(event.area)
359         return True
360
361     def get_model_coordinates_cairo_context(self, cr=None):
362         # OPTIMIZE: check whether this is a bottleneck during painting (many motion events) - if yes, use cache
363         if cr is None:
364             cr = self.window.cairo_create()
365
366         scale = self.scale
367         # check if scale is almost a power of two
368         scale_log2 = log(scale, 2)
369         scale_log2_rounded = round(scale_log2)
370         if abs(scale_log2-scale_log2_rounded) < 0.01:
371             scale = 2.0**scale_log2_rounded
372
373         rotation = self.rotation # maybe we should check if rotation is almost a multiple of 90 degrees?
374
375         cr.translate(self.translation_x, self.translation_y)
376         cr.rotate(rotation)
377         cr.scale(scale, scale)
378
379         # Align the translation such that (0,0) maps to an integer
380         # screen pixel, to keep image rendering fast and sharp.
381         x, y = cr.user_to_device(0, 0)
382         x, y = cr.device_to_user(round(x), round(y))
383         cr.translate(x, y)
384
385         if self.mirrored:
386             m = list(cr.get_matrix())
387             m[0] = -m[0]
388             m[2] = -m[2]
389             cr.set_matrix(cairo.Matrix(*m))
390         return cr
391
392     def is_translation_only(self):
393         return self.rotation == 0.0 and self.scale == 1.0 and not self.mirrored
394
395     def get_cursor_in_model_coordinates(self):
396         x, y, modifiers = self.window.get_pointer()
397         cr = self.get_model_coordinates_cairo_context()
398         return cr.device_to_user(x, y)
399
400     def get_visible_layers(self):
401         # FIXME: tileddrawwidget should not need to know whether the document has layers
402         layers = self.doc.layers
403         if not self.show_layers_above:
404             layers = self.doc.layers[0:self.doc.layer_idx+1]
405         layers = [l for l in layers if l.visible]
406         return layers
407
408     def repaint(self, device_bbox=None):
409         cr, surface, sparse, mipmap_level, gdk_clip_region = self.render_prepare(device_bbox)
410         self.render_execute(cr, surface, sparse, mipmap_level, gdk_clip_region)
411
412     def render_prepare(self, device_bbox):
413         if device_bbox is None:
414             w, h = self.window.get_size()
415             device_bbox = (0, 0, w, h)
416         #print 'device bbox', tuple(device_bbox)
417
418         gdk_clip_region = self.window.get_clip_region()
419         x, y, w, h = device_bbox
420         sparse = not gdk_clip_region.point_in(x+w/2, y+h/2)
421
422         cr = self.window.cairo_create()
423
424         # actually this is only neccessary if we are not answering an expose event
425         cr.rectangle(*device_bbox)
426         cr.clip()
427
428         # fill it all white, though not required in the most common case
429         if self.visualize_rendering:
430             # grey
431             tmp = random.random()
432             cr.set_source_rgb(tmp, tmp, tmp)
433             cr.paint()
434
435         # bye bye device coordinates
436         self.get_model_coordinates_cairo_context(cr)
437
438         # choose best mipmap
439         hq_zoom = False
440         if self.app and self.app.preferences['view.high_quality_zoom']:
441             hq_zoom = True
442         if hq_zoom:
443             # can cause a very clear slowdown on some hardware
444             # (we probably could avoid this by doing rendering differently)
445             mipmap_level = max(0, int(floor(log(1.0/self.scale,2))))
446         else:
447             mipmap_level = max(0, int(ceil(log(1/self.scale,2))))
448         # OPTIMIZE: if we would render tile scanlines, we could probably use the better one above...
449         mipmap_level = min(mipmap_level, tiledsurface.MAX_MIPMAP_LEVEL)
450         cr.scale(2**mipmap_level, 2**mipmap_level)
451
452         translation_only = self.is_translation_only()
453
454         # calculate the final model bbox with all the clipping above
455         x1, y1, x2, y2 = cr.clip_extents()
456         if not translation_only:
457             # Looks like cairo needs one extra pixel rendered for interpolation at the border.
458             # If we don't do this, we get dark stripe artefacts when panning while zoomed.
459             x1 -= 1
460             y1 -= 1
461             x2 += 1
462             y2 += 1
463         x1, y1 = int(floor(x1)), int(floor(y1))
464         x2, y2 = int(ceil (x2)), int(ceil (y2))
465
466         # alpha=True is just to get hardware acceleration, we don't
467         # actually use the alpha channel. Speedup factor 3 for
468         # ATI/Radeon Xorg driver (and hopefully others).
469         # https://bugs.freedesktop.org/show_bug.cgi?id=28670
470         surface = pixbufsurface.Surface(x1, y1, x2-x1+1, y2-y1+1, alpha=True)
471
472         del x1, y1, x2, y2, w, h
473
474         return cr, surface, sparse, mipmap_level, gdk_clip_region
475
476     def render_execute(self, cr, surface, sparse, mipmap_level, gdk_clip_region):
477         translation_only = self.is_translation_only()
478         model_bbox = surface.x, surface.y, surface.w, surface.h
479
480         #print 'model bbox', model_bbox
481
482         # not sure if it is a good idea to clip so tightly
483         # has no effect right now because device_bbox is always smaller
484         cr.rectangle(*model_bbox)
485         cr.clip()
486
487         layers = self.get_visible_layers()
488
489         if self.visualize_rendering:
490             surface.pixbuf.fill((int(random.random()*0xff)<<16)+0x00000000)
491
492         tiles = surface.get_tiles()
493
494         background = None
495         if self.current_layer_solo:
496             background = self.neutral_background_pixbuf
497             layers = [self.doc.layer]
498             # this is for hiding instead
499             #layers.pop(self.doc.layer_idx)
500         if self.overlay_layer:
501             idx = layers.index(self.doc.layer)
502             layers.insert(idx+1, self.overlay_layer)
503
504         for tx, ty in tiles:
505             if sparse:
506                 # it is worth checking whether this tile really will be visible
507                 # (to speed up the L-shaped expose event during scrolling)
508                 # (speedup clearly visible; slowdown measurable when always executing this code)
509                 N = tiledsurface.N
510                 if translation_only:
511                     x, y = cr.user_to_device(tx*N, ty*N)
512                     bbox = (int(x), int(y), N, N)
513                 else:
514                     #corners = [(tx*N, ty*N), ((tx+1)*N-1, ty*N), (tx*N, (ty+1)*N-1), ((tx+1)*N-1, (ty+1)*N-1)]
515                     # same problem as above: cairo needs to know one extra pixel for interpolation
516                     corners = [(tx*N-1, ty*N-1), ((tx+1)*N, ty*N-1), (tx*N-1, (ty+1)*N), ((tx+1)*N, (ty+1)*N)]
517                     corners = [cr.user_to_device(x_, y_) for (x_, y_) in corners]
518                     bbox = gdk.Rectangle(*helpers.rotated_rectangle_bbox(corners))
519
520                 if gdk_clip_region.rect_in(bbox) == gdk.OVERLAP_RECTANGLE_OUT:
521                     continue
522
523
524             dst = surface.get_tile_memory(tx, ty)
525             self.doc.blit_tile_into(dst, tx, ty, mipmap_level, layers, background)
526
527         if translation_only:
528             # not sure why, but using gdk directly is notably faster than the same via cairo
529             x, y = cr.user_to_device(surface.x, surface.y)
530             self.window.draw_pixbuf(None, surface.pixbuf, 0, 0, int(x), int(y), dither=gdk.RGB_DITHER_MAX)
531         else:
532             #print 'Position (screen coordinates):', cr.user_to_device(surface.x, surface.y)
533             cr.set_source_pixbuf(surface.pixbuf, round(surface.x), round(surface.y))
534             pattern = cr.get_source()
535
536             # We could set interpolation mode here (eg nearest neighbour)
537             #pattern.set_filter(cairo.FILTER_NEAREST)  # 1.6s
538             #pattern.set_filter(cairo.FILTER_FAST)     # 2.0s
539             #pattern.set_filter(cairo.FILTER_GOOD)     # 3.1s
540             #pattern.set_filter(cairo.FILTER_BEST)     # 3.1s
541             #pattern.set_filter(cairo.FILTER_BILINEAR) # 3.1s
542
543             if self.scale > 3.0:
544                 # pixelize at high zoom-in levels
545                 pattern.set_filter(cairo.FILTER_NEAREST)
546
547             cr.paint()
548
549         if self.doc.frame_enabled:
550             # Draw a semi-transparent black overlay for
551             # all the area outside the "document area"
552             cr.save()
553             cr.set_source_rgba(0, 0, 0, 0.6)
554             cr.set_operator(cairo.OPERATOR_OVER)
555             mipmap_factor = 2**mipmap_level
556             frame = self.doc.get_frame()
557             cr.rectangle(frame[0]/mipmap_factor, frame[1]/mipmap_factor,
558                             frame[2]/mipmap_factor, frame[3]/mipmap_factor)
559             cr.rectangle(*model_bbox)
560             cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
561             cr.fill()
562             cr.restore()
563
564         if self.visualize_rendering:
565             # visualize painted bboxes (blue)
566             cr.set_source_rgba(0, 0, random.random(), 0.4)
567             cr.paint()
568
569     def scroll(self, dx, dy):
570         self.translation_x -= dx
571         self.translation_y -= dy
572         if False:
573             # This speeds things up nicely when scrolling is already
574             # fast, but produces temporary artefacts and an
575             # annoyingliy non-constant framerate otherwise.
576             #
577             # It might be worth it if it was done only once per
578             # redraw, instead of once per motion event. Maybe try to
579             # implement something like "queue_scroll" with priority
580             # similar to redraw?
581             self.window.scroll(int(-dx), int(-dy))
582         else:
583             self.queue_draw()
584
585     def get_center(self):
586         w, h = self.window.get_size()
587         return w/2.0, h/2.0
588
589     def rotozoom_with_center(self, function, at_pointer=False):
590         if at_pointer and self.has_pointer and self.last_event_x is not None:
591             cx, cy = self.last_event_x, self.last_event_y
592         else:
593             w, h = self.window.get_size()
594             cx, cy = self.get_center()
595         cr = self.get_model_coordinates_cairo_context()
596         cx_device, cy_device = cr.device_to_user(cx, cy)
597         function()
598         self.scale = helpers.clamp(self.scale, self.zoom_min, self.zoom_max)
599         cr = self.get_model_coordinates_cairo_context()
600         cx_new, cy_new = cr.user_to_device(cx_device, cy_device)
601         self.translation_x += cx - cx_new
602         self.translation_y += cy - cy_new
603
604         self.queue_draw()
605
606     def zoom(self, zoom_step):
607         def f(): self.scale *= zoom_step
608         self.rotozoom_with_center(f, at_pointer=True)
609
610     def set_zoom(self, zoom):
611         def f(): self.scale = zoom
612         self.rotozoom_with_center(f, at_pointer=True)
613
614     def rotate(self, angle_step):
615         if self.mirrored: angle_step = -angle_step
616         def f(): self.rotation += angle_step
617         self.rotozoom_with_center(f)
618
619     def set_rotation(self, angle):
620         if self.mirrored: angle = -angle
621         def f(): self.rotation = angle
622         self.rotozoom_with_center(f)
623
624     def mirror(self):
625         def f(): self.mirrored = not self.mirrored
626         self.rotozoom_with_center(f)
627
628     def set_mirrored(self, mirrored):
629         def f(): self.mirrored = mirrored
630         self.rotozoom_with_center(f)
631
632     def start_drag(self, dragfunc):
633         self.dragfunc = dragfunc
634     def stop_drag(self, dragfunc):
635         if self.dragfunc == dragfunc:
636             self.dragfunc = None
637
638     def recenter_document(self):
639         x, y, w, h = self.doc.get_effective_bbox()
640         desired_cx_user = x+w/2
641         desired_cy_user = y+h/2
642
643         cr = self.get_model_coordinates_cairo_context()
644         w, h = self.window.get_size()
645         cx_user, cy_user = cr.device_to_user(w/2, h/2)
646
647         self.translation_x += (cx_user - desired_cx_user)*self.scale
648         self.translation_y += (cy_user - desired_cy_user)*self.scale
649         self.queue_draw()
650
651     def brush_modified_cb(self, settings):
652         self.update_cursor()
653
654     def update_cursor(self):
655         if not self.window:
656             return
657         elif self.override_cursor is not None:
658             c = self.override_cursor
659         elif not self.is_sensitive:
660             c = None
661         elif self.doc.layer.locked or not self.doc.layer.visible:
662             c = self.CANNOT_DRAW_CURSOR
663         else:
664             b = self.doc.brush.brushinfo
665             radius = b.get_effective_radius()*self.scale
666             c = cursor.get_brush_cursor(radius, b.is_eraser(), b.get_base_value('lock_alpha') > 0.9)
667         self.window.set_cursor(c)
668
669     def set_override_cursor(self, cursor):
670         """Set a cursor which will always be used.
671
672         Used by the colour picker. The override cursor will be used regardless
673         of the criteria update_cursor() normally uses. Pass None to let it
674         choose normally again.
675         """
676         self.override_cursor = cursor
677         self.update_cursor()
678
679     def toggle_show_layers_above(self):
680         self.show_layers_above = not self.show_layers_above
681         self.queue_draw()
682
683
684 if __name__ == '__main__':
685     tdw = TiledDrawWidget()
686     tdw.set_size_request(640, 480)
687     win = gtk.Window()
688     win.set_title("tdw test")
689     win.connect("destroy", lambda *a: gtk.main_quit())
690     win.add(tdw)
691     win.show_all()
692     gtk.main()