wxWidgets/wxPython/wx/lib/mixins/rubberband.py
Robin Dunn b881fc787d Lots of wx namespace updates for the wx.lib package and the demo from
Jeff Grimmett with some tweaks and changes from Robin


git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@24889 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
2003-12-17 00:34:40 +00:00

407 lines
12 KiB
Python

#---------------------------------------------------------------------------
# Name: wxPython.lib.mixins.rubberband
# Purpose: A mixin class for doing "RubberBand"-ing on a window.
#
# Author: Robb Shecter and members of wxPython-users
#
# Created: 11-September-2002
# RCS-ID: $Id$
# Copyright: (c) 2002 by db-X Corporation
# Licence: wxWindows license
#---------------------------------------------------------------------------
# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net)
#
# o 2.5 compatability update.
# o Tested, but there is an anomaly between first use and subsequent uses.
# First use is odd, subsequent uses seem to be OK. Init error?
# -- No, the first time it uses an aspect ratio, but after the reset it doesn't.
#
"""
A mixin class for doing "RubberBand"-ing on a window.
"""
import wx
#
# Some miscellaneous mathematical and geometrical functions
#
def isNegative(aNumber):
"""
x < 0: 1
else: 0
"""
return aNumber < 0
def normalizeBox(box):
"""
Convert any negative measurements in the current
box to positive, and adjust the origin.
"""
x, y, w, h = box
if w < 0:
x += (w+1)
w *= -1
if h < 0:
y += (h+1)
h *= -1
return (x, y, w, h)
def boxToExtent(box):
"""
Convert a box specification to an extent specification.
I put this into a seperate function after I realized that
I had been implementing it wrong in several places.
"""
b = normalizeBox(box)
return (b[0], b[1], b[0]+b[2]-1, b[1]+b[3]-1)
def pointInBox(x, y, box):
"""
Return True if the given point is contained in the box.
"""
e = boxToExtent(box)
return x >= e[0] and x <= e[2] and y >= e[1] and y <= e[3]
def pointOnBox(x, y, box, thickness=1):
"""
Return True if the point is on the outside edge
of the box. The thickness defines how thick the
edge should be. This is necessary for HCI reasons:
For example, it's normally very difficult for a user
to manuever the mouse onto a one pixel border.
"""
outerBox = box
innerBox = (box[0]+thickness, box[1]+thickness, box[2]-(thickness*2), box[3]-(thickness*2))
return pointInBox(x, y, outerBox) and not pointInBox(x, y, innerBox)
def getCursorPosition(x, y, box, thickness=1):
"""
Return a position number in the range 0 .. 7 to indicate
where on the box border the point is. The layout is:
0 1 2
7 3
6 5 4
"""
x0, y0, x1, y1 = boxToExtent(box)
w, h = box[2], box[3]
delta = thickness - 1
p = None
if pointInBox(x, y, (x0, y0, thickness, thickness)):
p = 0
elif pointInBox(x, y, (x1-delta, y0, thickness, thickness)):
p = 2
elif pointInBox(x, y, (x1-delta, y1-delta, thickness, thickness)):
p = 4
elif pointInBox(x, y, (x0, y1-delta, thickness, thickness)):
p = 6
elif pointInBox(x, y, (x0+thickness, y0, w-(thickness*2), thickness)):
p = 1
elif pointInBox(x, y, (x1-delta, y0+thickness, thickness, h-(thickness*2))):
p = 3
elif pointInBox(x, y, (x0+thickness, y1-delta, w-(thickness*2), thickness)):
p = 5
elif pointInBox(x, y, (x0, y0+thickness, thickness, h-(thickness*2))):
p = 7
return p
class RubberBand:
"""
A stretchable border which is drawn on top of an
image to define an area.
"""
def __init__(self, drawingSurface, aspectRatio=None):
self.__THICKNESS = 5
self.drawingSurface = drawingSurface
self.aspectRatio = aspectRatio
self.hasLetUp = 0
self.currentlyMoving = None
self.currentBox = None
self.__enabled = 1
self.__currentCursor = None
drawingSurface.Bind(wx.EVT_MOUSE_EVENTS, self.__handleMouseEvents)
drawingSurface.Bind(wx.EVT_PAINT, self.__handleOnPaint)
def __setEnabled(self, enabled):
self.__enabled = enabled
def __isEnabled(self):
return self.__enabled
def __handleOnPaint(self, event):
#print 'paint'
event.Skip()
def __isMovingCursor(self):
"""
Return True if the current cursor is one used to
mean moving the rubberband.
"""
return self.__currentCursor == wx.CURSOR_HAND
def __isSizingCursor(self):
"""
Return True if the current cursor is one of the ones
I may use to signify sizing.
"""
sizingCursors = [wx.CURSOR_SIZENESW,
wx.CURSOR_SIZENS,
wx.CURSOR_SIZENWSE,
wx.CURSOR_SIZEWE,
wx.CURSOR_SIZING,
wx.CURSOR_CROSS]
try:
sizingCursors.index(self.__currentCursor)
return 1
except ValueError:
return 0
def __handleMouseEvents(self, event):
"""
React according to the new event. This is the main
entry point into the class. This method contains the
logic for the class's behavior.
"""
if not self.enabled:
return
x, y = event.GetPosition()
# First make sure we have started a box.
if self.currentBox == None and not event.LeftDown():
# No box started yet. Set cursor to the initial kind.
self.__setCursor(wx.CURSOR_CROSS)
return
if event.LeftDown():
if self.currentBox == None:
# No RB Box, so start a new one.
self.currentBox = (x, y, 0, 0)
self.hasLetUp = 0
elif self.__isSizingCursor():
# Starting a sizing operation. Change the origin.
position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS)
self.currentBox = self.__denormalizeBox(position, self.currentBox)
elif event.Dragging() and event.LeftIsDown():
# Use the cursor type to determine operation
if self.__isMovingCursor():
if self.currentlyMoving or pointInBox(x, y, self.currentBox):
if not self.currentlyMoving:
self.currentlyMoving = (x - self.currentBox[0], y - self.currentBox[1])
self.__moveTo(x - self.currentlyMoving[0], y - self.currentlyMoving[1])
elif self.__isSizingCursor():
self.__resizeBox(x, y)
elif event.LeftUp():
self.hasLetUp = 1
self.currentlyMoving = None
self.__normalizeBox()
elif event.Moving() and not event.Dragging():
# Simple mouse movement event
self.__mouseMoved(x,y)
def __denormalizeBox(self, position, box):
x, y, w, h = box
b = box
if position == 2 or position == 3:
b = (x, y + (h-1), w, h * -1)
elif position == 0 or position == 1 or position == 7:
b = (x + (w-1), y + (h-1), w * -1, h * -1)
elif position == 6:
b = (x + (w-1), y, w * -1, h)
return b
def __resizeBox(self, x, y):
"""
Resize and repaint the box based on the given mouse
coordinates.
"""
# Implement the correct behavior for dragging a side
# of the box: Only change one dimension.
if not self.aspectRatio:
if self.__currentCursor == wx.CURSOR_SIZENS:
x = None
elif self.__currentCursor == wx.CURSOR_SIZEWE:
y = None
x0,y0,w0,h0 = self.currentBox
currentExtent = boxToExtent(self.currentBox)
if x == None:
if w0 < 1:
w0 += 1
else:
w0 -= 1
x = x0 + w0
if y == None:
if h0 < 1:
h0 += 1
else:
h0 -= 1
y = y0 + h0
x1,y1 = x, y
w, h = abs(x1-x0)+1, abs(y1-y0)+1
if self.aspectRatio:
w = max(w, int(h * self.aspectRatio))
h = int(w / self.aspectRatio)
w *= [1,-1][isNegative(x1-x0)]
h *= [1,-1][isNegative(y1-y0)]
newbox = (x0, y0, w, h)
self.__drawAndErase(boxToDraw=normalizeBox(newbox), boxToErase=normalizeBox(self.currentBox))
self.currentBox = (x0, y0, w, h)
def __normalizeBox(self):
"""
Convert any negative measurements in the current
box to positive, and adjust the origin.
"""
self.currentBox = normalizeBox(self.currentBox)
def __mouseMoved(self, x, y):
"""
Called when the mouse moved without any buttons pressed
or dragging being done.
"""
# Are we on the bounding box?
if pointOnBox(x, y, self.currentBox, thickness=self.__THICKNESS):
position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS)
cursor = [
wx.CURSOR_SIZENWSE,
wx.CURSOR_SIZENS,
wx.CURSOR_SIZENESW,
wx.CURSOR_SIZEWE,
wx.CURSOR_SIZENWSE,
wx.CURSOR_SIZENS,
wx.CURSOR_SIZENESW,
wx.CURSOR_SIZEWE
] [position]
self.__setCursor(cursor)
elif pointInBox(x, y, self.currentBox):
self.__setCursor(wx.CURSOR_HAND)
else:
self.__setCursor()
def __setCursor(self, id=None):
"""
Set the mouse cursor to the given id.
"""
if self.__currentCursor != id: # Avoid redundant calls
if id:
self.drawingSurface.SetCursor(wx.StockCursor(id))
else:
self.drawingSurface.SetCursor(wx.NullCursor)
self.__currentCursor = id
def __moveCenterTo(self, x, y):
"""
Move the rubber band so that its center is at (x,y).
"""
x0, y0, w, h = self.currentBox
x2, y2 = x - (w/2), y - (h/2)
self.__moveTo(x2, y2)
def __moveTo(self, x, y):
"""
Move the rubber band so that its origin is at (x,y).
"""
newbox = (x, y, self.currentBox[2], self.currentBox[3])
self.__drawAndErase(boxToDraw=newbox, boxToErase=self.currentBox)
self.currentBox = newbox
def __drawAndErase(self, boxToDraw, boxToErase=None):
"""
Draw one box shape and possibly erase another.
"""
dc = wx.ClientDC(self.drawingSurface)
dc.BeginDrawing()
dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT))
dc.SetBrush(wx.TRANSPARENT_BRUSH)
dc.SetLogicalFunction(wx.XOR)
if boxToErase:
r = wx.Rect(*boxToErase)
dc.DrawRectangleRect(r)
r = wx.Rect(*boxToDraw)
dc.DrawRectangleRect(r)
dc.EndDrawing()
def __dumpMouseEvent(self, event):
print 'Moving: ',event.Moving()
print 'Dragging: ',event.Dragging()
print 'LeftDown: ',event.LeftDown()
print 'LeftisDown: ',event.LeftIsDown()
print 'LeftUp: ',event.LeftUp()
print 'Position: ',event.GetPosition()
print 'x,y: ',event.GetX(),event.GetY()
print
#
# The public API:
#
def reset(self, aspectRatio=None):
"""
Clear the existing rubberband
"""
self.currentBox = None
self.aspectRatio = aspectRatio
self.drawingSurface.Refresh()
def getCurrentExtent(self):
"""
Return (x0, y0, x1, y1) or None if
no drawing has yet been done.
"""
if not self.currentBox:
extent = None
else:
extent = boxToExtent(self.currentBox)
return extent
enabled = property(__isEnabled, __setEnabled, None, 'True if I am responding to mouse events')
if __name__ == '__main__':
app = wx.PySimpleApp()
frame = wx.Frame(None, -1, title='RubberBand Test', size=(300,300))
# Add a panel that the rubberband will work on.
panel = wx.Panel(frame, -1)
panel.SetBackgroundColour(wx.BLUE)
# Create the rubberband
frame.rubberBand = RubberBand(drawingSurface=panel)
frame.rubberBand.reset(aspectRatio=0.5)
# Add a button that creates a new rubberband
def __newRubberBand(event):
frame.rubberBand.reset()
button = wx.Button(frame, 100, 'Reset Rubberband')
frame.Bind(wx.EVT_BUTTON, __newRubberBand, button)
# Layout the frame
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 5)
sizer.Add(button, 0, wx.ALIGN_CENTER | wx.ALL, 5)
frame.SetAutoLayout(1)
frame.SetSizer(sizer)
frame.Show(1)
app.MainLoop()