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