]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/mixins/rubberband.py
wxCheckListBox doesn't require wxUSE_OWNER_DRAWN when using WXUNIVERSAL
[wxWidgets.git] / wxPython / wx / lib / mixins / rubberband.py
CommitLineData
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"""
21A mixin class for doing "RubberBand"-ing on a window.
22"""
1fded56b 23
b881fc78 24import wx
d14a1e28
RD
25
26#
27# Some miscellaneous mathematical and geometrical functions
28#
29
30def isNegative(aNumber):
31 """
32 x < 0: 1
33 else: 0
34 """
35 return aNumber < 0
36
37
38def 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
53def 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
63def 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
71def 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
84def 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
120class 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 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
381if __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()