]>
Commit | Line | Data |
---|---|---|
51c5e1f2 RD |
1 | #Mouse Gestures |
2 | ||
7a0c9b39 | 3 | #Version 0.0.1 |
51c5e1f2 RD |
4 | |
5 | #By Daniel Pozmanter | |
6 | #drpython@bluebottle.com | |
7 | ||
8 | #Released under the terms of the wxWindows License. | |
9 | ||
8bd882ee RD |
10 | """ |
11 | This is a class to add Mouse Gestures to a program. | |
12 | It can be used in two ways: | |
51c5e1f2 | 13 | |
8bd882ee RD |
14 | 1. Automatic: |
15 | Automatically runs mouse gestures. | |
16 | You need to set the gestures, and their associated actions, | |
17 | as well as the Mouse Button/Modifiers to use. | |
51c5e1f2 | 18 | |
8bd882ee RD |
19 | 2. Manual: |
20 | Same as above, but you do not need to set the mouse button/modifiers. | |
21 | You can launch this from events as you wish. | |
51c5e1f2 | 22 | |
8bd882ee RD |
23 | An example is provided in the demo. |
24 | The parent window is where the mouse events will be recorded. | |
25 | (So if you want to record them in a pop up window, use manual mode, | |
26 | and set the pop up as the parent). | |
51c5e1f2 | 27 | |
8bd882ee RD |
28 | Start() starts recording mouse movement. |
29 | End() stops the recording, compiles all the gestures into a list, | |
30 | and looks through the registered gestures to find a match. | |
31 | The first matchs associated action is then run. | |
51c5e1f2 | 32 | |
8bd882ee RD |
33 | The marginoferror is how much to forgive when calculating movement: |
34 | If the margin is 25, then movement less than 25 pixels will not be detected. | |
51c5e1f2 | 35 | |
8bd882ee | 36 | Recognized: L, R, U, D, 1, 3, 7, 9 |
51c5e1f2 | 37 | |
8bd882ee RD |
38 | Styles: Manual (Automatic By Default), DisplayNumbersForDiagonals (Off By Default). |
39 | Not Yet Implemented | |
51c5e1f2 | 40 | |
8bd882ee RD |
41 | The criteria for a direction is as follows: |
42 | x in a row. (Where x is the WobbleTolerance). | |
43 | So if the WobbleTolerance is 9 | |
9f4cc34f | 44 | 'URUUUUUUUUUUUUUUURUURUUUU1' is Up. |
51c5e1f2 | 45 | |
8bd882ee RD |
46 | The higher this number, the less sensitive this class is. |
47 | So the more likely something like 1L will translate to 1. | |
51c5e1f2 | 48 | |
8bd882ee RD |
49 | This is good, since the mouse does tend to wobble somewhat, |
50 | and a higher number allows for this. | |
51 | ||
52 | To change this, use SetWobbleTolerance | |
53 | ||
54 | Also, to help with recognition of a diagonal versus | |
55 | a vey messy straight line, if the greater absolute value | |
56 | is not greater than twice the lesser, only the grater value | |
57 | is counted. | |
58 | ||
59 | In automatic mode, EVT_MOUSE_EVENTS is used. | |
60 | This allows the user to change the mouse button/modifiers at runtime. | |
61 | """ | |
7a0c9b39 RD |
62 | |
63 | ########################################### | |
64 | ||
65 | ''' | |
66 | Changelog: | |
67 | 0.0.1: Treats a mouse leaving event as mouse up. | |
68 | (Bug Report, Thanks Peter Damoc). | |
69 | ||
70 | ||
71 | 0.0.0: Initial Release. | |
72 | ''' | |
73 | ||
74 | ########################################### | |
75 | #ToDo: | |
51c5e1f2 | 76 | |
7a0c9b39 | 77 | #Fully Implement Manual Mode |
51c5e1f2 RD |
78 | |
79 | #Add "Ends With": AddGestureEndsWith(self, gesture, action, args) | |
80 | #Add "Starts With": AddGestuteStartsWith(self, gesture, action, args) | |
81 | ||
7a0c9b39 RD |
82 | #For better control of when the gesture starts and stops, |
83 | #use manual mode. | |
84 | #At the moment, you need to Bind the OnMouseMotion event if you want to use | |
85 | #manual mode. | |
51c5e1f2 RD |
86 | |
87 | import wx | |
88 | ||
89 | class MouseGestures: | |
90 | def __init__(self, parent, Auto=True, MouseButton=wx.MOUSE_BTN_MIDDLE): | |
91 | self.parent = parent | |
7a0c9b39 | 92 | |
51c5e1f2 RD |
93 | self.gestures = [] |
94 | self.actions = [] | |
95 | self.actionarguments = [] | |
96 | ||
97 | self.mousebutton = MouseButton | |
7a0c9b39 | 98 | self.modifiers = [] |
51c5e1f2 RD |
99 | |
100 | self.recording = False | |
101 | ||
102 | self.lastposition = (-1, -1) | |
103 | ||
104 | self.pen = wx.Pen(wx.Colour(0, 144, 255), 5) | |
105 | ||
106 | self.dc = wx.ScreenDC() | |
107 | self.dc.SetPen(self.pen) | |
108 | ||
109 | self.showgesture = False | |
110 | ||
111 | self.wobbletolerance = 7 | |
112 | ||
113 | self.rawgesture = '' | |
114 | ||
115 | self.SetAuto(Auto) | |
116 | ||
7a0c9b39 RD |
117 | def _check_modifiers(self, event): |
118 | '''Internal: Returns True if all needed modifiers are down | |
119 | for the given event.''' | |
120 | if len(self.modifiers) > 0: | |
121 | good = True | |
122 | if wx.WXK_CONTROL in self.modifiers: | |
123 | good = good and event.ControlDown() | |
124 | if wx.WXK_SHIFT in self.modifiers: | |
125 | good = good and event.ShiftDown() | |
126 | if wx.WXK_ALT in self.modifiers: | |
127 | good = good and event.AltDown() | |
128 | return good | |
129 | return True | |
130 | ||
51c5e1f2 | 131 | def AddGesture(self, gesture, action, *args): |
7a0c9b39 | 132 | '''Registers a gesture, and an associated function, with any arguments needed.''' |
51c5e1f2 RD |
133 | #Make Sure not a duplicate: |
134 | self.RemoveGesture(gesture) | |
135 | ||
136 | self.gestures.append(gesture) | |
137 | self.actions.append(action) | |
138 | self.actionarguments.append(args) | |
139 | ||
7a0c9b39 RD |
140 | def DoAction(self, gesture): |
141 | '''If the gesture is in the array of registered gestures, run the associated function.''' | |
142 | if gesture in self.gestures: | |
143 | i = self.gestures.index(gesture) | |
144 | apply(self.actions[i], self.actionarguments[i]) | |
145 | ||
51c5e1f2 | 146 | def End(self): |
7a0c9b39 RD |
147 | '''Stops recording the points to create the mouse gesture from, |
148 | and creates the mouse gesture, returns the result as a string.''' | |
51c5e1f2 RD |
149 | self.recording = False |
150 | ||
151 | #Figure out the gestures (Look for occurances of 5 in a row or more): | |
152 | ||
153 | tempstring = '0' | |
7a0c9b39 | 154 | possiblechange = '0' |
51c5e1f2 RD |
155 | |
156 | directions = '' | |
157 | ||
158 | for g in self.rawgesture: | |
159 | l = len(tempstring) | |
160 | if g != tempstring[l - 1]: | |
161 | if g == possiblechange: | |
162 | tempstring = g + g | |
163 | else: | |
7a0c9b39 | 164 | possiblechange = g |
51c5e1f2 RD |
165 | else: |
166 | tempstring += g | |
167 | if len(tempstring) >= self.wobbletolerance: | |
168 | ld = len(directions) | |
169 | if ld > 0: | |
170 | if directions[ld - 1] != g: | |
171 | directions += g | |
172 | else: | |
173 | directions += g | |
174 | tempstring = '0' | |
7a0c9b39 RD |
175 | |
176 | if self.showgesture: | |
177 | self.parent.Refresh() | |
51c5e1f2 RD |
178 | |
179 | return directions | |
180 | ||
181 | def GetDirection(self, point1, point2): | |
7a0c9b39 | 182 | '''Gets the direction between two points.''' |
51c5e1f2 RD |
183 | #point1 is the old point |
184 | #point2 is current | |
185 | ||
186 | x1, y1 = point1 | |
187 | x2, y2 = point2 | |
188 | ||
189 | #(Negative = Left, Up) | |
190 | #(Positive = Right, Down) | |
191 | ||
192 | horizontal = x2 - x1 | |
193 | vertical = y2 - y1 | |
194 | ||
195 | horizontalchange = abs(horizontal) > 0 | |
196 | verticalchange = abs(vertical) > 0 | |
197 | ||
198 | if horizontalchange and verticalchange: | |
199 | ah = abs(horizontal) | |
200 | av = abs(vertical) | |
201 | if ah > av: | |
202 | if (ah / av) > 2: | |
203 | vertical = 0 | |
204 | verticalchange = False | |
205 | elif av > ah: | |
206 | if (av / ah) > 2: | |
207 | horizontal = 0 | |
7a0c9b39 | 208 | horizontalchange = False |
51c5e1f2 RD |
209 | |
210 | if horizontalchange and verticalchange: | |
211 | #Diagonal | |
212 | if (horizontal > 0) and (vertical > 0): | |
213 | return '3' | |
214 | elif (horizontal > 0) and (vertical < 0): | |
215 | return '9' | |
216 | elif (horizontal < 0) and (vertical > 0): | |
217 | return '1' | |
218 | else: | |
7a0c9b39 | 219 | return '7' |
51c5e1f2 RD |
220 | else: |
221 | #Straight Line | |
222 | if horizontalchange: | |
223 | if horizontal > 0: | |
224 | return 'R' | |
225 | else: | |
226 | return 'L' | |
227 | else: | |
228 | if vertical > 0: | |
229 | return 'D' | |
230 | else: | |
231 | return 'U' | |
51c5e1f2 | 232 | |
7a0c9b39 RD |
233 | def GetRecording(self): |
234 | '''Returns whether or not Gesture Recording has started.''' | |
235 | return self.recording | |
236 | ||
51c5e1f2 | 237 | def OnMotion(self, event): |
7a0c9b39 | 238 | '''Internal. Used if Start() has been run''' |
51c5e1f2 RD |
239 | if self.recording: |
240 | currentposition = event.GetPosition() | |
241 | if self.lastposition != (-1, -1): | |
242 | self.rawgesture += self.GetDirection(self.lastposition, currentposition) | |
243 | if self.showgesture: | |
244 | #Draw it! | |
245 | px1, py1 = self.parent.ClientToScreen(self.lastposition) | |
246 | px2, py2 = self.parent.ClientToScreen(currentposition) | |
7a0c9b39 | 247 | self.dc.DrawLine(px1, py1, px2, py2) |
51c5e1f2 RD |
248 | |
249 | self.lastposition = currentposition | |
250 | ||
251 | event.Skip() | |
252 | ||
7a0c9b39 RD |
253 | def OnMouseEvent(self, event): |
254 | '''Internal. Used in Auto Mode.''' | |
255 | if event.ButtonDown(self.mousebutton) and self._check_modifiers(event): | |
256 | self.Start() | |
257 | elif (event.ButtonUp(self.mousebutton) or event.Leaving()) and self.GetRecording(): | |
258 | result = self.End() | |
259 | self.DoAction(result) | |
51c5e1f2 RD |
260 | event.Skip() |
261 | ||
262 | def RemoveGesture(self, gesture): | |
7a0c9b39 | 263 | '''Removes a gesture, and its associated action''' |
51c5e1f2 RD |
264 | if gesture in self.gestures: |
265 | i = self.gestures.index(gesture) | |
7a0c9b39 | 266 | |
51c5e1f2 RD |
267 | del self.gestures[i] |
268 | del self.actions[i] | |
269 | del self.actionarguments[i] | |
270 | ||
271 | def SetAuto(self, auto): | |
7a0c9b39 | 272 | '''Warning: Once auto is set, it stays set, unless you manually use UnBind''' |
51c5e1f2 | 273 | if auto: |
7a0c9b39 | 274 | self.parent.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouseEvent) |
51c5e1f2 RD |
275 | self.parent.Bind(wx.EVT_MOTION, self.OnMotion) |
276 | ||
7a0c9b39 RD |
277 | def SetGesturePen(self, pen): |
278 | '''Sets the wx pen used to visually represent each gesture''' | |
279 | self.pen = pen | |
280 | self.dc.SetPen(self.pen) | |
281 | ||
282 | def SetGesturePen(self, colour, width): | |
283 | '''Sets the colour and width of the line drawn to visually represent each gesture''' | |
284 | self.pen = wx.Pen(colour, width) | |
285 | self.dc.SetPen(self.pen) | |
286 | ||
51c5e1f2 | 287 | def SetGesturesVisible(self, vis): |
7a0c9b39 | 288 | '''Sets whether a line is drawn to visually represent each gesture''' |
51c5e1f2 RD |
289 | self.showgesture = vis |
290 | ||
7a0c9b39 RD |
291 | def SetModifiers(self, modifiers=[]): |
292 | '''Takes an array of wx Key constants (Control, Shift, and/or Alt). | |
293 | Leave empty to unset all modifiers.''' | |
51c5e1f2 RD |
294 | self.modifiers = modifiers |
295 | ||
7a0c9b39 RD |
296 | def SetMouseButton(self, mousebutton): |
297 | '''Takes the wx constant for the target mousebutton''' | |
298 | self.mousebutton = mousebutton | |
299 | ||
51c5e1f2 | 300 | def SetWobbleTolerance(self, wobbletolerance): |
7a0c9b39 | 301 | '''Sets just how much wobble this class can take!''' |
51c5e1f2 RD |
302 | self.WobbleTolerance = wobbletolerance |
303 | ||
304 | def Start(self): | |
7a0c9b39 | 305 | '''Starts recording the points to create the mouse gesture from''' |
51c5e1f2 | 306 | self.recording = True |
7a0c9b39 | 307 | self.rawgesture = '' |
51c5e1f2 | 308 | self.lastposition = (-1, -1) |
7a0c9b39 | 309 | if self.showgesture: |
8bd882ee | 310 | self.parent.Refresh() |