Layermoving might cause NPE
[mypaint:librarian-mypaint.git] / gui / canvasevent.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2008-2012 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
8 """Canvas input event handling.
9 """
10
11 import pygtkcompat
12 from buttonmap import get_handler_object
13
14 import math
15 from numpy import isfinite
16
17 import gobject
18 import gtk
19 from gtk import gdk
20 from gtk import keysyms
21
22
23 # Actions it makes sense to bind to a button.
24 # Notably, tablet pads tend to offer many more buttons than the usual 3...
25
26 extra_actions = ["ShowPopupMenu",
27                  "Undo", "Redo",
28                  "Bigger", "Smaller",
29                  "MoreOpaque", "LessOpaque",
30                  "PickContext",
31                  "Fullscreen",
32                  "ToggleSubwindows",
33                  "BrushChooserPopup",
34                  "ColorRingPopup",
35                  "ColorDetailsDialog",
36                  "ColorChangerWashPopup",
37                  "ColorChangerCrossedBowlPopup",
38                  "ColorHistoryPopup",
39                  "PalettePrev",
40                  "PaletteNext",
41                  ]
42
43
44 class ModeRegistry (type):
45     """Lookup table for interaction modes and their associated actions
46
47     Operates as the metaclass for `InteractionMode`, so all you need to do to
48     create the association for a mode subclass is to define an
49     ``__action_name__`` entry in the class's namespace containing the name of
50     the associated `gtk.Action` defined in ``mypaint.xml``.
51
52     """
53
54     action_name_to_mode_class = {}
55
56
57     # (Special-cased @staticmethod)
58     def __new__(cls, name, bases, dict):
59         """Creates and records a new (InteractionMode) class.
60
61         :param cls: this metaclass
62         :param name: name of the class under construction
63         :param bases: immediate base classes of the class under construction
64         :param dict: class dict for the class under construction
65         :rtype: the constructed class, a regular InteractionMode class object
66
67         If it exists, the ``__action_name__`` entry in `dict` is recorded,
68         and can be used as a key for lookup of the returned class via the
69         ``@classmethod``s defined on `ModeRegistry`.
70
71         """
72         action_name = dict.get("__action_name__", None)
73         mode_class = super(ModeRegistry, cls).__new__(cls, name, bases, dict)
74         if action_name is not None:
75             action_name = str(action_name)
76             cls.action_name_to_mode_class[action_name] = mode_class
77         return mode_class
78
79
80     @classmethod
81     def get_mode_class(cls, action_name):
82         """Looks up a registered mode class by its associated action's name.
83
84         :param action_name: a string containing an action name (see this
85            metaclass's docs regarding the ``__action_name__`` class variable)
86         :rtype: an InteractionMode class object, or `None`.
87
88         """
89         return cls.action_name_to_mode_class.get(action_name, None)
90
91
92     @classmethod
93     def get_action_names(cls):
94         """Returns all action names associated with interaction.
95
96         :rtype: an iterable of action name strings.
97
98         """
99         return cls.action_name_to_mode_class.keys()
100
101
102 class InteractionMode (object):
103     """Required base class for temporary interaction modes.
104
105     Active interaction mode objects process input events, and can manipulate
106     document views (TiledDrawWidget), the document model data (lib.document),
107     and the mode stack they sit on. Interactions encapsulate state about their
108     particular kind of interaction; for example a drag interaction typically
109     contains the starting position for the drag.
110
111     Event handler methods can create new sub-modes and push them to the stack.
112     It is conventional to pass the current event to the equivalent method on
113     the new object when this transfer of control happens.
114
115     Subclasses may nominate a related `GtkAction` instance in the UI by setting
116     the class-level variable ``__action_name__``: this should be the name of an
117     action defined in `gui.app.Application.builder`'s XML file.
118
119     """
120
121
122     ## Class configuration
123
124     #: All InteractionMode subclasses register themselves.
125     __metaclass__ = ModeRegistry
126
127     #: See the docs for `gui.canvasevent.ModeRegistry`.
128     __action_name__ = None
129
130     is_live_updateable = False # CHECK: what's this for?
131
132     #: Timeout for Document.mode_flip_action_activated_cb(). How long, in
133     #: milliseconds, it takes for the controller to change the key-up action
134     #: when activated with a keyboard "Flip<ModeName>" action. Set to zero
135     #: for modes where key-up should exit the mode at any time, and to a larger
136     #: number for modes where the behaviour changes.
137     keyup_timeout = 500
138
139     ## Defaults for instances (sue me, I'm lazy)
140
141     #: The `gui.document.Document` this mode affects: see enter()
142     doc = None
143
144
145     def stackable_on(self, mode):
146         """Tests whether the mode can usefully stack onto an active mode.
147
148         :param mode: another mode object
149         :rtype: bool
150
151         This method should return True if this mode can usefully be stacked
152         onto another mode when switching via toolbars buttons or other actions.
153
154         """
155         return False
156
157
158     def enter(self, doc):
159         """Enters the mode: called by `ModeStack.push()` etc.
160
161         :param doc: the `gui.document.Document` this mode should affect.
162             A reference is kept in `self.doc`.
163
164         This is called when the mode becomes active, i.e. when it becomes the
165         top mode on a ModeStack, and before input is sent to it. Note that a
166         mode may be entered only to be left immediately: mode stacks are
167         cleared by repeated pop()ing.
168
169         """
170         self.doc = doc
171         assert not hasattr(super(InteractionMode, self), "enter")
172
173
174     def leave(self):
175         """Leaves the mode: called by `ModeStack.pop()` etc.
176
177         This is called when an active mode becomes inactive, i.e. when it is
178         no longer the top mode on its ModeStack.
179
180         """
181         self.doc = None
182         assert not hasattr(super(InteractionMode, self), "leave")
183
184
185     def button_press_cb(self, tdw, event):
186         """Handler for ``button-press-event``s."""
187         assert not hasattr(super(InteractionMode, self), "button_press_cb")
188
189
190     def motion_notify_cb(self, tdw, event):
191         """Handler for ``motion-notify-event``s."""
192         assert not hasattr(super(InteractionMode, self), "motion_notify_cb")
193
194
195     def button_release_cb(self, tdw, event):
196         """Handler for ``button-release-event``s."""
197         assert not hasattr(super(InteractionMode, self), "button_release_cb")
198
199
200     def scroll_cb(self, tdw, event):
201         """Handler for ``scroll-event``s.
202         """
203         assert not hasattr(super(InteractionMode, self), "scroll_cb")
204
205
206     def key_press_cb(self, win, tdw, event):
207         """Handler for ``key-press-event``s.
208
209         The base class implementation does nothing.
210         Keypresses are received by the main window only, but at this point it
211         has applied some heuristics to determine the active doc and view.
212         These are passed through to the active mode and are accessible to
213         keypress handlers via `self.doc` and the `tdw` argument.
214
215         """
216         assert not hasattr(super(InteractionMode, self), "key_press_cb")
217         return True
218
219
220     def key_release_cb(self, win, tdw, event):
221         """Handler for ``key-release-event``s.
222
223         The base class implementation does nothing. See `key_press_cb` for
224         details of the additional arguments.
225
226         """
227         assert not hasattr(super(InteractionMode, self), "key_release_cb")
228         return True
229
230
231     def model_structure_changed_cb(self, doc):
232         """Handler for model structural changes.
233
234         Only called for the top mode on the stack, when the document model
235         structure changes, so if a mode depends on the model structure, check
236         in enter() too.
237
238         """
239         assert not hasattr(super(InteractionMode, self),
240                            "model_structure_changed_cb")
241
242
243     ## Drag sub-API (FIXME: this is in the wrong place)
244     # Defined here to allow mixins to provide behaviour for both both drags and
245     # regular events without having to derive from DragMode. Really these
246     # buck-stops-here definitions belong in DragMode, so consider moving them
247     # somewhere more sensible.
248
249     def drag_start_cb(self, tdw, event):
250         assert not hasattr(super(InteractionMode, self), "drag_start_cb")
251
252     def drag_update_cb(self, tdw, event, dx, dy):
253         assert not hasattr(super(InteractionMode, self), "drag_update_cb")
254
255     def drag_stop_cb(self):
256         assert not hasattr(super(InteractionMode, self), "drag_stop_cb")
257
258
259     ## Internal utility functions
260
261     def current_modifiers(self):
262         """Returns the current set of modifier keys as a Gdk bitmask.
263
264         For use in handlers for keypress events when the key in question is
265         itself a modifier, handlers of multiple types of event, and when the
266         triggering event isn't available. Pointer button event handling should
267         use ``event.state & gtk.accelerator_get_default_mod_mask()``.
268
269         """
270         if pygtkcompat.USE_GTK3:
271             display = gdk.Display.get_default()
272         else:
273             display = gdk.display_get_default()
274         screen, x, y, modifiers = display.get_pointer()
275         modifiers &= gtk.accelerator_get_default_mod_mask()
276         return modifiers
277
278
279
280 class ScrollableModeMixin (InteractionMode):
281     """Mixin for scrollable modes.
282
283     Implements some immediate rotation and zoom commands for the scroll wheel.
284     These should be useful in many modes, but perhaps not all.
285
286     """
287
288     def scroll_cb(self, tdw, event):
289         """Handles scroll-wheel events.
290
291         Normal scroll wheel events: zoom.
292         Shift+scroll, or left/right scroll: rotation.
293
294         """
295         d = event.direction
296         if d == gdk.SCROLL_UP:
297             if event.state & gdk.SHIFT_MASK:
298                 self.doc.rotate('RotateLeft')
299                 return True
300             else:
301                 self.doc.zoom('ZoomIn')
302                 return True
303         elif d == gdk.SCROLL_DOWN:
304             if event.state & gdk.SHIFT_MASK:
305                 self.doc.rotate('RotateRight')
306                 return True
307             else:
308                 self.doc.zoom('ZoomOut')
309                 return True
310         elif d == gdk.SCROLL_RIGHT:
311             self.doc.rotate('RotateRight')
312             return True
313         elif d == gdk.SCROLL_LEFT:
314             self.doc.rotate('RotateLeft')
315             return True
316         return super(ScrollableModeMixin, self).scroll_cb(tdw, event)
317
318
319 class FreehandOnlyMode (InteractionMode):
320     """A freehand-only drawing mode, which cannot be switched with modifiers.
321
322     This mode can be used with the basic CanvasController, and in the absence
323     of the main application.
324
325     """
326
327     is_live_updateable = True
328
329
330     def enter(self, **kwds):
331         super(FreehandOnlyMode, self).enter(**kwds)
332         self.reset_drawing_state()
333
334
335     def leave(self, **kwds):
336         self.reset_drawing_state()
337         super(FreehandOnlyMode, self).leave(**kwds)
338
339
340     def reset_drawing_state(self):
341         self.last_event_had_pressure_info = False
342         # Windows stuff
343         self.motions = []
344
345
346     def button_press_cb(self, tdw, event):
347         result = False
348         if event.button == 1 and event.type == gdk.BUTTON_PRESS:
349             # Single button press
350             # Stroke started, notify observers
351             try:
352                 observers = self.doc.input_stroke_started_observers
353             except AttributeError:
354                 pass
355             else:
356                 for func in observers:
357                     func(event)
358             # Mouse button pressed (while painting without pressure
359             # information)
360             if not self.last_event_had_pressure_info:
361                 # For the mouse we don't get a motion event for "pressure"
362                 # changes, so we simulate it. (Note: we can't use the
363                 # event's button state because it carries the old state.)
364                 self.motion_notify_cb(tdw, event, button1_pressed=True)
365             result = True
366
367         # Collaborate, but likely with nothing
368         result |= bool(super(FreehandOnlyMode, self).button_press_cb(tdw, event))
369         return result
370
371
372     def button_release_cb(self, tdw, event):
373         # (see comment above in button_press_cb)
374         result = False
375         if event.button == 1:
376             if not self.last_event_had_pressure_info:
377                 self.motion_notify_cb(tdw, event, button1_pressed=False)
378             # Notify observers after processing the event
379             try:
380                 observers = self.doc.input_stroke_ended_observers
381             except AttributeError:
382                 pass
383             else:
384                 for func in observers:
385                     func(event)
386             result = True
387         result |= bool(super(FreehandOnlyMode, self).button_release_cb(tdw, event))
388         return result
389
390
391     def motion_notify_cb(self, tdw, event, button1_pressed=None):
392         # Purely responsible for drawing.
393         if not tdw.is_sensitive:
394             return super(FreehandOnlyMode, self).motion_notify_cb(tdw, event)
395
396         model = tdw.doc
397         app = tdw.app
398
399         last_event_time, last_x, last_y = self.doc.get_last_event_info(tdw)
400         if last_event_time:
401             dtime = (event.time - last_event_time)/1000.0
402             dx = event.x - last_x
403             dy = event.y - last_y
404         else:
405             dtime = None
406         if dtime is None:
407             return super(FreehandOnlyMode, self).motion_notify_cb(tdw, event)
408
409         same_device = True
410         if app is not None:
411             same_device = app.device_monitor.device_used(event.device)
412
413         # Refuse drawing if the layer is locked or hidden
414         if model.layer.locked or not model.layer.visible:
415             return super(FreehandOnlyMode, self).motion_notify_cb(tdw, event)
416             # TODO: some feedback, maybe
417
418         x, y = tdw.display_to_model(event.x, event.y)
419
420         pressure = event.get_axis(gdk.AXIS_PRESSURE)
421
422         if pressure is not None and (   pressure > 1.0
423                                      or pressure < 0.0
424                                      or not isfinite(pressure)):
425             if not hasattr(self, 'bad_devices'):
426                 self.bad_devices = []
427             if event.device.name not in self.bad_devices:
428                 print 'WARNING: device "%s" is reporting bad pressure %+f' \
429                     % (event.device.name, pressure)
430                 self.bad_devices.append(event.device.name)
431             if not isfinite(pressure):
432                 # infinity/nan: use button state (instead of clamping in
433                 # brush.hpp) https://gna.org/bugs/?14709
434                 pressure = None
435
436         # Fake pressure if we have none based on the extra argument passed if
437         # this is a fake.
438         if pressure is None:
439             self.last_event_had_pressure_info = False
440             if button1_pressed is None:
441                 button1_pressed = event.state & gdk.BUTTON1_MASK
442             if button1_pressed:
443                 pressure = 0.5
444             else:
445                 pressure = 0.0
446         else:
447             self.last_event_had_pressure_info = True
448
449         xtilt = event.get_axis(gdk.AXIS_XTILT)
450         ytilt = event.get_axis(gdk.AXIS_YTILT)
451         # Check whether tilt is present.  For some tablets without
452         # tilt support GTK reports a tilt axis with value nan, instead
453         # of None.  https://gna.org/bugs/?17084
454         if xtilt is None or ytilt is None or not isfinite(xtilt+ytilt):
455             xtilt = 0.0
456             ytilt = 0.0
457
458         # Tilt inputs are assumed to be relative to the viewport, but the
459         # canvas may be rotated or mirrored, or both. Compensate before
460         # passing them to the brush engine. https://gna.org/bugs/?19988
461         if not (xtilt == 0 and ytilt == 0):
462             if tdw.mirrored:
463                 xtilt *= -1.0
464             if tdw.rotation != 0:
465                 tilt_angle = math.atan2(ytilt, xtilt) - tdw.rotation
466                 tilt_magnitude = math.sqrt((xtilt**2) + (ytilt**2))
467                 xtilt = tilt_magnitude * math.cos(tilt_angle)
468                 ytilt = tilt_magnitude * math.sin(tilt_angle)
469
470         if event.state & gdk.CONTROL_MASK or event.state & gdk.MOD1_MASK:
471             # HACK: color picking, do not paint
472             # Don't simply return; this is a workaround for unwanted lines
473             # in https://gna.org/bugs/?16169
474             pressure = 0.0
475
476         if app is not None and app.pressure_mapping:
477             pressure = app.pressure_mapping(pressure)
478
479         if event.state & gdk.SHIFT_MASK:
480             pressure = 0.0
481
482         if pressure:
483             tdw.set_last_painting_pos((x, y))
484
485         # If the device has changed and the last pressure value from the
486         # previous device is not equal to 0.0, this can leave a visible stroke
487         # on the layer even if the 'new' device is not pressed on the tablet
488         # and has a pressure axis == 0.0.  Reseting the brush when the device
489         # changes fixes this issue, but there may be a much more elegant
490         # solution that only resets the brush on this edge-case.
491
492         if not same_device:
493             model.brush.reset()
494
495         # On Windows, GTK timestamps have a resolution around
496         # 15ms, but tablet events arrive every 8ms.
497         # https://gna.org/bugs/index.php?16569
498         # TODO: proper fix in the brush engine, using only smooth,
499         #       filtered speed inputs, will make this unneccessary
500         if dtime < 0.0:
501             print 'Time is running backwards, dtime=%f' % dtime
502             dtime = 0.0
503         data = (x, y, pressure, xtilt, ytilt)
504         if dtime == 0.0:
505             self.motions.append(data)
506         elif dtime > 0.0:
507             if self.motions:
508                 # replay previous events that had identical timestamp
509                 if dtime > 0.1:
510                     # really old events, don't associate them with the new one
511                     step = 0.1
512                 else:
513                     step = dtime
514                 step /= len(self.motions)+1
515                 for data_old in self.motions:
516                     model.stroke_to(step, *data_old)
517                     dtime -= step
518                 self.motions = []
519             model.stroke_to(dtime, *data)
520
521         super(FreehandOnlyMode, self).motion_notify_cb(tdw, event)
522         return True
523
524
525 class SwitchableModeMixin (InteractionMode):
526     """Adds functionality for performing actions via modifiers & ptr buttons
527
528     Mode switching happens in response to button- or key-press events, using
529     the main app's ``button_mapping`` to look up action names. These actions
530     can switch control to other modes by pushing them onto the mode stack;
531     they can invoke popup States; or they can trigger regular GtkActions.
532
533     Not every switchable mode can perform any such action. Subclasses should
534     name actions they can invoke in ``permitted_switch_actions``. If this set
535     is left empty, any action can be performed
536
537     """
538
539     permitted_switch_actions = set()  #: Optional whitelist for mode switching
540
541
542     def button_press_cb(self, tdw, event):
543         """Button-press event handler. Permits switching."""
544
545         # Never switch in the middle of an active drag (see DragMode)
546         if getattr(self, 'in_drag', False):
547             return super(SwitchableModeMixin, self).button_press_cb(tdw, event)
548
549         # Ignore accidental presses
550         if event.type != gdk.BUTTON_PRESS:
551             # Single button-presses only, not 2ble/3ple
552             return super(SwitchableModeMixin, self).button_press_cb(tdw, event)
553         if event.button != 1:
554             # check whether we are painting (accidental)
555             if event.state & gdk.BUTTON1_MASK:
556                 # Do not allow mode switching in the middle of
557                 # painting. This often happens by accident with wacom
558                 # tablet's stylus button.
559                 #
560                 # However we allow dragging if the user's pressure is
561                 # still below the click threshold.  This is because
562                 # some tablet PCs are not able to produce a
563                 # middle-mouse click without reporting pressure.
564                 # https://gna.org/bugs/index.php?15907
565                 return super(SwitchableModeMixin,
566                              self).button_press_cb(tdw, event)
567
568         # Look up action
569         btn_map = self.doc.app.button_mapping
570         modifiers = event.state & gtk.accelerator_get_default_mod_mask()
571         action_name = btn_map.lookup(modifiers, event.button)
572
573         # Forbid actions not named in the whitelist, if it's defined
574         if len(self.permitted_switch_actions) > 0:
575             if action_name not in self.permitted_switch_actions:
576                 action_name = None
577
578         # Perform allowed action if one was looked up
579         if action_name is not None:
580             return self._dispatch_named_action(None, tdw, event, action_name)
581
582         # Otherwise fall through to the next behaviour
583         return super(SwitchableModeMixin, self).button_press_cb(tdw, event)
584
585
586     def key_press_cb(self, win, tdw, event):
587         """Keypress event handler. Permits switching."""
588
589         # Never switch in the middle of an active drag (see DragMode)
590         if getattr(self, 'in_drag', False):
591             return super(SwitchableModeMixin,self).key_press_cb(win,tdw,event)
592
593         # Naively pick an action based on the button map
594         btn_map = self.doc.app.button_mapping
595         action_name = None
596         if event.is_modifier:
597             # If the keypress is a modifier only, determine the modifier mask a
598             # subsequent Button1 press event would get. This is used for early
599             # spring-loaded mode switching.
600             mods = self.current_modifiers()
601             action_name = btn_map.get_unique_action_for_modifiers(mods)
602             # Only mode-based immediate dispatch is allowed, however.
603             # Might relax this later.
604             if action_name is not None:
605                 if not action_name.endswith("Mode"):
606                     action_name = None
607         else:
608             # Strategy 2: pretend that the space bar is really button 2.
609             if event.keyval == keysyms.space:
610                 mods = event.state & gtk.accelerator_get_default_mod_mask()
611                 action_name = btn_map.lookup(mods, 2)
612
613         # Forbid actions not named in the whitelist, if it's defined
614         if len(self.permitted_switch_actions) > 0:
615             if action_name not in self.permitted_switch_actions:
616                 action_name = None
617
618         # If we found something to do, dispatch
619         if action_name is not None:
620             return self._dispatch_named_action(win, tdw, event, action_name)
621
622         # Otherwise, fall through to the next behaviour
623         return super(SwitchableModeMixin, self).key_press_cb(win, tdw, event)
624
625
626     def _dispatch_named_action(self, win, tdw, event, action_name):
627         # Send a named action from the button map to some handler code
628         app = tdw.app
629         drawwindow = app.drawWindow
630         tdw.doc.split_stroke()
631         if action_name == 'ShowPopupMenu':
632             # Unfortunately still a special case.
633             # Just firing the action doesn't work well with pads which fire a
634             # button-release event immediately after the button-press.
635             # Name it after the action however, in case we find a fix.
636             drawwindow.show_popupmenu(event=event)
637             return True
638         handler_type, handler = get_handler_object(app, action_name)
639         if handler_type == 'mode_class':
640             # Transfer control to another mode temporarily.
641             assert issubclass(handler, SpringLoadedModeMixin)
642             mode = handler()
643             self.doc.modes.push(mode)
644             if win is not None:
645                 return mode.key_press_cb(win, tdw, event)
646             else:
647                 return mode.button_press_cb(tdw, event)
648         elif handler_type == 'popup_state':
649             # Still needed. The code is more tailored to MyPaint's
650             # purposes. The names are action names, but have the more
651             # tailored popup states code shadow generic action activation.
652             if win is not None:
653                 # WORKAROUND: dispatch keypress events via the kbm so it can
654                 # keep track of pressed-down keys. Popup states become upset if
655                 # this doesn't happen: https://gna.org/bugs/index.php?20325
656                 action = app.find_action(action_name)
657                 return app.kbm.activate_keydown_event(action, event)
658             else:
659                 # Pointer: popup states handle these themselves sanely.
660                 handler.activate(event)
661                 return True
662         elif handler_type == 'gtk_action':
663             # Generic named action activation. GtkActions trigger without
664             # event details, so they're less flexible.
665             # Hack: Firing the action in an idle handler helps with
666             # actions that are sensitive to immediate button-release
667             # events. But not ShowPopupMenu, sadly: we'd break button
668             # hold behaviour for more reasonable devices if we used
669             # this trick.
670             gobject.idle_add(handler.activate)
671             return True
672         else:
673             return False
674
675
676 class SwitchableFreehandMode (SwitchableModeMixin, ScrollableModeMixin,
677                               FreehandOnlyMode):
678     """The default mode: freehand drawing, accepting modifiers to switch modes.
679     """
680
681     __action_name__ = 'SwitchableFreehandMode'
682     permitted_switch_actions = set()   # Any action is permitted
683
684     def __init__(self, ignore_modifiers=True, **args):
685         # Ignore the additional arg that flip actions feed us
686         super(SwitchableFreehandMode, self).__init__(**args)
687
688
689
690 class ModeStack (object):
691     """A stack of InteractionModes. The top of the stack is the active mode.
692
693     The stack can never be empty: if the final element is popped, it will be
694     replaced with a new instance of its `default_mode_class`.
695
696     """
697
698     #: Class to instantiate if stack is empty: callable with 0 args.
699     default_mode_class = SwitchableFreehandMode
700
701
702     def __init__(self, doc):
703         """Initialize, associated with a particular CanvasController (doc)
704
705         :param doc: Controller instance: the main MyPaint app uses
706             an instance of `gui.document.Document`. Simpler drawing
707             surfaces can use a basic CanvasController and a
708             simpler `default_mode_class`.
709         :type doc: CanvasController
710         """
711         object.__init__(self)
712         self._stack = []
713         self._doc = doc
714         self.observers = []
715
716
717     def _notify_observers(self):
718         top_mode = self._stack[-1]
719         for func in self.observers:
720             func(top_mode)
721
722
723     @property
724     def top(self):
725         """The top node on the stack.
726         """
727         # Perhaps rename to "active()"?
728         new_mode = self._check()
729         if new_mode is not None:
730             new_mode.enter(doc=self._doc)
731             self._notify_observers()
732         return self._stack[-1]
733
734
735     def context_push(self, mode):
736         """Context-aware push.
737
738         :param mode: mode to be stacked and made active
739         :type mode: `InteractionMode`
740
741         Stacks a mode onto the topmost element in the stack it is compatible
742         with, as determined by its ``stackable_on()`` method. Incompatible
743         top modes are popped one by one until either a compatible mode is
744         found, or the stack is emptied, then the new mode is pushed.
745
746         """
747         # Pop until the stack is empty, or the top mode is compatible
748         while len(self._stack) > 0:
749             if mode.stackable_on(self._stack[-1]):
750                 break
751             self._stack.pop(-1).leave()
752             if len(self._stack) > 0:
753                 self._stack[-1].enter(doc=self._doc)
754         # Stack on top of any remaining compatible mode
755         if len(self._stack) > 0:
756             self._stack[-1].leave()
757         self._doc.model.split_stroke()
758         self._stack.append(mode)
759         mode.enter(doc=self._doc)
760         self._notify_observers()
761
762
763     def pop(self):
764         """Pops a mode, leaving the old top mode and entering the exposed top.
765         """
766         if len(self._stack) > 0:
767             old_mode = self._stack.pop(-1)
768             old_mode.leave()
769         top_mode = self._check()
770         if top_mode is None:
771             top_mode = self._stack[-1]
772         self._doc.model.split_stroke()
773         top_mode.enter(doc=self._doc)
774         self._notify_observers()
775
776
777     def push(self, mode):
778         """Pushes a mode, and enters it.
779         """
780         if len(self._stack) > 0:
781             self._stack[-1].leave()
782         self._doc.model.split_stroke()
783         self._stack.append(mode)
784         mode.enter(doc=self._doc)
785         self._notify_observers()
786
787
788     def reset(self, replacement=None):
789         """Clears the stack, popping the final element and replacing it.
790
791         :param replacement: Optional mode to go on top of the cleared stack.
792         :type replacement: `InteractionMode`.
793
794         """
795         while len(self._stack) > 0:
796             old_mode = self._stack.pop(-1)
797             old_mode.leave()
798             if len(self._stack) > 0:
799                 self._stack[-1].enter(doc=self._doc)
800         top_mode = self._check(replacement)
801         assert top_mode is not None
802         self._notify_observers()
803
804
805     def _check(self, replacement=None):
806         # Ensures that the stack is non-empty.
807         # Returns the new top mode if one was pushed.
808         if len(self._stack) > 0:
809             return None
810         if replacement is not None:
811             mode = replacement
812         else:
813             mode = self.default_mode_class()
814         self._stack.append(mode)
815         mode.enter(doc=self._doc)
816         return mode
817
818
819     def __repr__(self):
820         s = '<ModeStack ['
821         s += ", ".join([m.__class__.__name__ for m in self._stack])
822         s += ']>'
823         return s
824
825
826     def __len__(self):
827         return len(self._stack)
828
829
830 class SpringLoadedModeMixin (InteractionMode):
831     """Behavioural add-ons for modes which last as long as modifiers are held.
832
833     When a spring-loaded mode is first entered, it remembers which modifier
834     keys were held down at that time. When keys are released, if the held
835     modifiers are no longer held down, the mode stack is popped and the mode
836     exits.
837
838     """
839
840
841     def __init__(self, ignore_modifiers=False, **kwds):
842         """Construct, possibly ignoring initial modifiers.
843
844         :param ignore_modifiers: If True, ignore the initial set of modifiers.
845
846         Springloaded modes can be instructed to ignore the initial set of
847         modifiers when they're entered. This is appropriate when the mode is
848         being entered in response to a keyboard shortcut. Modifiers don't mean
849         the same thing for keyboard shortcuts. Conversely, toolbar buttons and
850         mode-switching via pointer buttons should use the default behaviour.
851
852         In practice, it's not quite so clear cut. Instead we have keyboard-
853         friendly "Flip*" actions (which allow the mode to be toggled off with a
854         second press) that use the ``ignore_modifiers`` behaviour, and a
855         secondary layer of radioactions which don't (but which reflect the
856         state prettily).
857
858         """
859         super(SpringLoadedModeMixin, self).__init__(**kwds)
860         self.ignore_modifiers = ignore_modifiers
861
862
863     def enter(self, **kwds):
864         """Enter the mode, recording the held modifier keys the first time.
865
866         The attribute `self.initial_modifiers` is set the first time the mode
867         is entered.
868
869         """
870
871         super(SpringLoadedModeMixin, self).enter(**kwds)
872         assert self.doc is not None
873
874         if self.ignore_modifiers:
875             self.initial_modifiers = 0
876             return
877
878         old_modifiers = getattr(self, "initial_modifiers", None)
879         if old_modifiers is not None:
880             # Re-entering due to an overlying mode being popped from the stack.
881             if old_modifiers != 0:
882                 # This mode started with modifiers held the first time round,
883                 modifiers = self.current_modifiers()
884                 if (modifiers & old_modifiers) == 0:
885                     # But they're not held any more, so queue a further pop.
886                     gobject.idle_add(self.__pop_modestack_idle_cb)
887         else:
888             # This mode is being entered for the first time; record modifiers
889             modifiers = self.current_modifiers()
890             self.initial_modifiers = self.current_modifiers()
891
892
893     def __pop_modestack_idle_cb(self):
894         # Pop the mode stack when this mode is re-entered but has to leave
895         # straight away because its modifiers are no longer held. Doing it in
896         # an idle function avoids confusing the derived class's enter() method:
897         # a leave() during an enter() would be strange.
898         if self.initial_modifiers is not None:
899             self.doc.modes.pop()
900         return False
901
902
903     def key_release_cb(self, win, tdw, event):
904         """Leave the mode if the initial modifier keys are no longer held.
905
906         If the spring-loaded mode leaves because the modifiers keys held down
907         when it was entered are no longer held, this method returns True, and
908         so should the supercaller.
909
910         """
911         if self.initial_modifiers:
912             modifiers = self.current_modifiers()
913             if modifiers & self.initial_modifiers == 0:
914                 self.doc.modes.pop()
915                 return True
916         return super(SpringLoadedModeMixin,self).key_release_cb(win,tdw,event)
917
918
919 class DragMode (InteractionMode):
920     """Base class for drag activities.
921
922     The drag can be entered when the pen is up or down: if the pen is down, the
923     initial position will be determined from the first motion event.
924
925     """
926
927     inactive_cursor = gdk.Cursor(gdk.BOGOSITY)
928     active_cursor = None
929
930     def __init__(self, **kwds):
931         super(DragMode, self).__init__(**kwds)
932         self._grab_broken_conninfo = None
933         self._reset_drag_state()
934
935
936     def _reset_drag_state(self):
937         self.last_x = None
938         self.last_y = None
939         self.start_x = None
940         self.start_y = None
941         self._start_keyval = None
942         self._start_button = None
943         self._grab_widget = None
944         if self._grab_broken_conninfo is not None:
945             tdw, connid = self._grab_broken_conninfo
946             tdw.disconnect(connid)
947             self._grab_broken_conninfo = None
948
949
950     def _stop_drag(self, t=gdk.CURRENT_TIME):
951         # Stops any active drag, calls drag_stop_cb(), and cleans up.
952         if not self.in_drag:
953             return
954         tdw = self._grab_widget
955         tdw.grab_remove()
956         gdk.keyboard_ungrab(t)
957         gdk.pointer_ungrab(t)
958         self._grab_widget = None
959         self.drag_stop_cb()
960         self._reset_drag_state()
961
962
963     def _start_drag(self, tdw, event):
964         # Attempt to start a new drag, calling drag_start_cb() if successful.
965         if self.in_drag:
966             return
967         if hasattr(event, "x"):
968             self.start_x = event.x
969             self.start_y = event.y
970         else:
971             #last_x, last_y = tdw.get_pointer()
972             last_t, last_x, last_y = self.doc.get_last_event_info(tdw)
973             self.start_x = last_x
974             self.start_y = last_y
975         tdw_window = tdw.get_window()
976         event_mask = gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK \
977                    | gdk.POINTER_MOTION_MASK
978         cursor = self.active_cursor
979         if cursor is None:
980             cursor = self.inactive_cursor
981
982         # Grab the pointer
983         grab_status = gdk.pointer_grab(tdw_window, False, event_mask, None,
984                                        cursor, event.time)
985         if grab_status != gdk.GRAB_SUCCESS:
986             print "DEBUG: pointer grab failed:", grab_status
987             print "DEBUG:  gdk_pointer_is_grabbed():", gdk.pointer_is_grabbed()
988             # There seems to be a race condition between this grab under
989             # PyGTK/GTK2 and some other grab - possibly just the implicit grabs
990             # on colour selectors: https://gna.org/bugs/?20068 Only pointer
991             # events are affected, and PyGI+GTK3 is unaffected.
992             #
993             # It's probably safest to exit the mode and not start the drag.
994             # This condition should be rare enough for this to be a valid
995             # approach: the irritation of having to click again to do something
996             # should be far less than that of getting "stuck" in a drag.
997             print "DEBUG: exiting mode"
998             self.doc.modes.pop()
999
1000             # Sometimes a pointer ungrab is needed even though the grab
1001             # apparently failed to avoid the UI partially "locking up" with the
1002             # stylus (and only the stylus). Happens when WMs like Xfwm
1003             # intercept an <Alt>Button combination for window management
1004             # purposes. Results in gdk.GRAB_ALREADY_GRABBED, but this line is
1005             # necessary to avoid the rest of the UI becoming unresponsive even
1006             # though the canvas can be drawn on with the stylus. Are we
1007             # cancelling an implicit grab here, and why is it device specific?
1008             gdk.pointer_ungrab()
1009             return
1010
1011         # We managed to establish a grab, so watch for it being broken.
1012         # This signal is disconnected when the mode leaves.
1013         connid = tdw.connect("grab-broken-event", self.tdw_grab_broken_cb)
1014         self._grab_broken_conninfo = (tdw, connid)
1015
1016         # Grab the keyboard too, to be certain of getting the key release event
1017         # for a spacebar drag.
1018         grab_status = gdk.keyboard_grab(tdw_window, False, event.time)
1019         if grab_status != gdk.GRAB_SUCCESS:
1020             print "DEBUG: keyboard grab failed:", grab_status
1021             gdk.pointer_ungrab()
1022             self.doc.modes.pop()
1023             return
1024
1025         # GTK too...
1026         tdw.grab_add()
1027         self._grab_widget = tdw
1028
1029         # Drag has started, perform whatever action the mode needs.
1030         self.drag_start_cb(tdw, event)
1031
1032         ## Break the grab after a while for debugging purposes
1033         #gobject.timeout_add_seconds(5, self.__break_own_grab_cb, tdw, False)
1034
1035
1036     def __break_own_grab_cb(self, tdw, fake=False):
1037         if fake:
1038             ev = gdk.Event(gdk.GRAB_BROKEN)
1039             ev.window = tdw.get_window()
1040             ev.send_event = True
1041             ev.put()
1042         else:
1043             import os
1044             os.system("wmctrl -s 0")
1045         return False
1046
1047
1048     def tdw_grab_broken_cb(self, tdw, event):
1049         # Cede control as cleanly as possible if something else grabs either
1050         # the keyboard or the pointer while a grab is active.
1051         # One possible cause for https://gna.org/bugs/?20333
1052         print "DEBUG: grab-broken-event on", tdw
1053         print "DEBUG:   send_event  :", event.send_event
1054         print "DEBUG:   keyboard    :", event.keyboard
1055         print "DEBUG:   implicit    :", event.implicit
1056         print "DEBUG:   grab_window :", event.grab_window
1057         print "DEBUG: exiting", self
1058         self.doc.modes.pop()
1059         return True
1060
1061
1062     @property
1063     def in_drag(self):
1064         return self._grab_widget is not None
1065
1066
1067     def enter(self, **kwds):
1068         super(DragMode, self).enter(**kwds)
1069         assert self.doc is not None
1070         self.doc.tdw.set_override_cursor(self.inactive_cursor)
1071
1072
1073     def leave(self, **kwds):
1074         self._stop_drag()
1075         if self.doc is not None:
1076             self.doc.tdw.set_override_cursor(None)
1077         super(DragMode, self).leave(**kwds)
1078
1079
1080     def button_press_cb(self, tdw, event):
1081         if event.type == gdk.BUTTON_PRESS:
1082             if self.in_drag:
1083                 if self._start_button is None:
1084                     # Doing this allows single clicks to exit keyboard
1085                     # initiated drags, e.g. those forced when handling a
1086                     # keyboard event somewhere else.
1087                     self._start_button = event.button
1088             else:
1089                 self._start_drag(tdw, event)
1090                 if self.in_drag:
1091                     # Grab succeeded
1092                     self.last_x = event.x
1093                     self.last_y = event.y
1094                     self._start_button = event.button
1095         return super(DragMode, self).button_press_cb(tdw, event)
1096
1097
1098     def button_release_cb(self, tdw, event):
1099         if self.in_drag:
1100             if event.button == self._start_button:
1101                 self._stop_drag()
1102         return super(DragMode, self).button_release_cb(tdw, event)
1103
1104
1105     def motion_notify_cb(self, tdw, event):
1106         # We might be here because an Action manipulated the modes stack
1107         # but if that's the case then we should wait for a button or
1108         # a keypress to initiate the drag.
1109         if self.in_drag:
1110             if self.last_x is not None:
1111                 dx = event.x - self.last_x
1112                 dy = event.y - self.last_y
1113                 self.drag_update_cb(tdw, event, dx, dy)
1114             self.last_x = event.x
1115             self.last_y = event.y
1116             return True
1117         # Fall through to other behavioral mixins, just in case
1118         return super(DragMode, self).motion_notify_cb(tdw, event)
1119
1120
1121     def key_press_cb(self, win, tdw, event):
1122         if self.in_drag:
1123             # Eat keypresses in the middle of a drag no matter how
1124             # it was started.
1125             return True
1126         elif event.keyval == keysyms.space:
1127             # Start drags on space
1128             if event.keyval != self._start_keyval:
1129                 self._start_keyval = event.keyval
1130                 self._start_drag(tdw, event)
1131             return True
1132         # Fall through to other behavioral mixins
1133         return super(DragMode, self).key_press_cb(win, tdw, event)
1134
1135
1136     def key_release_cb(self, win, tdw, event):
1137         if self.in_drag:
1138             if event.keyval == self._start_keyval:
1139                 self._stop_drag()
1140                 self._start_keyval = None
1141             return True
1142         # Fall through to other behavioral mixins
1143         return super(DragMode, self).key_release_cb(win, tdw, event)
1144
1145
1146     def _force_drag_start(self):
1147         # Attempt to force a drag to start, using the current event.
1148         event = gtk.get_current_event()
1149         self._fake_drag_start(event)
1150
1151
1152     def _fake_drag_start(self, event):
1153         if event is None:
1154             print "no event"
1155             return
1156         if self.in_drag:
1157             return
1158         tdw = self.doc.tdw
1159         if hasattr(event, "keyval"):
1160             if event.keyval != self._start_keyval:
1161                 self._start_keyval = event.keyval
1162                 self._start_drag(tdw, event)
1163         elif ( hasattr(event, "x") and hasattr(event, "y")
1164                and hasattr(event, "button") ):
1165             self._start_drag(tdw, event)
1166             if self.in_drag:
1167                 # Grab succeeded
1168                 self.last_x = event.x
1169                 self.last_y = event.y
1170                 self._start_button = event.button
1171
1172
1173 class SpringLoadedDragMode (SpringLoadedModeMixin, DragMode):
1174     """Spring-loaded drag mode convenience base, with a key-release refinement
1175
1176     If modifier keys were held when the mode was entered, a normal
1177     spring-loaded mode exits whenever those keys are all released. We don't
1178     want that to happen during drags however, so add this little refinement.
1179
1180     """
1181     # XXX: refactor: could this just be merged into SpringLoadedModeMixin?
1182
1183     def key_release_cb(self, win, tdw, event):
1184         if event.is_modifier and self.in_drag:
1185             return False
1186         return super(SpringLoadedDragMode, self).key_release_cb(win,tdw,event)
1187
1188
1189 class OneshotDragModeMixin (InteractionMode):
1190     """Drag modes that can exit immediately when the drag stops.
1191
1192     If SpringLoadedModeMixin is not also part of the mode object's class
1193     hierarchy, it will always exit at the end of a drag.
1194
1195     If the mode object does inherit SpringLoadedModeMixin behaviour, what
1196     happens at the end of a drag is controlled by a class variable setting.
1197
1198     """
1199
1200     unmodified_persist = False
1201     #: If true, and spring-loaded, stay active if no modifiers held initially.
1202
1203
1204     def drag_stop_cb(self):
1205         if not hasattr(self, "initial_modifiers"):
1206             # Always exit at the end of a drag if not spring-loaded.
1207             self.doc.modes.pop()
1208         elif self.initial_modifiers != 0:
1209             # If started with modifiers, keeping the modifiers held keeps
1210             # spring-loaded modes active. If not, exit the mode.
1211             if (self.initial_modifiers & self.current_modifiers()) == 0:
1212                 self.doc.modes.pop()
1213         else:
1214             # No modifiers were held when this mode was entered.
1215             if not self.unmodified_persist:
1216                 self.doc.modes.pop()
1217         return super(OneshotDragModeMixin, self).drag_stop_cb()
1218
1219
1220 class PanViewMode (SpringLoadedDragMode, OneshotDragModeMixin):
1221     """A oneshot mode for translating the viewport by dragging."""
1222
1223     __action_name__ = 'PanViewMode'
1224
1225     @property
1226     def inactive_cursor(self):
1227         return self.doc.app.cursors.get_action_cursor(
1228                 self.__action_name__)
1229     @property
1230     def active_cursor(self):
1231         return self.doc.app.cursors.get_action_cursor(
1232                 self.__action_name__)
1233
1234     def stackable_on(self, mode):
1235         return isinstance(mode, SwitchableModeMixin)
1236
1237     def drag_update_cb(self, tdw, event, dx, dy):
1238         tdw.scroll(-dx, -dy)
1239         super(PanViewMode, self).drag_update_cb(tdw, event, dx, dy)
1240
1241
1242 class ZoomViewMode (SpringLoadedDragMode, OneshotDragModeMixin):
1243     """A oneshot mode for zooming the viewport by dragging."""
1244
1245     __action_name__ = 'ZoomViewMode'
1246
1247     @property
1248     def active_cursor(self):
1249         return self.doc.app.cursors.get_action_cursor(
1250                 self.__action_name__)
1251     @property
1252     def inactive_cursor(self):
1253         return self.doc.app.cursors.get_action_cursor(
1254                 self.__action_name__)
1255
1256     def stackable_on(self, mode):
1257         return isinstance(mode, SwitchableModeMixin)
1258
1259     def drag_update_cb(self, tdw, event, dx, dy):
1260         tdw.scroll(-dx, -dy)
1261         tdw.zoom(math.exp(dy/100.0), center=(event.x, event.y))
1262         # TODO: Let modifiers constrain the zoom amount to 
1263         #       the defined steps.
1264         super(ZoomViewMode, self).drag_update_cb(tdw, event, dx, dy)
1265
1266
1267 class RotateViewMode (SpringLoadedDragMode, OneshotDragModeMixin):
1268     """A oneshot mode for rotating the viewport by dragging."""
1269
1270     __action_name__ = 'RotateViewMode'
1271
1272     @property
1273     def active_cursor(self):
1274         return self.doc.app.cursors.get_action_cursor(
1275                 self.__action_name__)
1276     @property
1277     def inactive_cursor(self):
1278         return self.doc.app.cursors.get_action_cursor(
1279                 self.__action_name__)
1280
1281     def stackable_on(self, mode):
1282         return isinstance(mode, SwitchableModeMixin)
1283
1284     def drag_update_cb(self, tdw, event, dx, dy):
1285         # calculate angular velocity from the rotation center
1286         x, y = event.x, event.y
1287         cx, cy = tdw.get_center()
1288         x, y = x-cx, y-cy
1289         phi2 = math.atan2(y, x)
1290         x, y = x-dx, y-dy
1291         phi1 = math.atan2(y, x)
1292         tdw.rotate(phi2-phi1, center=(cx, cy))
1293         # TODO: Allow modifiers to constrain the transformation angle
1294         #       to 22.5 degree steps.
1295         super(RotateViewMode, self).drag_update_cb(tdw, event, dx, dy)
1296
1297
1298 import linemode
1299 from linemode import StraightMode
1300 from linemode import SequenceMode
1301 from linemode import EllipseMode
1302 from framewindow import FrameEditMode
1303
1304
1305 class LayerMoveMode (SwitchableModeMixin,
1306                      ScrollableModeMixin,
1307                      SpringLoadedDragMode):
1308     """Moving a layer interactively.
1309
1310     MyPaint is tile-based, and tiles must align between layers, so moving
1311     layers involves copying data around. This is slow for very large layers, so
1312     the work is broken into chunks and processed in the idle phase of the GUI
1313     for greater responsivity.
1314
1315     """
1316
1317     __action_name__ = 'LayerMoveMode'
1318
1319
1320     @property
1321     def active_cursor(self):
1322         cursor_name = "cursor_hand_closed"
1323         if not self._move_possible:
1324             cursor_name = "cursor_forbidden_everywhere"
1325         return self.doc.app.cursors.get_action_cursor(
1326                 self.__action_name__, cursor_name)
1327
1328     @property
1329     def inactive_cursor(self):
1330         cursor_name = "cursor_hand_open"
1331         if not self._move_possible:
1332             cursor_name = "cursor_forbidden_everywhere"
1333         return self.doc.app.cursors.get_action_cursor(
1334                 self.__action_name__, cursor_name)
1335
1336     unmodified_persist = True
1337     permitted_switch_actions = set([
1338             'RotateViewMode', 'ZoomViewMode', 'PanViewMode',
1339         ] + extra_actions)
1340
1341
1342     def stackable_on(self, mode):
1343         # Any drawing mode
1344         return isinstance(mode, linemode.LineModeBase) \
1345             or isinstance(mode, SwitchableFreehandMode)
1346
1347
1348     def __init__(self, **kwds):
1349         super(LayerMoveMode, self).__init__(ignore_modifiers=True, **kwds)
1350         self.model_x0 = None
1351         self.model_y0 = None
1352         self.final_model_dx = None
1353         self.final_model_dy = None
1354         self._drag_update_idler_srcid = None
1355         self.layer = None
1356         self.move = None
1357         self.final_modifiers = 0
1358         self._move_possible = False
1359
1360
1361     def enter(self, **kwds):
1362         super(LayerMoveMode, self).enter(**kwds)
1363         self.final_modifiers = self.initial_modifiers
1364         self._update_cursors()
1365         self.doc.tdw.set_override_cursor(self.inactive_cursor)
1366
1367
1368     def model_structure_changed_cb(self, doc):
1369         super(LayerMoveMode, self).model_structure_changed_cb(doc)
1370         if self.move is not None:
1371             # Cursor update is deferred to the end of the drag
1372             return
1373         self._update_cursors()
1374
1375
1376     def _update_cursors(self):
1377         layer = self.doc.model.get_current_layer()
1378         self._move_possible = layer.visible and not layer.locked
1379         self.doc.tdw.set_override_cursor(self.inactive_cursor)
1380
1381
1382     def drag_start_cb(self, tdw, event):
1383         if self.layer is None:
1384             self.layer = self.doc.model.get_current_layer()
1385             model_x, model_y = tdw.display_to_model(self.start_x, self.start_y)
1386             self.model_x0 = model_x
1387             self.model_y0 = model_y
1388             self.drag_start_tdw = tdw
1389             self.move = None
1390         return super(LayerMoveMode, self).drag_start_cb(tdw, event)
1391
1392
1393     def drag_update_cb(self, tdw, event, dx, dy):
1394         assert self.layer is not None
1395
1396         # Begin moving, if we're not already
1397         if self.move is None and self._move_possible:
1398             self.move = self.layer.get_move(self.model_x0, self.model_y0)
1399
1400         # Update the active move 
1401         model_x, model_y = tdw.display_to_model(event.x, event.y)
1402         model_dx = model_x - self.model_x0
1403         model_dy = model_y - self.model_y0
1404         self.final_model_dx = model_dx
1405         self.final_model_dy = model_dy
1406
1407         if self.move is not None:
1408             self.move.update(model_dx, model_dy)
1409             # Keep showing updates in the background for feedback.
1410             if self._drag_update_idler_srcid is None:
1411                 idler = self._drag_update_idler
1412                 self._drag_update_idler_srcid = gobject.idle_add(idler)
1413
1414         return super(LayerMoveMode, self).drag_update_cb(tdw, event, dx, dy)
1415
1416
1417     def _drag_update_idler(self):
1418         # Process tile moves in chunks in a background idler
1419         # Terminate if asked
1420         if self._drag_update_idler_srcid is None:
1421             self.move.cleanup()
1422             return False
1423         # Process some tile moves, and carry on if there's more to do
1424         if self.move.process():
1425             return True
1426         # Nothing more to do for this move
1427         self.move.cleanup()
1428         self._drag_update_idler_srcid = None
1429         return False
1430
1431
1432     def drag_stop_cb(self):
1433         self._drag_update_idler_srcid = None   # ask it to finish
1434         if self.move is not None:
1435             # Arrange for the background work to be done, and look busy
1436             tdw = self.drag_start_tdw
1437             tdw.set_sensitive(False)
1438             tdw.set_override_cursor(gdk.Cursor(gdk.WATCH))
1439             self.final_modifiers = self.current_modifiers()
1440             gobject.idle_add(self._finalize_move_idler)
1441         else:
1442             # Still need cleanup for tracking state, cursors etc.
1443             self._drag_cleanup()
1444
1445         return super(LayerMoveMode, self).drag_stop_cb()
1446
1447
1448     def _drag_cleanup(self):
1449         # Reset drag tracking state
1450         self.model_x0 = self.model_y0 = None
1451         self.drag_start_tdw = self.move = None
1452         self.final_model_dx = self.final_model_dy = None
1453         self.layer = None
1454
1455         # Cursor setting:
1456         # Reset busy cursor after drag which performed a move,
1457         # catch doc structure changes that happen during a drag
1458         self._update_cursors()
1459
1460         # Leave mode if started with modifiers held and the user had released
1461         # them all at the end of the drag.
1462         if self.initial_modifiers:
1463             if (self.final_modifiers & self.initial_modifiers) == 0:
1464                 self.doc.modes.pop()
1465         else:
1466             self.doc.modes.pop()
1467
1468     def _finalize_move_idler(self):
1469         # Finalize everything once the drag's finished.
1470
1471         # Keep processing until the move queue is done.
1472         if self.move.process():
1473             return True
1474
1475         # Cleanup tile moves
1476         self.move.cleanup()
1477         tdw = self.drag_start_tdw
1478         dx = self.final_model_dx
1479         dy = self.final_model_dy
1480
1481         # Arrange for the strokemap to be moved too;
1482         # this happens in its own background idler.
1483         for stroke in self.layer.strokes:
1484             stroke.translate(dx, dy)
1485             # Minor problem: huge strokemaps take a long time to move, and the
1486             # translate must be forced to completion before drawing or any
1487             # further layer moves. This can cause apparent hangs for no
1488             # reason later on. Perhaps it would be better to process them
1489             # fully in this hourglass-cursor phase after all?
1490         for child in self.doc.model.get_layer_children(self.layer):
1491             child.translate(dx, dy)
1492
1493         # Record move so it can be undone
1494         self.doc.model.record_layer_move(self.layer, dx, dy)
1495
1496         # Restore sensitivity
1497         tdw.set_sensitive(True)
1498
1499         # Post-drag cleanup: cursor etc.
1500         self._drag_cleanup()
1501
1502         # All done, stop idle processing
1503         return False