]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/mixins/rubberband.py
Patch from Andrea that fixes the following problems/issues:
[wxWidgets.git] / wxPython / wx / lib / mixins / rubberband.py
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 #---------------------------------------------------------------------------
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 #
19
20 """
21 A mixin class for doing "RubberBand"-ing on a window.
22 """
23
24 import wx
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
134
135 drawingSurface.Bind(wx.EVT_MOUSE_EVENTS, self.__handleMouseEvents)
136 drawingSurface.Bind(wx.EVT_PAINT, self.__handleOnPaint)
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 """
153 return self.__currentCursor == wx.CURSOR_HAND
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 """
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]
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.
187 self.__setCursor(wx.CURSOR_CROSS)
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:
238 if self.__currentCursor == wx.CURSOR_SIZENS:
239 x = None
240 elif self.__currentCursor == wx.CURSOR_SIZEWE:
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 = [
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
292 ] [position]
293 self.__setCursor(cursor)
294 elif pointInBox(x, y, self.currentBox):
295 self.__setCursor(wx.CURSOR_HAND)
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:
305 self.drawingSurface.SetCursor(wx.StockCursor(id))
306 else:
307 self.drawingSurface.SetCursor(wx.NullCursor)
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 """
330 dc = wx.ClientDC(self.drawingSurface)
331 dc.BeginDrawing()
332 dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT))
333 dc.SetBrush(wx.TRANSPARENT_BRUSH)
334 dc.SetLogicalFunction(wx.XOR)
335 if boxToErase:
336 r = wx.Rect(*boxToErase)
337 dc.DrawRectangleRect(r)
338
339 r = wx.Rect(*boxToDraw)
340 dc.DrawRectangleRect(r)
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 print
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__':
382 app = wx.PySimpleApp()
383 frame = wx.Frame(None, -1, title='RubberBand Test', size=(300,300))
384
385 # Add a panel that the rubberband will work on.
386 panel = wx.Panel(frame, -1)
387 panel.SetBackgroundColour(wx.BLUE)
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()
396 button = wx.Button(frame, 100, 'Reset Rubberband')
397 frame.Bind(wx.EVT_BUTTON, __newRubberBand, button)
398
399 # Layout the frame
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)
403 frame.SetAutoLayout(1)
404 frame.SetSizer(sizer)
405 frame.Show(1)
406 app.MainLoop()