1 # This file is part of MyPaint.
2 # Copyright (C) 2009 by Martin Renold <martinxyz@gmx.ch>
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.
13 """Supervisor instance for GUI states.
15 This class mainly deals with the various ways how the user can
16 leave such a mode, eg. if the mode is entered by holding down a
17 key long enough, it will be left when the key is released.
22 self.keys_pressed = {}
24 def get_active_states(self):
25 return [s for s in self.states if s.active]
26 active_states = property(get_active_states)
28 def create_state(self, enter, leave, popup=None):
29 s = State(self, popup)
30 s.popup = None # FIXME: who uses this? hack?
36 def create_popup_state(self, popup):
37 return self.create_state(popup.enter, popup.leave, popup)
42 A GUI state is a mode which the GUI is in, for example an active
43 popup window or a special (usually short-lived) view on the
44 document. The application defines functions to be called when the
45 state is entered or left.
48 #: How long a key can be held down to go through as single hit (and not
50 max_key_hit_duration = 0.250
52 #: The state is automatically left after this time (ignored during
54 autoleave_timeout = 0.800
56 ##: popups only: how long the cursor is allowed outside before closing
57 ##: (ignored during press-and-hold)"
58 #outside_popup_timeout = 0.050
60 #: state to activate when this state is activated while already active
61 #: (None = just leave this state)
64 #: Allowed buttons and their masks for starting and continuing states
65 #: triggered by gdk button press events.
66 allowed_buttons_masks = {
69 3: gdk.BUTTON3_MASK, }
71 def __init__(self, stategroup, popup):
75 self.autoleave_timer = None
76 self.outside_popup_timer = None
78 popup.connect("enter-notify-event", self.popup_enter_notify_cb)
79 popup.connect("leave-notify-event", self.popup_leave_notify_cb)
80 popup.popup_state = self # FIXME: hacky?
81 self.outside_popup_timeout = popup.outside_popup_timeout
84 #print 'entering state, calling', self.on_enter.__name__
85 assert not self.active
87 self.enter_time = gtk.get_current_event_time()/1000.0
88 self.connected_motion_handler = None
89 if self.autoleave_timeout:
90 self.autoleave_timer = gobject.timeout_add(int(1000*self.autoleave_timeout), self.autoleave_timeout_cb)
93 def leave(self, reason=None):
94 #print 'leaving state, calling', self.on_leave.__name__
97 if self.autoleave_timer:
98 gobject.source_remove(self.autoleave_timer)
99 self.autoleave_timer = None
100 if self.outside_popup_timer:
101 gobject.source_remove(self.outside_popup_timer)
102 self.outside_popup_timer = None
103 self.disconnect_motion_handler()
104 self.on_leave(reason)
106 def activate(self, action_or_event=None):
108 Called from the GUI code, eg. when a gtk.Action is
109 activated. The action is used to figure out the key.
112 # pressing the key again
115 self.next_state.activate(action_or_event)
118 # first leave other active states from the same stategroup
119 for state in self.sg.active_states:
123 self.mouse_button = None
126 if isinstance(action_or_event, gdk.Event):
128 # currently, we only support mouse buttons being pressed here
129 assert e.type == gdk.BUTTON_PRESS
130 # let's just note down what mous button that was
132 if e.button in self.allowed_buttons_masks:
133 self.mouse_button = e.button
137 # register for key release events, see keyboard.py
139 a.keyup_callback = self.keyup_cb
141 self.activated_by_keyboard = self.keydown # FIXME: should probably be renamed (mouse button possible)
144 def toggle(self, action=None):
145 if isinstance(action, gtk.ToggleAction):
146 want_active = action.get_active()
148 want_active = not self.active
151 self.activate(action)
156 def keyup_cb(self, widget, event):
160 if event.time/1000.0 - self.enter_time < self.max_key_hit_duration:
161 pass # accept as one-time hit
163 if self.outside_popup_timer:
164 self.leave('outside')
168 def autoleave_timeout_cb(self):
170 self.leave('timeout')
171 def outside_popup_timeout_cb(self):
173 self.leave('outside')
175 def popup_enter_notify_cb(self, widget, event):
178 if self.outside_popup_timer:
179 gobject.source_remove(self.outside_popup_timer)
180 self.outside_popup_timer = None
182 def popup_leave_notify_cb(self, widget, event):
185 # allow to leave the window for a short time
186 if self.outside_popup_timer:
187 gobject.source_remove(self.outside_popup_timer)
188 self.outside_popup_timer = gobject.timeout_add(int(1000*self.outside_popup_timeout), self.outside_popup_timeout_cb)
192 # ColorPicker-only stuff (for now)
195 def motion_notify_cb(self, widget, event):
198 # We can't leave the state yet if button 1 is being pressed without
199 # risking putting an accidental dab on the canvas. This happens with
200 # some resistive touchscreens where a logical "button 3" is physically
201 # a stylus button 3 plus a nib press (which is also a button 1).
202 pressure = event.get_axis(gdk.AXIS_PRESSURE)
203 button1_down = event.state & gdk.BUTTON1_MASK
204 if pressure or button1_down:
207 # Leave if the button we started with is no longer being pressed.
208 button_mask = self.allowed_buttons_masks.get(self.mouse_button, 0)
209 if not event.state & button_mask:
210 self.disconnect_motion_handler()
211 self.keyup_cb(widget, event)
213 def disconnect_motion_handler(self):
214 if not self.connected_motion_handler:
216 widget, handler_id = self.connected_motion_handler
217 widget.disconnect(handler_id)
218 self.connected_motion_handler = None
220 def register_mouse_grab(self, widget):
223 # fix for https://gna.org/bugs/?14871 (problem with tablet and pointer grabs)
224 widget.add_events(gdk.POINTER_MOTION_MASK
225 # proximity mask might help with scrollwheel events (https://gna.org/bugs/index.php?16253)
226 | gdk.PROXIMITY_OUT_MASK
227 | gdk.PROXIMITY_IN_MASK
229 widget.set_extension_events (gdk.EXTENSION_EVENTS_ALL)
232 # we are reacting to a keyboard event, we will not be
233 # waiting for a mouse button release
234 assert not self.mouse_button
236 if self.mouse_button:
237 # we are able to wait for a button release now
239 # register for events
240 assert self.mouse_button in self.allowed_buttons_masks
241 handler_id = widget.connect("motion-notify-event", self.motion_notify_cb)
242 assert not self.connected_motion_handler
243 self.connected_motion_handler = (widget, handler_id)
245 # The user is neither pressing a key, nor holding down a button.
246 # This happens when activating the color picker from the menu.
248 # Stop everything, release the pointer grab.
249 # (TODO: wait for a click instead, or show an instruction dialog)