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