OSDN Git Service

version bump
[mypaint-anime/master.git] / gui / stategroup.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2009 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
10 gdk = gtk.gdk
11
12 class StateGroup():
13     """Supervisor instance for GUI states.
14
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.
18     """
19
20     def __init__(self):
21         self.states = []
22         self.keys_pressed = {}
23
24     def get_active_states(self):
25         return [s for s in self.states if s.active]
26     active_states = property(get_active_states)
27
28     def create_state(self, enter, leave, popup=None):
29         s = State(self, popup)
30         s.popup = None # FIXME: who uses this? hack?
31         s.on_enter = enter
32         s.on_leave = leave
33         self.states.append(s)
34         return s
35
36     def create_popup_state(self, popup):
37         return self.create_state(popup.enter, popup.leave, popup)
38
39 class State:
40     """A GUI state.
41
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.
46     """
47
48     #: How long a key can be held down to go through as single hit (and not
49     #: press-and-hold)
50     max_key_hit_duration = 0.250
51
52     #: The state is automatically left after this time (ignored during
53     #: press-and-hold)
54     autoleave_timeout = 0.800
55
56     ##: popups only: how long the cursor is allowed outside before closing
57     ##: (ignored during press-and-hold)"
58     #outside_popup_timeout = 0.050
59
60     #: state to activate when this state is activated while already active
61     #: (None = just leave this state)
62     next_state = None
63
64     #: Allowed buttons and their masks for starting and continuing states
65     #: triggered by gdk button press events.
66     allowed_buttons_masks = {
67         1: gdk.BUTTON1_MASK,
68         2: gdk.BUTTON2_MASK,
69         3: gdk.BUTTON3_MASK, }
70
71     def __init__(self, stategroup, popup):
72         self.sg = stategroup
73         self.active = False
74         self.popup = popup
75         self.autoleave_timer = None
76         self.outside_popup_timer = None
77         if popup:
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
82
83     def enter(self):
84         #print 'entering state, calling', self.on_enter.__name__
85         assert not self.active
86         self.active = True
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)
91         self.on_enter()
92
93     def leave(self, reason=None):
94         #print 'leaving state, calling', self.on_leave.__name__
95         assert self.active
96         self.active = False
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)
105
106     def activate(self, action_or_event=None):
107         """
108         Called from the GUI code, eg. when a gtk.Action is
109         activated. The action is used to figure out the key.
110         """
111         if self.active:
112             # pressing the key again
113             if self.next_state:
114                 self.leave()
115                 self.next_state.activate(action_or_event)
116                 return
117
118         # first leave other active states from the same stategroup
119         for state in self.sg.active_states:
120             state.leave()
121
122         self.keydown = False
123         self.mouse_button = None
124
125         if action_or_event:
126             if isinstance(action_or_event, gdk.Event):
127                 e = action_or_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
131                 assert e.button
132                 if e.button in self.allowed_buttons_masks:
133                     self.mouse_button = e.button
134
135             else:
136                 a = action_or_event
137                 # register for key release events, see keyboard.py
138                 if a.keydown:
139                     a.keyup_callback = self.keyup_cb
140                     self.keydown = True
141         self.activated_by_keyboard = self.keydown # FIXME: should probably be renamed (mouse button possible)
142         self.enter()
143
144     def toggle(self, action=None):
145         if isinstance(action, gtk.ToggleAction):
146             want_active = action.get_active()
147         else:
148             want_active = not self.active
149         if want_active:
150             if not self.active:
151                 self.activate(action)
152         else:
153             if self.active:
154                 self.leave()
155
156     def keyup_cb(self, widget, event):
157         if not self.active:
158             return
159         self.keydown = False
160         if event.time/1000.0 - self.enter_time < self.max_key_hit_duration:
161             pass # accept as one-time hit
162         else:
163             if self.outside_popup_timer:
164                 self.leave('outside')
165             else:
166                 self.leave('keyup')
167
168     def autoleave_timeout_cb(self):
169         if not self.keydown:
170             self.leave('timeout')
171     def outside_popup_timeout_cb(self):
172         if not self.keydown:
173             self.leave('outside')
174
175     def popup_enter_notify_cb(self, widget, event):
176         if not self.active:
177             return
178         if self.outside_popup_timer:
179             gobject.source_remove(self.outside_popup_timer)
180             self.outside_popup_timer = None
181
182     def popup_leave_notify_cb(self, widget, event):
183         if not self.active:
184             return
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)
189
190
191
192     # ColorPicker-only stuff (for now)
193
194
195     def motion_notify_cb(self, widget, event):
196         assert self.keydown
197
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:
205             return
206
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)
212
213     def disconnect_motion_handler(self):
214         if not self.connected_motion_handler:
215             return
216         widget, handler_id = self.connected_motion_handler
217         widget.disconnect(handler_id)
218         self.connected_motion_handler = None
219
220     def register_mouse_grab(self, widget):
221         assert self.active
222
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
228                           )
229         widget.set_extension_events (gdk.EXTENSION_EVENTS_ALL)
230
231         if self.keydown:
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
235             return
236         if self.mouse_button:
237             # we are able to wait for a button release now
238             self.keydown = True
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)
244         else:
245             # The user is neither pressing a key, nor holding down a button.
246             # This happens when activating the color picker from the menu.
247             #
248             # Stop everything, release the pointer grab.
249             # (TODO: wait for a click instead, or show an instruction dialog)
250             print 'COV'
251             self.leave(None)
252