~brian-sidebotham/wxwidgets-cmake/wxpython-2.9.4

« back to all changes in this revision

Viewing changes to wxPython/wx/lib/mixins/rubberband.py

  • Committer: Brian Sidebotham
  • Date: 2013-08-03 14:30:08 UTC
  • Revision ID: brian.sidebotham@gmail.com-20130803143008-c7806tkych1tp6fc
Initial import into Bazaar

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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: rubberband.py 24889 2003-12-17 00:34:40Z RD $
 
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()