]>
Commit | Line | Data |
---|---|---|
d14a1e28 RD |
1 | #--------------------------------------------------------------------------- |
2 | # Name: wxPython.lib.mixins.rubberband | |
3 | # Purpose: A mixin class for doing "RubberBand"-ing on a window. | |
4 | # | |
5 | # Author: Robb Shecter and members of wxPython-users | |
6 | # | |
7 | # Created: 11-September-2002 | |
8 | # RCS-ID: $Id$ | |
9 | # Copyright: (c) 2002 by db-X Corporation | |
10 | # Licence: wxWindows license | |
11 | #--------------------------------------------------------------------------- | |
b881fc78 RD |
12 | # 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
13 | # | |
14 | # o 2.5 compatability update. | |
15 | # o Tested, but there is an anomaly between first use and subsequent uses. | |
16 | # First use is odd, subsequent uses seem to be OK. Init error? | |
17 | # -- No, the first time it uses an aspect ratio, but after the reset it doesn't. | |
18 | # | |
1fded56b | 19 | |
d14a1e28 RD |
20 | """ |
21 | A mixin class for doing "RubberBand"-ing on a window. | |
22 | """ | |
1fded56b | 23 | |
b881fc78 | 24 | import wx |
d14a1e28 RD |
25 | |
26 | # | |
27 | # Some miscellaneous mathematical and geometrical functions | |
28 | # | |
29 | ||
30 | def isNegative(aNumber): | |
31 | """ | |
32 | x < 0: 1 | |
33 | else: 0 | |
34 | """ | |
35 | return aNumber < 0 | |
36 | ||
37 | ||
38 | def normalizeBox(box): | |
39 | """ | |
40 | Convert any negative measurements in the current | |
41 | box to positive, and adjust the origin. | |
42 | """ | |
43 | x, y, w, h = box | |
44 | if w < 0: | |
45 | x += (w+1) | |
46 | w *= -1 | |
47 | if h < 0: | |
48 | y += (h+1) | |
49 | h *= -1 | |
50 | return (x, y, w, h) | |
51 | ||
52 | ||
53 | def boxToExtent(box): | |
54 | """ | |
55 | Convert a box specification to an extent specification. | |
56 | I put this into a seperate function after I realized that | |
57 | I had been implementing it wrong in several places. | |
58 | """ | |
59 | b = normalizeBox(box) | |
60 | return (b[0], b[1], b[0]+b[2]-1, b[1]+b[3]-1) | |
61 | ||
62 | ||
63 | def pointInBox(x, y, box): | |
64 | """ | |
65 | Return True if the given point is contained in the box. | |
66 | """ | |
67 | e = boxToExtent(box) | |
68 | return x >= e[0] and x <= e[2] and y >= e[1] and y <= e[3] | |
69 | ||
70 | ||
71 | def pointOnBox(x, y, box, thickness=1): | |
72 | """ | |
73 | Return True if the point is on the outside edge | |
74 | of the box. The thickness defines how thick the | |
75 | edge should be. This is necessary for HCI reasons: | |
76 | For example, it's normally very difficult for a user | |
77 | to manuever the mouse onto a one pixel border. | |
78 | """ | |
79 | outerBox = box | |
80 | innerBox = (box[0]+thickness, box[1]+thickness, box[2]-(thickness*2), box[3]-(thickness*2)) | |
81 | return pointInBox(x, y, outerBox) and not pointInBox(x, y, innerBox) | |
82 | ||
83 | ||
84 | def getCursorPosition(x, y, box, thickness=1): | |
85 | """ | |
86 | Return a position number in the range 0 .. 7 to indicate | |
87 | where on the box border the point is. The layout is: | |
88 | ||
89 | 0 1 2 | |
90 | 7 3 | |
91 | 6 5 4 | |
92 | """ | |
93 | x0, y0, x1, y1 = boxToExtent(box) | |
94 | w, h = box[2], box[3] | |
95 | delta = thickness - 1 | |
96 | p = None | |
97 | ||
98 | if pointInBox(x, y, (x0, y0, thickness, thickness)): | |
99 | p = 0 | |
100 | elif pointInBox(x, y, (x1-delta, y0, thickness, thickness)): | |
101 | p = 2 | |
102 | elif pointInBox(x, y, (x1-delta, y1-delta, thickness, thickness)): | |
103 | p = 4 | |
104 | elif pointInBox(x, y, (x0, y1-delta, thickness, thickness)): | |
105 | p = 6 | |
106 | elif pointInBox(x, y, (x0+thickness, y0, w-(thickness*2), thickness)): | |
107 | p = 1 | |
108 | elif pointInBox(x, y, (x1-delta, y0+thickness, thickness, h-(thickness*2))): | |
109 | p = 3 | |
110 | elif pointInBox(x, y, (x0+thickness, y1-delta, w-(thickness*2), thickness)): | |
111 | p = 5 | |
112 | elif pointInBox(x, y, (x0, y0+thickness, thickness, h-(thickness*2))): | |
113 | p = 7 | |
114 | ||
115 | return p | |
116 | ||
117 | ||
118 | ||
119 | ||
120 | class RubberBand: | |
121 | """ | |
122 | A stretchable border which is drawn on top of an | |
123 | image to define an area. | |
124 | """ | |
125 | def __init__(self, drawingSurface, aspectRatio=None): | |
126 | self.__THICKNESS = 5 | |
127 | self.drawingSurface = drawingSurface | |
128 | self.aspectRatio = aspectRatio | |
129 | self.hasLetUp = 0 | |
130 | self.currentlyMoving = None | |
131 | self.currentBox = None | |
132 | self.__enabled = 1 | |
133 | self.__currentCursor = None | |
b881fc78 RD |
134 | |
135 | drawingSurface.Bind(wx.EVT_MOUSE_EVENTS, self.__handleMouseEvents) | |
136 | drawingSurface.Bind(wx.EVT_PAINT, self.__handleOnPaint) | |
d14a1e28 RD |
137 | |
138 | def __setEnabled(self, enabled): | |
139 | self.__enabled = enabled | |
140 | ||
141 | def __isEnabled(self): | |
142 | return self.__enabled | |
143 | ||
144 | def __handleOnPaint(self, event): | |
145 | #print 'paint' | |
146 | event.Skip() | |
147 | ||
148 | def __isMovingCursor(self): | |
149 | """ | |
150 | Return True if the current cursor is one used to | |
151 | mean moving the rubberband. | |
152 | """ | |
b881fc78 | 153 | return self.__currentCursor == wx.CURSOR_HAND |
d14a1e28 RD |
154 | |
155 | def __isSizingCursor(self): | |
156 | """ | |
157 | Return True if the current cursor is one of the ones | |
158 | I may use to signify sizing. | |
159 | """ | |
b881fc78 RD |
160 | sizingCursors = [wx.CURSOR_SIZENESW, |
161 | wx.CURSOR_SIZENS, | |
162 | wx.CURSOR_SIZENWSE, | |
163 | wx.CURSOR_SIZEWE, | |
164 | wx.CURSOR_SIZING, | |
165 | wx.CURSOR_CROSS] | |
d14a1e28 RD |
166 | try: |
167 | sizingCursors.index(self.__currentCursor) | |
168 | return 1 | |
169 | except ValueError: | |
170 | return 0 | |
171 | ||
172 | ||
173 | def __handleMouseEvents(self, event): | |
174 | """ | |
175 | React according to the new event. This is the main | |
176 | entry point into the class. This method contains the | |
177 | logic for the class's behavior. | |
178 | """ | |
179 | if not self.enabled: | |
180 | return | |
181 | ||
182 | x, y = event.GetPosition() | |
183 | ||
184 | # First make sure we have started a box. | |
185 | if self.currentBox == None and not event.LeftDown(): | |
186 | # No box started yet. Set cursor to the initial kind. | |
b881fc78 | 187 | self.__setCursor(wx.CURSOR_CROSS) |
d14a1e28 RD |
188 | return |
189 | ||
190 | if event.LeftDown(): | |
191 | if self.currentBox == None: | |
192 | # No RB Box, so start a new one. | |
193 | self.currentBox = (x, y, 0, 0) | |
194 | self.hasLetUp = 0 | |
195 | elif self.__isSizingCursor(): | |
196 | # Starting a sizing operation. Change the origin. | |
197 | position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS) | |
198 | self.currentBox = self.__denormalizeBox(position, self.currentBox) | |
199 | ||
200 | elif event.Dragging() and event.LeftIsDown(): | |
201 | # Use the cursor type to determine operation | |
202 | if self.__isMovingCursor(): | |
203 | if self.currentlyMoving or pointInBox(x, y, self.currentBox): | |
204 | if not self.currentlyMoving: | |
205 | self.currentlyMoving = (x - self.currentBox[0], y - self.currentBox[1]) | |
206 | self.__moveTo(x - self.currentlyMoving[0], y - self.currentlyMoving[1]) | |
207 | elif self.__isSizingCursor(): | |
208 | self.__resizeBox(x, y) | |
209 | ||
210 | elif event.LeftUp(): | |
211 | self.hasLetUp = 1 | |
212 | self.currentlyMoving = None | |
213 | self.__normalizeBox() | |
214 | ||
215 | elif event.Moving() and not event.Dragging(): | |
216 | # Simple mouse movement event | |
217 | self.__mouseMoved(x,y) | |
218 | ||
219 | def __denormalizeBox(self, position, box): | |
220 | x, y, w, h = box | |
221 | b = box | |
222 | if position == 2 or position == 3: | |
223 | b = (x, y + (h-1), w, h * -1) | |
224 | elif position == 0 or position == 1 or position == 7: | |
225 | b = (x + (w-1), y + (h-1), w * -1, h * -1) | |
226 | elif position == 6: | |
227 | b = (x + (w-1), y, w * -1, h) | |
228 | return b | |
229 | ||
230 | def __resizeBox(self, x, y): | |
231 | """ | |
232 | Resize and repaint the box based on the given mouse | |
233 | coordinates. | |
234 | """ | |
235 | # Implement the correct behavior for dragging a side | |
236 | # of the box: Only change one dimension. | |
237 | if not self.aspectRatio: | |
b881fc78 | 238 | if self.__currentCursor == wx.CURSOR_SIZENS: |
d14a1e28 | 239 | x = None |
b881fc78 | 240 | elif self.__currentCursor == wx.CURSOR_SIZEWE: |
d14a1e28 RD |
241 | y = None |
242 | ||
243 | x0,y0,w0,h0 = self.currentBox | |
244 | currentExtent = boxToExtent(self.currentBox) | |
245 | if x == None: | |
246 | if w0 < 1: | |
247 | w0 += 1 | |
248 | else: | |
249 | w0 -= 1 | |
250 | x = x0 + w0 | |
251 | if y == None: | |
252 | if h0 < 1: | |
253 | h0 += 1 | |
254 | else: | |
255 | h0 -= 1 | |
256 | y = y0 + h0 | |
257 | x1,y1 = x, y | |
258 | w, h = abs(x1-x0)+1, abs(y1-y0)+1 | |
259 | if self.aspectRatio: | |
260 | w = max(w, int(h * self.aspectRatio)) | |
261 | h = int(w / self.aspectRatio) | |
262 | w *= [1,-1][isNegative(x1-x0)] | |
263 | h *= [1,-1][isNegative(y1-y0)] | |
264 | newbox = (x0, y0, w, h) | |
265 | self.__drawAndErase(boxToDraw=normalizeBox(newbox), boxToErase=normalizeBox(self.currentBox)) | |
266 | self.currentBox = (x0, y0, w, h) | |
267 | ||
268 | def __normalizeBox(self): | |
269 | """ | |
270 | Convert any negative measurements in the current | |
271 | box to positive, and adjust the origin. | |
272 | """ | |
273 | self.currentBox = normalizeBox(self.currentBox) | |
274 | ||
275 | def __mouseMoved(self, x, y): | |
276 | """ | |
277 | Called when the mouse moved without any buttons pressed | |
278 | or dragging being done. | |
279 | """ | |
280 | # Are we on the bounding box? | |
281 | if pointOnBox(x, y, self.currentBox, thickness=self.__THICKNESS): | |
282 | position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS) | |
283 | cursor = [ | |
b881fc78 RD |
284 | wx.CURSOR_SIZENWSE, |
285 | wx.CURSOR_SIZENS, | |
286 | wx.CURSOR_SIZENESW, | |
287 | wx.CURSOR_SIZEWE, | |
288 | wx.CURSOR_SIZENWSE, | |
289 | wx.CURSOR_SIZENS, | |
290 | wx.CURSOR_SIZENESW, | |
291 | wx.CURSOR_SIZEWE | |
d14a1e28 RD |
292 | ] [position] |
293 | self.__setCursor(cursor) | |
294 | elif pointInBox(x, y, self.currentBox): | |
b881fc78 | 295 | self.__setCursor(wx.CURSOR_HAND) |
d14a1e28 RD |
296 | else: |
297 | self.__setCursor() | |
298 | ||
299 | def __setCursor(self, id=None): | |
300 | """ | |
301 | Set the mouse cursor to the given id. | |
302 | """ | |
303 | if self.__currentCursor != id: # Avoid redundant calls | |
304 | if id: | |
b881fc78 | 305 | self.drawingSurface.SetCursor(wx.StockCursor(id)) |
d14a1e28 | 306 | else: |
b881fc78 | 307 | self.drawingSurface.SetCursor(wx.NullCursor) |
d14a1e28 RD |
308 | self.__currentCursor = id |
309 | ||
310 | def __moveCenterTo(self, x, y): | |
311 | """ | |
312 | Move the rubber band so that its center is at (x,y). | |
313 | """ | |
314 | x0, y0, w, h = self.currentBox | |
315 | x2, y2 = x - (w/2), y - (h/2) | |
316 | self.__moveTo(x2, y2) | |
317 | ||
318 | def __moveTo(self, x, y): | |
319 | """ | |
320 | Move the rubber band so that its origin is at (x,y). | |
321 | """ | |
322 | newbox = (x, y, self.currentBox[2], self.currentBox[3]) | |
323 | self.__drawAndErase(boxToDraw=newbox, boxToErase=self.currentBox) | |
324 | self.currentBox = newbox | |
325 | ||
326 | def __drawAndErase(self, boxToDraw, boxToErase=None): | |
327 | """ | |
328 | Draw one box shape and possibly erase another. | |
329 | """ | |
b881fc78 | 330 | dc = wx.ClientDC(self.drawingSurface) |
d14a1e28 | 331 | dc.BeginDrawing() |
b881fc78 RD |
332 | dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT)) |
333 | dc.SetBrush(wx.TRANSPARENT_BRUSH) | |
334 | dc.SetLogicalFunction(wx.XOR) | |
d14a1e28 | 335 | if boxToErase: |
b881fc78 RD |
336 | r = wx.Rect(*boxToErase) |
337 | dc.DrawRectangleRect(r) | |
338 | ||
339 | r = wx.Rect(*boxToDraw) | |
340 | dc.DrawRectangleRect(r) | |
d14a1e28 RD |
341 | dc.EndDrawing() |
342 | ||
343 | def __dumpMouseEvent(self, event): | |
344 | print 'Moving: ',event.Moving() | |
345 | print 'Dragging: ',event.Dragging() | |
346 | print 'LeftDown: ',event.LeftDown() | |
347 | print 'LeftisDown: ',event.LeftIsDown() | |
348 | print 'LeftUp: ',event.LeftUp() | |
349 | print 'Position: ',event.GetPosition() | |
350 | print 'x,y: ',event.GetX(),event.GetY() | |
351 | ||
352 | ||
353 | ||
354 | # | |
355 | # The public API: | |
356 | # | |
357 | ||
358 | def reset(self, aspectRatio=None): | |
359 | """ | |
360 | Clear the existing rubberband | |
361 | """ | |
362 | self.currentBox = None | |
363 | self.aspectRatio = aspectRatio | |
364 | self.drawingSurface.Refresh() | |
365 | ||
366 | def getCurrentExtent(self): | |
367 | """ | |
368 | Return (x0, y0, x1, y1) or None if | |
369 | no drawing has yet been done. | |
370 | """ | |
371 | if not self.currentBox: | |
372 | extent = None | |
373 | else: | |
374 | extent = boxToExtent(self.currentBox) | |
375 | return extent | |
376 | ||
377 | enabled = property(__isEnabled, __setEnabled, None, 'True if I am responding to mouse events') | |
378 | ||
379 | ||
380 | ||
381 | if __name__ == '__main__': | |
b881fc78 RD |
382 | app = wx.PySimpleApp() |
383 | frame = wx.Frame(None, -1, title='RubberBand Test', size=(300,300)) | |
d14a1e28 RD |
384 | |
385 | # Add a panel that the rubberband will work on. | |
b881fc78 RD |
386 | panel = wx.Panel(frame, -1) |
387 | panel.SetBackgroundColour(wx.BLUE) | |
d14a1e28 RD |
388 | |
389 | # Create the rubberband | |
390 | frame.rubberBand = RubberBand(drawingSurface=panel) | |
391 | frame.rubberBand.reset(aspectRatio=0.5) | |
392 | ||
393 | # Add a button that creates a new rubberband | |
394 | def __newRubberBand(event): | |
395 | frame.rubberBand.reset() | |
b881fc78 RD |
396 | button = wx.Button(frame, 100, 'Reset Rubberband') |
397 | frame.Bind(wx.EVT_BUTTON, __newRubberBand, button) | |
d14a1e28 RD |
398 | |
399 | # Layout the frame | |
b881fc78 RD |
400 | sizer = wx.BoxSizer(wx.VERTICAL) |
401 | sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 5) | |
402 | sizer.Add(button, 0, wx.ALIGN_CENTER | wx.ALL, 5) | |
d14a1e28 RD |
403 | frame.SetAutoLayout(1) |
404 | frame.SetSizer(sizer) | |
405 | frame.Show(1) | |
406 | app.MainLoop() |