Forward port recent changes on the 2.8 branch to HEAD

git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@46083 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
Robin Dunn
2007-05-16 23:39:42 +00:00
parent f6342fb5e6
commit 0b0849b5a5
87 changed files with 3807 additions and 1586 deletions

View File

@@ -1766,7 +1766,7 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
TR_NO_LINES # don't draw lines at all
TR_LINES_AT_ROOT # connect top-level nodes
TR_TWIST_BUTTONS # draw mac-like twist buttons
TR_SINGLE # single selection mode
TR_SINGLE # single selection mode
TR_MULTIPLE # can select multiple items
TR_EXTENDED # todo: allow extended selection
TR_HAS_VARIABLE_ROW_HEIGHT # allows rows to have variable height
@@ -1870,12 +1870,13 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
self._vistaselection = False
# Connection lines style
grey = (160,160,160)
if wx.Platform != "__WXMAC__":
self._dottedPen = wx.Pen("grey", 1, wx.USER_DASH)
self._dottedPen = wx.Pen(grey, 1, wx.USER_DASH)
self._dottedPen.SetDashes([1,1])
self._dottedPen.SetCap(wx.CAP_BUTT)
else:
self._dottedPen = wx.Pen("grey", 1)
self._dottedPen = wx.Pen(grey, 1)
# Pen Used To Draw The Border Around Selected Items
self._borderPen = wx.BLACK_PEN
@@ -1893,8 +1894,6 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
if major < 10:
style |= TR_ROW_LINES
self._windowStyle = style
# Create the default check image list
self.SetImageListCheck(13, 13)
@@ -2169,13 +2168,13 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
dc = wx.ClientDC(self)
self.RefreshLine(item)
if self._windowStyle & TR_AUTO_CHECK_CHILD:
if self.HasFlag(TR_AUTO_CHECK_CHILD):
ischeck = self.IsItemChecked(item)
self.AutoCheckChild(item, ischeck)
if self._windowStyle & TR_AUTO_CHECK_PARENT:
if self.HasFlag(TR_AUTO_CHECK_PARENT):
ischeck = self.IsItemChecked(item)
self.AutoCheckParent(item, ischeck)
elif self._windowStyle & TR_AUTO_TOGGLE_CHILD:
elif self.HasFlag(TR_AUTO_TOGGLE_CHILD):
self.AutoToggleChild(item)
e = TreeEvent(wxEVT_TREE_ITEM_CHECKED, self.GetId())
@@ -2310,12 +2309,6 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
self._dirty = True
def HasFlag(self, flag):
"""Returns whether CustomTreeCtrl has a flag."""
return self._windowStyle & flag
def HasChildren(self, item):
"""Returns whether an item has children or not."""
@@ -2349,19 +2342,19 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
# want to update the inherited styles, but right now
# none of the parents has updatable styles
if self._windowStyle & TR_MULTIPLE and not (styles & TR_MULTIPLE):
if self.HasFlag(TR_MULTIPLE) and not (styles & TR_MULTIPLE):
selections = self.GetSelections()
for select in selections[0:-1]:
self.SelectItem(select, False)
self._windowStyle = styles
self.SetWindowStyle(styles)
self._dirty = True
def GetTreeStyle(self):
"""Returns the CustomTreeCtrl style."""
return self._windowStyle
return self.GetWindowStyle()
def HasButtons(self):
@@ -3155,10 +3148,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
def DoInsertItem(self, parentId, previous, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None):
"""Actually inserts an item in the tree."""
if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if ct_type < 0 or ct_type > 2:
@@ -3190,10 +3183,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
if self._anchor:
raise Exception("\nERROR: Tree Can Have Only One Root")
if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if ct_type < 0 or ct_type > 2:
@@ -3226,10 +3219,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
def PrependItem(self, parent, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None):
"""Appends an item as a first child of parent."""
if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
return self.DoInsertItem(parent, 0, text, ct_type, wnd, image, selImage, data)
@@ -3238,10 +3231,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
def InsertItemByItem(self, parentId, idPrevious, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None):
"""Auxiliary function to cope with the C++ hideous multifunction."""
if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
parent = parentId
@@ -3264,10 +3257,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
def InsertItemByIndex(self, parentId, before, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None):
"""Auxiliary function to cope with the C++ hideous multifunction."""
if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
parent = parentId
@@ -3282,10 +3275,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
def InsertItem(self, parentId, input, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None):
"""Inserts an item after the given previous."""
if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if type(input) == type(1):
@@ -3297,10 +3290,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
def AppendItem(self, parentId, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None):
"""Appends an item as a last child of its parent."""
if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT):
if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT):
raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT")
parent = parentId
@@ -3562,7 +3555,8 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
for child in self._itemWithWindow:
if not self.IsVisible(child):
wnd = child.GetWindow()
wnd.Hide()
if wnd:
wnd.Hide()
def Unselect(self):
@@ -4565,7 +4559,7 @@ class CustomTreeCtrl(wx.PyScrolledWindow):
else: # no custom buttons
if self._windowStyle & TR_TWIST_BUTTONS:
if self.HasFlag(TR_TWIST_BUTTONS):
# We draw something like the Mac twist buttons
dc.SetPen(wx.BLACK_PEN)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,347 @@
"""
Module that holds the GUI modes used by FloatCanvas
Note that this can only be imported after a wx.App() has been created.
This approach was inpired by Christian Blouin, who also wrote the initial
version of the code.
"""
import wx
## fixme: events should live in their own module, so all of FloatCanvas
## wouldn't have to be imported here.
import FloatCanvas, Resources
import numpy as N
## create all the Cursors, so they don't need to be created each time.
if "wxMac" in wx.PlatformInfo: # use 16X16 cursors for wxMac
HandCursor = wx.CursorFromImage(Resources.getHand16Image())
GrabHandCursor = wx.CursorFromImage(Resources.getGrabHand16Image())
img = Resources.getMagPlus16Image()
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
MagPlusCursor = wx.CursorFromImage(img)
img = Resources.getMagMinus16Image()
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
MagMinusCursor = wx.CursorFromImage(img)
else: # use 24X24 cursors for GTK and Windows
HandCursor = wx.CursorFromImage(Resources.getHandImage())
GrabHandCursor = wx.CursorFromImage(Resources.getGrabHandImage())
img = Resources.getMagPlusImage()
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
MagPlusCursor = wx.CursorFromImage(img)
img = Resources.getMagMinusImage()
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
MagMinusCursor = wx.CursorFromImage(img)
class GUIBase:
"""
Basic Mouse mode and baseclass for other GUImode.
This one does nothing with any event
"""
def __init__(self, parent):
self.parent = parent
Cursor = wx.NullCursor
# Handlers
def OnLeftDown(self, event):
pass
def OnLeftUp(self, event):
pass
def OnLeftDouble(self, event):
pass
def OnRightDown(self, event):
pass
def OnRightUp(self, event):
pass
def OnRightDouble(self, event):
pass
def OnMiddleDown(self, event):
pass
def OnMiddleUp(self, event):
pass
def OnMiddleDouble(self, event):
pass
def OnWheel(self, event):
pass
def OnMove(self, event):
pass
def UpdateScreen(self):
"""
Update gets called if the screen has been repainted in the middle of a zoom in
so the Rubber Band Box can get updated
"""
pass
class GUIMouse(GUIBase):
"""
Mouse mode checks for a hit test, and if nothing is hit,
raises a FloatCanvas mouse event for each event.
"""
Cursor = wx.NullCursor
# Handlers
def OnLeftDown(self, event):
EventType = FloatCanvas.EVT_FC_LEFT_DOWN
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnLeftUp(self, event):
EventType = FloatCanvas.EVT_FC_LEFT_UP
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnLeftDouble(self, event):
EventType = FloatCanvas.EVT_FC_LEFT_DCLICK
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnMiddleDown(self, event):
EventType = FloatCanvas.EVT_FC_MIDDLE_DOWN
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnMiddleUp(self, event):
EventType = FloatCanvas.EVT_FC_MIDDLE_UP
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnMiddleDouble(self, event):
EventType = FloatCanvas.EVT_FC_MIDDLE_DCLICK
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnRightDown(self, event):
EventType = FloatCanvas.EVT_FC_RIGHT_DOWN
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnRightUp(self, event):
EventType = FloatCanvas.EVT_FC_RIGHT_UP
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnRightDouble(self, event):
EventType = FloatCanvas.EVT_FC_RIGHT_DCLICK
if not self.parent.HitTest(event, EventType):
self.parent._RaiseMouseEvent(event, EventType)
def OnWheel(self, event):
EventType = FloatCanvas.EVT_FC_MOUSEWHEEL
self.parent._RaiseMouseEvent(event, EventType)
def OnMove(self, event):
## The Move event always gets raised, even if there is a hit-test
self.parent.MouseOverTest(event)
self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION)
class GUIMove(GUIBase):
Cursor = HandCursor
GrabCursor = GrabHandCursor
def __init__(self, parent):
GUIBase.__init__(self, parent)
self.StartMove = None
self.PrevMoveXY = None
def OnLeftDown(self, event):
self.parent.SetCursor(self.GrabCursor)
self.parent.CaptureMouse()
self.StartMove = N.array( event.GetPosition() )
self.PrevMoveXY = (0,0)
def OnLeftUp(self, event):
if self.StartMove is not None:
StartMove = self.StartMove
EndMove = N.array(event.GetPosition())
DiffMove = StartMove-EndMove
if N.sum(DiffMove**2) > 16:
self.parent.MoveImage(DiffMove, 'Pixel')
self.StartMove = None
self.parent.SetCursor(self.Cursor)
def OnMove(self, event):
# Allways raise the Move event.
self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION)
if event.Dragging() and event.LeftIsDown() and not self.StartMove is None:
xy1 = N.array( event.GetPosition() )
wh = self.parent.PanelSize
xy_tl = xy1 - self.StartMove
dc = wx.ClientDC(self.parent)
dc.BeginDrawing()
x1,y1 = self.PrevMoveXY
x2,y2 = xy_tl
w,h = self.parent.PanelSize
##fixme: This sure could be cleaner!
if x2 > x1 and y2 > y1:
xa = xb = x1
ya = yb = y1
wa = w
ha = y2 - y1
wb = x2- x1
hb = h
elif x2 > x1 and y2 <= y1:
xa = x1
ya = y1
wa = x2 - x1
ha = h
xb = x1
yb = y2 + h
wb = w
hb = y1 - y2
elif x2 <= x1 and y2 > y1:
xa = x1
ya = y1
wa = w
ha = y2 - y1
xb = x2 + w
yb = y1
wb = x1 - x2
hb = h - y2 + y1
elif x2 <= x1 and y2 <= y1:
xa = x2 + w
ya = y1
wa = x1 - x2
ha = h
xb = x1
yb = y2 + h
wb = w
hb = y1 - y2
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetBrush(self.parent.BackgroundBrush)
dc.DrawRectangle(xa, ya, wa, ha)
dc.DrawRectangle(xb, yb, wb, hb)
self.PrevMoveXY = xy_tl
if self.parent._ForeDrawList:
dc.DrawBitmapPoint(self.parent._ForegroundBuffer,xy_tl)
else:
dc.DrawBitmapPoint(self.parent._Buffer,xy_tl)
dc.EndDrawing()
def OnWheel(self, event):
"""
By default, zoom in/out by a 0.1 factor per Wheel event.
"""
if event.GetWheelRotation() < 0:
self.parent.Zoom(0.9)
else:
self.parent.Zoom(1.1)
class GUIZoomIn(GUIBase):
Cursor = MagPlusCursor
def __init__(self, parent):
GUIBase.__init__(self, parent)
self.StartRBBox = None
self.PrevRBBox = None
def OnLeftDown(self, event):
self.StartRBBox = N.array( event.GetPosition() )
self.PrevRBBox = None
self.parent.CaptureMouse()
def OnLeftUp(self, event):
#if self.parent.HasCapture():
# self.parent.ReleaseMouse()
if event.LeftUp() and not self.StartRBBox is None:
self.PrevRBBox = None
EndRBBox = event.GetPosition()
StartRBBox = self.StartRBBox
# if mouse has moved less that ten pixels, don't use the box.
if ( abs(StartRBBox[0] - EndRBBox[0]) > 10
and abs(StartRBBox[1] - EndRBBox[1]) > 10 ):
EndRBBox = self.parent.PixelToWorld(EndRBBox)
StartRBBox = self.parent.PixelToWorld(StartRBBox)
BB = N.array(((min(EndRBBox[0],StartRBBox[0]),
min(EndRBBox[1],StartRBBox[1])),
(max(EndRBBox[0],StartRBBox[0]),
max(EndRBBox[1],StartRBBox[1]))),N.float_)
self.parent.ZoomToBB(BB)
else:
Center = self.parent.PixelToWorld(StartRBBox)
self.parent.Zoom(1.5,Center)
self.StartRBBox = None
def OnMove(self, event):
# Allways raise the Move event.
self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION)
if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None):
xy0 = self.StartRBBox
xy1 = N.array( event.GetPosition() )
wh = abs(xy1 - xy0)
wh[0] = max(wh[0], int(wh[1]*self.parent.AspectRatio))
wh[1] = int(wh[0] / self.parent.AspectRatio)
xy_c = (xy0 + xy1) / 2
dc = wx.ClientDC(self.parent)
dc.BeginDrawing()
dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
dc.SetBrush(wx.TRANSPARENT_BRUSH)
dc.SetLogicalFunction(wx.XOR)
if self.PrevRBBox:
dc.DrawRectanglePointSize(*self.PrevRBBox)
self.PrevRBBox = ( xy_c - wh/2, wh )
dc.DrawRectanglePointSize( *self.PrevRBBox )
dc.EndDrawing()
def UpdateScreen(self):
"""
Update gets called if the screen has been repainted in the middle of a zoom in
so the Rubber Band Box can get updated
"""
if self.PrevRBBox is not None:
dc = wx.ClientDC(self.parent)
dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
dc.SetBrush(wx.TRANSPARENT_BRUSH)
dc.SetLogicalFunction(wx.XOR)
dc.DrawRectanglePointSize(*self.PrevRBBox)
def OnRightDown(self, event):
self.parent.Zoom(1/1.5, event.GetPosition(), centerCoords="pixel")
def OnWheel(self, event):
if event.GetWheelRotation() < 0:
self.parent.Zoom(0.9)
else:
self.parent.Zoom(1.1)
class GUIZoomOut(GUIBase):
Cursor = MagMinusCursor
def OnLeftDown(self, event):
self.parent.Zoom(1/1.5, event.GetPosition(), centerCoords="pixel")
def OnRightDown(self, event):
self.parent.Zoom(1.5, event.GetPosition(), centerCoords="pixel")
def OnWheel(self, event):
if event.GetWheelRotation() < 0:
self.parent.Zoom(0.9)
else:
self.parent.Zoom(1.1)
def OnMove(self, event):
# Allways raise the Move event.
self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION)

View File

@@ -4,18 +4,9 @@ A Panel that includes the FloatCanvas and Navigation controls
"""
import wx
import FloatCanvas, Resources
ID_ZOOM_IN_BUTTON = wx.NewId()
ID_ZOOM_OUT_BUTTON = wx.NewId()
ID_ZOOM_TO_FIT_BUTTON = wx.NewId()
ID_MOVE_MODE_BUTTON = wx.NewId()
ID_POINTER_BUTTON = wx.NewId()
#---------------------------------------------------------------------------
class NavCanvas(wx.Panel):
"""
NavCanvas.py
@@ -23,96 +14,80 @@ class NavCanvas(wx.Panel):
This is a high level window that encloses the FloatCanvas in a panel
and adds a Navigation toolbar.
Copyright: Christopher Barker)
"""
License: Same as the version of wxPython you are using it with
Please let me know if you're using this!!!
Contact me at:
Chris.Barker@noaa.gov
"""
def __init__(self, parent, id = -1,
size = wx.DefaultSize,
**kwargs): # The rest just get passed into FloatCanvas
wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size)
def __init__(self,
parent,
id = wx.ID_ANY,
size = wx.DefaultSize,
**kwargs): # The rest just get passed into FloatCanvas
wx.Panel.__init__(self, parent, id, size=size)
self.BuildToolbar()
## Create the vertical sizer for the toolbar and Panel
box = wx.BoxSizer(wx.VERTICAL)
box.Add(self.BuildToolbar(), 0, wx.ALL | wx.ALIGN_LEFT | wx.GROW, 4)
self.Canvas = FloatCanvas.FloatCanvas( self, wx.NewId(),
size = wx.DefaultSize,
**kwargs)
box.Add(self.Canvas,1,wx.GROW)
box.Add(self.ToolBar, 0, wx.ALL | wx.ALIGN_LEFT | wx.GROW, 4)
box.Fit(self)
self.SetSizer(box)
self.Canvas = FloatCanvas.FloatCanvas(self, **kwargs)
box.Add(self.Canvas, 1, wx.GROW)
self.SetSizerAndFit(box)
import GUIMode # here so that it doesn't get imported before wx.App()
self.GUIZoomIn = GUIMode.GUIZoomIn(self.Canvas)
self.GUIZoomOut = GUIMode.GUIZoomOut(self.Canvas)
self.GUIMove = GUIMode.GUIMove(self.Canvas)
self.GUIMouse = GUIMode.GUIMouse(self.Canvas)
# default to Mouse mode
self.ToolBar.ToggleTool(ID_POINTER_BUTTON,1)
self.Canvas.SetMode("Mouse")
self.ToolBar.ToggleTool(self.PointerTool.GetId(), True)
self.Canvas.SetMode(self.GUIMouse)
return None
def __getattr__(self, name):
"""
Delegate all extra methods to the Canvas
"""
attrib = getattr(self.Canvas, name)
## add the attribute to this module's dict for future calls
self.__dict__[name] = attrib
return attrib
def BuildToolbar(self):
tb = wx.ToolBar(self,-1)
tb = wx.ToolBar(self)
self.ToolBar = tb
tb.SetToolBitmapSize((24,24))
tb.AddTool(ID_POINTER_BUTTON, Resources.getPointerBitmap(), isToggle=True, shortHelpString = "Pointer")
wx.EVT_TOOL(self, ID_POINTER_BUTTON, self.SetToolMode)
tb.AddTool(ID_ZOOM_IN_BUTTON, Resources.getMagPlusBitmap(), isToggle=True, shortHelpString = "Zoom In")
wx.EVT_TOOL(self, ID_ZOOM_IN_BUTTON, self.SetToolMode)
tb.AddTool(ID_ZOOM_OUT_BUTTON, Resources.getMagMinusBitmap(), isToggle=True, shortHelpString = "Zoom Out")
wx.EVT_TOOL(self, ID_ZOOM_OUT_BUTTON, self.SetToolMode)
tb.AddTool(ID_MOVE_MODE_BUTTON, Resources.getHandBitmap(), isToggle=True, shortHelpString = "Move")
wx.EVT_TOOL(self, ID_MOVE_MODE_BUTTON, self.SetToolMode)
self.PointerTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getPointerBitmap(), shortHelp = "Pointer")
self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIMouse), self.PointerTool)
self.ZoomInTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getMagPlusBitmap(), shortHelp = "Zoom In")
self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIZoomIn), self.ZoomInTool)
self.ZoomOutTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getMagMinusBitmap(), shortHelp = "Zoom Out")
self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIZoomOut), self.ZoomOutTool)
self.MoveTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getHandBitmap(), shortHelp = "Move")
self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIMove), self.MoveTool)
tb.AddSeparator()
tb.AddControl(wx.Button(tb, ID_ZOOM_TO_FIT_BUTTON, "Zoom To Fit",wx.DefaultPosition, wx.DefaultSize))
wx.EVT_BUTTON(self, ID_ZOOM_TO_FIT_BUTTON, self.ZoomToFit)
self.ZoomButton = wx.Button(tb, label="Zoom To Fit")
tb.AddControl(self.ZoomButton)
self.ZoomButton.Bind(wx.EVT_BUTTON, self.ZoomToFit)
tb.Realize()
S = tb.GetSize()
tb.SetSizeHints(S[0],S[1])
## fixme: remove this when the bug is fixed!
wx.CallAfter(self.HideShowHack) # this required on wxPython 2.8.3 on OS-X
return tb
def SetToolMode(self,event):
for id in [ID_ZOOM_IN_BUTTON,
ID_ZOOM_OUT_BUTTON,
ID_MOVE_MODE_BUTTON,
ID_POINTER_BUTTON]:
self.ToolBar.ToggleTool(id,0)
self.ToolBar.ToggleTool(event.GetId(),1)
if event.GetId() == ID_ZOOM_IN_BUTTON:
self.Canvas.SetMode("ZoomIn")
elif event.GetId() == ID_ZOOM_OUT_BUTTON:
self.Canvas.SetMode("ZoomOut")
elif event.GetId() == ID_MOVE_MODE_BUTTON:
self.Canvas.SetMode("Move")
elif event.GetId() == ID_POINTER_BUTTON:
self.Canvas.SetMode("Mouse")
def HideShowHack(self):
##fixme: remove this when the bug is fixed!
"""
Hack to hide and show button on toolbar to get around OS-X bug on
wxPython2.8 on OS-X
"""
self.ZoomButton.Hide()
self.ZoomButton.Show()
def SetMode(self, Mode):
self.Canvas.SetMode(Mode)
def ZoomToFit(self,Event):
self.Canvas.ZoomToBB()
self.Canvas.SetFocus() # Otherwise the focus stays on the Button, and wheel events are lost.

View File

@@ -0,0 +1,170 @@
"""
A Bounding Box object and assorted utilities , subclassed from a numpy array
"""
import numpy as N
class BBox(N.ndarray):
"""
A Bounding Box object:
Takes Data as an array. Data is any python sequence that can be turned into a
2x2 numpy array of floats:
[[MinX, MinY ],
[MaxX, MaxY ]]
It is a subclass of numpy.ndarray, so for the most part it can be used as
an array, and arrays that fit the above description can be used in its place.
Usually created by the factory functions:
asBBox
and
fromPoints
"""
def __new__(subtype, data):
"""
Takes Data as an array. Data is any python sequence that can be turned into a
2x2 numpy array of floats:
[[MinX, MinY ],
[MaxX, MaxY ]]
You don't usually call this directly. BBox objects are created with the factory functions:
asBBox
and
fromPoints
"""
arr = N.array(data, N.float)
arr.shape = (2,2)
if arr[0,0] > arr[1,0] or arr[0,1] > arr[1,1]:
# note: zero sized BB OK.
raise ValueError("BBox values not aligned: \n minimum values must be less that maximum values")
return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr)
def Overlaps(self, BB):
"""
Overlap(BB):
Tests if the given Bounding Box overlaps with this one.
Returns True is the Bounding boxes overlap, False otherwise
If they are just touching, returns True
"""
if ( (self[1,0] >= BB[0,0]) and (self[0,0] <= BB[1,0]) and
(self[1,1] >= BB[0,1]) and (self[0,1] <= BB[1,1]) ):
return True
else:
return False
def Inside(self, BB):
"""
Inside(BB):
Tests if the given Bounding Box is entirely inside this one.
Returns True if it is entirely inside, or touching the
border.
Returns False otherwise
"""
if ( (BB[0,0] >= self[0,0]) and (BB[1,0] <= self[1,0]) and
(BB[0,1] >= self[0,1]) and (BB[1,1] <= self[1,1]) ):
return True
else:
return False
def Merge(self, BB):
"""
Joins this bounding box with the one passed in, maybe making this one bigger
"""
if BB[0,0] < self[0,0]: self[0,0] = BB[0,0]
if BB[0,1] < self[0,1]: self[0,1] = BB[0,1]
if BB[1,0] > self[1,0]: self[1,0] = BB[1,0]
if BB[1,1] > self[1,1]: self[1,1] = BB[1,1]
### This could be used for a make BB from a bunch of BBs
#~ def _getboundingbox(bboxarray): # lrk: added this
#~ # returns the bounding box of a bunch of bounding boxes
#~ upperleft = N.minimum.reduce(bboxarray[:,0])
#~ lowerright = N.maximum.reduce(bboxarray[:,1])
#~ return N.array((upperleft, lowerright), N.float)
#~ _getboundingbox = staticmethod(_getboundingbox)
## Save the ndarray __eq__ for internal use.
Array__eq__ = N.ndarray.__eq__
def __eq__(self, BB):
"""
__eq__(BB) The equality operator
A == B if and only if all the entries are the same
"""
return N.all(self.Array__eq__(BB))
def asBBox(data):
"""
returns a BBox object.
If object is a BBox, it is returned unaltered
If object is a numpy array, a BBox object is returned that shares a
view of the data with that array
"""
if isinstance(data, BBox):
return data
arr = N.asarray(data, N.float)
return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr)
def fromPoints(Points):
"""
fromPoints (Points).
reruns the bounding box of the set of points in Points. Points can
be any python object that can be turned into a numpy NX2 array of Floats.
If a single point is passed in, a zero-size Bounding Box is returned.
"""
Points = N.asarray(Points, N.float).reshape(-1,2)
arr = N.vstack( (Points.min(0), Points.max(0)) )
return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr)
def fromBBArray(BBarray):
"""
Builds a BBox object from an array of Bounding Boxes.
The resulting Bounding Box encompases all the included BBs.
The BBarray is in the shape: (Nx2x2) where BBarray[n] is a 2x2 array that represents a BBox
"""
#upperleft = N.minimum.reduce(BBarray[:,0])
#lowerright = N.maximum.reduce(BBarray[:,1])
# BBarray = N.asarray(BBarray, N.float).reshape(-1,2)
# arr = N.vstack( (BBarray.min(0), BBarray.max(0)) )
BBarray = N.asarray(BBarray, N.float).reshape(-1,2,2)
arr = N.vstack( (BBarray[:,0,:].min(0), BBarray[:,1,:].max(0)) )
return asBBox(arr)
#return asBBox( (upperleft, lowerright) ) * 2

View File

@@ -0,0 +1,354 @@
"""
Test code for the BBox Object
"""
import unittest
from BBox import *
class testCreator(unittest.TestCase):
def testCreates(self):
B = BBox(((0,0),(5,5)))
self.failUnless(isinstance(B, BBox))
def testType(self):
B = N.array(((0,0),(5,5)))
self.failIf(isinstance(B, BBox))
def testDataType(self):
B = BBox(((0,0),(5,5)))
self.failUnless(B.dtype == N.float)
def testShape(self):
B = BBox((0,0,5,5))
self.failUnless(B.shape == (2,2))
def testShape2(self):
self.failUnlessRaises(ValueError, BBox, (0,0,5) )
def testShape3(self):
self.failUnlessRaises(ValueError, BBox, (0,0,5,6,7) )
def testArrayConstruction(self):
A = N.array(((4,5),(10,12)), N.float_)
B = BBox(A)
self.failUnless(isinstance(B, BBox))
def testMinMax(self):
self.failUnlessRaises(ValueError, BBox, (0,0,-1,6) )
def testMinMax2(self):
self.failUnlessRaises(ValueError, BBox, (0,0,1,-6) )
def testMinMax(self):
# OK to have a zero-sized BB
B = BBox(((0,0),(0,5)))
self.failUnless(isinstance(B, BBox))
def testMinMax2(self):
# OK to have a zero-sized BB
B = BBox(((10.0,-34),(10.0,-34.0)))
self.failUnless(isinstance(B, BBox))
def testMinMax3(self):
# OK to have a tiny BB
B = BBox(((0,0),(1e-20,5)))
self.failUnless(isinstance(B, BBox))
def testMinMax4(self):
# Should catch tiny difference
self.failUnlessRaises(ValueError, BBox, ((0,0), (-1e-20,5)) )
class testAsBBox(unittest.TestCase):
def testPassThrough(self):
B = BBox(((0,0),(5,5)))
C = asBBox(B)
self.failUnless(B is C)
def testPassThrough2(self):
B = (((0,0),(5,5)))
C = asBBox(B)
self.failIf(B is C)
def testPassArray(self):
# Different data type
A = N.array( (((0,0),(5,5))) )
C = asBBox(A)
self.failIf(A is C)
def testPassArray2(self):
# same data type -- should be a view
A = N.array( (((0,0),(5,5))), N.float_ )
C = asBBox(A)
A[0,0] = -10
self.failUnless(C[0,0] == A[0,0])
class testIntersect(unittest.TestCase):
def testSame(self):
B = BBox(((-23.5, 456),(56, 532.0)))
C = BBox(((-23.5, 456),(56, 532.0)))
self.failUnless(B.Overlaps(C) )
def testUpperLeft(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (0, 12),(10, 32.0) ) )
self.failUnless(B.Overlaps(C) )
def testUpperRight(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (12, 12),(25, 32.0) ) )
self.failUnless(B.Overlaps(C) )
def testLowerRight(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (12, 5),(25, 15) ) )
self.failUnless(B.Overlaps(C) )
def testLowerLeft(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (-10, 5),(8.5, 15) ) )
self.failUnless(B.Overlaps(C) )
def testBelow(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (-10, 5),(8.5, 9.2) ) )
self.failIf(B.Overlaps(C) )
def testAbove(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (-10, 25.001),(8.5, 32) ) )
self.failIf(B.Overlaps(C) )
def testLeft(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (4, 8),(4.95, 32) ) )
self.failIf(B.Overlaps(C) )
def testRight(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (17.1, 8),(17.95, 32) ) )
self.failIf(B.Overlaps(C) )
def testInside(self):
B = BBox( ( (-15, -25),(-5, -10) ) )
C = BBox( ( (-12, -22), (-6, -8) ) )
self.failUnless(B.Overlaps(C) )
def testOutside(self):
B = BBox( ( (-15, -25),(-5, -10) ) )
C = BBox( ( (-17, -26), (3, 0) ) )
self.failUnless(B.Overlaps(C) )
def testTouch(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (15, 8),(17.95, 32) ) )
self.failUnless(B.Overlaps(C) )
def testCorner(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (15, 25),(17.95, 32) ) )
self.failUnless(B.Overlaps(C) )
def testZeroSize(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (15, 25),(15, 25) ) )
self.failUnless(B.Overlaps(C) )
def testZeroSize2(self):
B = BBox( ( (5, 10),(5, 10) ) )
C = BBox( ( (15, 25),(15, 25) ) )
self.failIf(B.Overlaps(C) )
def testZeroSize3(self):
B = BBox( ( (5, 10),(5, 10) ) )
C = BBox( ( (0, 8),(10, 12) ) )
self.failUnless(B.Overlaps(C) )
def testZeroSize4(self):
B = BBox( ( (5, 1),(10, 25) ) )
C = BBox( ( (8, 8),(8, 8) ) )
self.failUnless(B.Overlaps(C) )
class testEquality(unittest.TestCase):
def testSame(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
self.failUnless(B == C)
def testIdentical(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
self.failUnless(B == B)
def testNotSame(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = BBox( ( (1.0, 2.0), (5.0, 10.1) ) )
self.failIf(B == C)
def testWithArray(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = N.array( ( (1.0, 2.0), (5.0, 10.0) ) )
self.failUnless(B == C)
def testWithArray2(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = N.array( ( (1.0, 2.0), (5.0, 10.0) ) )
self.failUnless(C == B)
def testWithArray2(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = N.array( ( (1.01, 2.0), (5.0, 10.0) ) )
self.failIf(C == B)
class testInside(unittest.TestCase):
def testSame(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
self.failUnless(B.Inside(C))
def testPoint(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = BBox( ( (3.0, 4.0), (3.0, 4.0) ) )
self.failUnless(B.Inside(C))
def testPointOutside(self):
B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
C = BBox( ( (-3.0, 4.0), (0.10, 4.0) ) )
self.failIf(B.Inside(C))
def testUpperLeft(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (0, 12),(10, 32.0) ) )
self.failIf(B.Inside(C) )
def testUpperRight(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (12, 12),(25, 32.0) ) )
self.failIf(B.Inside(C) )
def testLowerRight(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (12, 5),(25, 15) ) )
self.failIf(B.Inside(C) )
def testLowerLeft(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (-10, 5),(8.5, 15) ) )
self.failIf(B.Inside(C) )
def testBelow(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (-10, 5),(8.5, 9.2) ) )
self.failIf(B.Inside(C) )
def testAbove(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (-10, 25.001),(8.5, 32) ) )
self.failIf(B.Inside(C) )
def testLeft(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (4, 8),(4.95, 32) ) )
self.failIf(B.Inside(C) )
def testRight(self):
B = BBox( ( (5, 10),(15, 25) ) )
C = BBox( ( (17.1, 8),(17.95, 32) ) )
self.failIf(B.Inside(C) )
class testFromPoints(unittest.TestCase):
def testCreate(self):
Pts = N.array( ((5,2),
(3,4),
(1,6),
), N.float_ )
B = fromPoints(Pts)
#B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) )
self.failUnless(B[0,0] == 1.0 and
B[0,1] == 2.0 and
B[1,0] == 5.0 and
B[1,1] == 6.0
)
def testCreateInts(self):
Pts = N.array( ((5,2),
(3,4),
(1,6),
) )
B = fromPoints(Pts)
self.failUnless(B[0,0] == 1.0 and
B[0,1] == 2.0 and
B[1,0] == 5.0 and
B[1,1] == 6.0
)
def testSinglePoint(self):
Pts = N.array( (5,2), N.float_ )
B = fromPoints(Pts)
self.failUnless(B[0,0] == 5.0 and
B[0,1] == 2.0 and
B[1,0] == 5.0 and
B[1,1] == 2.0
)
def testListTuples(self):
Pts = [ (3, 6.5),
(13, 43.2),
(-4.32, -4),
(65, -23),
(-0.0001, 23.432),
]
B = fromPoints(Pts)
self.failUnless(B[0,0] == -4.32 and
B[0,1] == -23.0 and
B[1,0] == 65.0 and
B[1,1] == 43.2
)
class testMerge(unittest.TestCase):
A = BBox( ((-23.5, 456), (56, 532.0)) )
B = BBox( ((-20.3, 460), (54, 465 )) )# B should be completely inside A
C = BBox( ((-23.5, 456), (58, 540.0)) )# up and to the right or A
D = BBox( ((-26.5, 12), (56, 532.0)) )
def testInside(self):
C = self.A.copy()
C.Merge(self.B)
self.failUnless(C == self.A)
def testFullOutside(self):
C = self.B.copy()
C.Merge(self.A)
self.failUnless(C == self.A)
def testUpRight(self):
A = self.A.copy()
A.Merge(self.C)
self.failUnless(A[0] == self.A[0] and A[1] == self.C[1])
def testDownLeft(self):
A = self.A.copy()
A.Merge(self.D)
self.failUnless(A[0] == self.D[0] and A[1] == self.A[1])
class testBBarray(unittest.TestCase):
BBarray = N.array( ( ((-23.5, 456), (56, 532.0)),
((-20.3, 460), (54, 465 )),
((-23.5, 456), (58, 540.0)),
((-26.5, 12), (56, 532.0)),
),
dtype=N.float)
print BBarray
BB = asBBox( ((-26.5, 12.), ( 58. , 540.)) )
def testJoin(self):
BB = fromBBArray(self.BBarray)
self.failUnless(BB == self.BB, "Wrong BB was created. It was:\n%s \nit should have been:\n%s"%(BB, self.BB))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,115 @@
"""
Part of the floatcanvas.Utilities package.
This module contains assorted GUI-related utilities that can be used
with FloatCanvas
So far, they are:
RubberBandBox: used to draw a RubberBand Box on the screen
"""
import wx
from floatcanvas import FloatCanvas
class RubberBandBox:
"""
Class to provide a rubber band box that can be drawn on a Window
"""
def __init__(self, Canvas, CallBack, Tol=5):
"""
To initialize:
RubberBandBox(Canvas, CallBack)
Canvas: the FloatCanvas you want the Rubber band box to be used on
CallBack: is the method you want called when the mouse is
released. That method will be called, passing in a rect
parameter, where rect is: (Point, WH) of the rect in
world coords.
Tol: The tolerance for the smallest rectangle allowed. defaults
to 5. In pixels
Methods:
Enable() : Enables the Rubber Band Box (Binds the events)
Disable() : Enables the Rubber Band Box (Unbinds the events)
Attributes:
CallBack: The callback function, if it's replaced you need to
call Enable() again.
"""
self.Canvas = Canvas
self.CallBack = CallBack
self.Tol = Tol
self.Drawing = False
self.RBRect = None
self.StartPointWorld = None
return None
def Enable(self):
"""
Called when you want the rubber band box to be enabled
"""
# bind events:
self.Canvas.Bind(FloatCanvas.EVT_MOTION, self.OnMove )
self.Canvas.Bind(FloatCanvas.EVT_LEFT_DOWN, self.OnLeftDown)
self.Canvas.Bind(FloatCanvas.EVT_LEFT_UP, self.OnLeftUp )
def Disable(self):
"""
Called when you don't want the rubber band box to be enabled
"""
# unbind events:
self.Canvas.Unbind(FloatCanvas.EVT_MOTION)
self.Canvas.Unbind(FloatCanvas.EVT_LEFT_DOWN)
self.Canvas.Unbind(FloatCanvas.EVT_LEFT_UP)
def OnMove(self, event):
if self.Drawing:
x, y = self.StartPoint
Cornerx, Cornery = event.GetPosition()
w, h = ( Cornerx - x, Cornery - y)
if abs(w) > self.Tol and abs(h) > self.Tol:
# draw the RB box
dc = wx.ClientDC(self.Canvas)
dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
dc.SetBrush(wx.TRANSPARENT_BRUSH)
dc.SetLogicalFunction(wx.XOR)
if self.RBRect:
dc.DrawRectangle(*self.RBRect)
self.RBRect = (x, y, w, h )
dc.DrawRectangle(*self.RBRect)
event.Skip() # skip so that other events can catch these
def OnLeftDown(self, event):
# Start drawing
self.Drawing = True
self.StartPoint = event.GetPosition()
self.StartPointWorld = event.Coords
def OnLeftUp(self, event):
# Stop Drawing
if self.Drawing:
self.Drawing = False
if self.RBRect:
WH = event.Coords - self.StartPointWorld
wx.CallAfter(self.CallBack, (self.StartPointWorld, WH))
self.RBRect = None
self.StartPointWorld = None

View File

@@ -0,0 +1,7 @@
"""
__init__ for the floatcanvas Utilities package
"""
pass

View File

@@ -29,8 +29,7 @@ It is double buffered, so re-draws after the window is uncovered by
something else are very quick.
It relies on NumPy, which is needed for speed (maybe, I haven't profiled
it). It will also use numarray, if you don't have Numeric, but it is
slower.
properly) and convenience.
Bugs and Limitations: Lots: patches, fixes welcome
@@ -60,12 +59,6 @@ If you are zoomed in, it checks the Bounding box of an object before
drawing it. This makes it a great deal faster when there are a lot of
objects and you are zoomed in so that only a few are shown.
One solution is to be able to pass some sort of object set to the DC
directly. I've used DC.DrawPointList(Points), and it helped a lot with
drawing lots of points. However, when zoomed in, the Bounding boxes need
to be checked, so I may some day write C++ code that does the loop and
checks the BBs.
Mouse Events:
There are a full set of custom mouse events. They are just like the
@@ -80,19 +73,25 @@ clicked, mouse-over'd, etc.
See the Demo for what it can do, and how to use it.
Copyright: Christopher Barker
License: Same as the version of wxPython you are using it with.
TRAC site for some docs and updates:
http://morticia.cs.dal.ca/FloatCanvas/
SVN for latest code:
svn://morticia.cs.dal.ca/FloatCanvas
Mailing List:
http://mail.mithis.com/cgi-bin/mailman/listinfo/floatcanvas
Check for updates or answers to questions, send me an email.
Please let me know if you're using this!!!
Contact me at:
Chris.Barker@noaa.gov
"""
__version__ = "0.9.10"
__version__ = "0.9.18"

View File

@@ -95,7 +95,10 @@ class ImageView(wx.Window):
if image is None:
return
bmp = image.ConvertToBitmap()
try:
bmp = image.ConvertToBitmap()
except:
return
iwidth = bmp.GetWidth() # dimensions of image file
iheight = bmp.GetHeight()

View File

@@ -221,6 +221,8 @@ class InspectionFrame(wx.Frame):
def OnClose(self, evt):
self.SaveSettings(self.config)
self.mgr.UnInit()
del self.mgr
evt.Skip()
@@ -533,6 +535,9 @@ class InspectionInfoPanel(wx.stc.StyledTextCtrl):
def FmtSizerItem(self, obj):
if obj is None:
return ['SizerItem: None']
st = ['SizerItem:']
st.append(self.Fmt('proportion', obj.GetProportion()))
st.append(self.Fmt('flag',

View File

@@ -47,6 +47,30 @@ class MaskedComboBoxSelectEvent(wx.PyCommandEvent):
this event was generated."""
return self.__selection
class MaskedComboBoxEventHandler(wx.EvtHandler):
"""
This handler ensures that the derived control can react to events
from the base control before any external handlers run, to ensure
proper behavior.
"""
def __init__(self, combobox):
wx.EvtHandler.__init__(self)
self.combobox = combobox
combobox.PushEventHandler(self)
self.Bind(wx.EVT_SET_FOCUS, self.combobox._OnFocus ) ## defeat automatic full selection
self.Bind(wx.EVT_KILL_FOCUS, self.combobox._OnKillFocus ) ## run internal validator
self.Bind(wx.EVT_LEFT_DCLICK, self.combobox._OnDoubleClick) ## select field under cursor on dclick
self.Bind(wx.EVT_RIGHT_UP, self.combobox._OnContextMenu ) ## bring up an appropriate context menu
self.Bind(wx.EVT_CHAR, self.combobox._OnChar ) ## handle each keypress
self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDownInComboBox ) ## for special processing of up/down keys
self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDown ) ## for processing the rest of the control keys
## (next in evt chain)
self.Bind(wx.EVT_COMBOBOX, self.combobox._OnDropdownSelect ) ## to bring otherwise completely independent base
## ctrl selection into maskededit framework
self.Bind(wx.EVT_TEXT, self.combobox._OnTextChange ) ## color control appropriately & keep
## track of previous value for undo
class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
"""
@@ -152,18 +176,15 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
self._SetKeycodeHandler(wx.WXK_UP, self._OnSelectChoice)
self._SetKeycodeHandler(wx.WXK_DOWN, self._OnSelectChoice)
self.replace_next_combobox_event = False
self.correct_selection = -1
if setupEventHandling:
## Setup event handlers
self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection
self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator
self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick
self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu
self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress
self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDownInComboBox ) ## for special processing of up/down keys
self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys
## (next in evt chain)
self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep
## track of previous value for undo
## Setup event handling functions through event handler object,
## to guarantee processing prior to giving event callbacks from
## outside the class:
self.evt_handler = MaskedComboBoxEventHandler(self)
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnWindowDestroy )
@@ -171,6 +192,13 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
return "<MaskedComboBox: %s>" % self.GetValue()
def OnWindowDestroy(self, event):
# clean up associated event handler object:
if self.RemoveEventHandler(self.evt_handler):
self.evt_handler.Destroy()
event.Skip()
def _CalcSize(self, size=None):
"""
Calculate automatic size if allowed; augment base mixin function
@@ -252,7 +280,9 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
# Record current selection and insertion point, for undo
self._prevSelection = self._GetSelection()
self._prevInsertionPoint = self._GetInsertionPoint()
## dbg('MaskedComboBox::_SetValue(%s), selection beforehand: %d' % (value, self.GetSelection()))
wx.ComboBox.SetValue(self, value)
## dbg('MaskedComboBox::_SetValue(%s), selection now: %d' % (value, self.GetSelection()))
# text change events don't always fire, so we check validity here
# to make certain formatting is applied:
self._CheckValid()
@@ -264,11 +294,14 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
masked control. NOTE: this must be done in the class derived
from the base wx control.
"""
## dbg('MaskedComboBox::SetValue(%s)' % value, indent=1)
if not self._mask:
wx.ComboBox.SetValue(value) # revert to base control behavior
## dbg('no mask; deferring to base class', indent=0)
return
# else...
# empty previous contents, replacing entire value:
## dbg('MaskedComboBox::SetValue: selection beforehand: %d' % (self.GetSelection()))
self._SetInsertionPoint(0)
self._SetSelection(0, self._masklength)
@@ -306,15 +339,26 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
dateparts = value.split(' ')
dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
value = string.join(dateparts, ' ')
## dbg('adjusted value: "%s"' % value)
value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
else:
raise
## dbg('adjusted value: "%s"' % value)
self._SetValue(value)
#### dbg('queuing insertion after .SetValue', replace_to)
wx.CallAfter(self._SetInsertionPoint, replace_to)
wx.CallAfter(self._SetSelection, replace_to, replace_to)
# Attempt to compensate for fact that calling .SetInsertionPoint() makes the
# selection index -1, even if the resulting set value is in the list.
# So, if we are setting a value that's in the list, use index selection instead.
if value in self._choices:
index = self._choices.index(value)
self._prevValue = self._curValue
self._curValue = self._choices[index]
self._ctrl_constraints._autoCompleteIndex = index
self.SetSelection(index)
else:
self._SetValue(value)
#### dbg('queuing insertion after .SetValue', replace_to)
wx.CallAfter(self._SetInsertionPoint, replace_to)
wx.CallAfter(self._SetSelection, replace_to, replace_to)
## dbg(indent=0)
def _Refresh(self):
@@ -509,26 +553,60 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
Necessary override for bookkeeping on choice selection, to keep current value
current.
"""
## dbg('MaskedComboBox::SetSelection(%d)' % index)
## dbg('MaskedComboBox::SetSelection(%d)' % index, indent=1)
if self._mask:
self._prevValue = self._curValue
self._curValue = self._choices[index]
self._ctrl_constraints._autoCompleteIndex = index
if index != -1:
self._curValue = self._choices[index]
else:
self._curValue = None
wx.ComboBox.SetSelection(self, index)
## dbg('selection now: %d' % self.GetCurrentSelection(), indent=0)
def _OnKeyDownInComboBox(self, event):
"""
This function is necessary because navigation and control key
events do not seem to normally be seen by the wxComboBox's
EVT_CHAR routine. (Tabs don't seem to be visible no matter
what... {:-( )
This function is necessary because navigation and control key events
do not seem to normally be seen by the wxComboBox's EVT_CHAR routine.
(Tabs don't seem to be visible no matter what, except for CB_READONLY
controls, for some bizarre reason... {:-( )
"""
key = event.GetKeyCode()
## dbg('MaskedComboBox::OnKeyDownInComboBox(%d)' % key)
if event.GetKeyCode() in self._nav + self._control:
self._OnChar(event)
return
if not self._IsEditable():
# WANTS_CHARS with CB_READONLY apparently prevents navigation on WXK_TAB;
# ensure we can still navigate properly, as maskededit mixin::OnChar assumes
# that event.Skip() will just work, but it doesn't:
if self._keyhandlers.has_key(key):
self._keyhandlers[key](event)
# else pass
else:
## dbg('calling OnChar()')
self._OnChar(event)
else:
event.Skip() # let mixin default KeyDown behavior occur
## dbg(indent=0)
def _OnDropdownSelect(self, event):
"""
This function appears to be necessary because dropdown selection seems to
manipulate the contents of the control in an inconsistent way, properly
changing the selection index, but *not* the value. (!) Calling SetSelection()
on a selection event for the same selection would seem like a nop, but it seems to
fix the problem.
"""
## dbg('MaskedComboBox::OnDropdownSelect(%d)' % event.GetSelection(), indent=1)
if self.replace_next_combobox_event:
## dbg('replacing EVT_COMBOBOX')
self.replace_next_combobox_event = False
self._OnAutoSelect(self._ctrl_constraints, self.correct_selection)
else:
## dbg('skipping EVT_COMBOBOX')
event.Skip()
## dbg(indent=0)
def _OnSelectChoice(self, event):
@@ -585,7 +663,7 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
Override mixin (empty) autocomplete handler, so that autocompletion causes
combobox to update appropriately.
"""
## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
## dbg('MaskedComboBox::OnAutoSelect(%d, %d)' % (field._index, match_index), indent=1)
## field._autoCompleteIndex = match_index
if field == self._ctrl_constraints:
self.SetSelection(match_index)
@@ -594,7 +672,7 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
self._CheckValid()
## dbg('field._autoCompleteIndex:', match_index)
## dbg('self.GetSelection():', self.GetSelection())
## dbg('self.GetCurrentSelection():', self.GetCurrentSelection())
end = self._goEnd(getPosOnly=True)
## dbg('scheduling set of end position to:', end)
# work around bug in wx 2.5
@@ -614,15 +692,24 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
item in the list. (and then does the usual OnReturn bit.)
"""
## dbg('MaskedComboBox::OnReturn', indent=1)
## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
## dbg('current value: "%s"' % self.GetValue(), 'current selection:', self.GetCurrentSelection())
if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
## wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
self.replace_next_combobox_event = True
self.correct_selection = self._ctrl_constraints._autoCompleteIndex
event.m_keyCode = wx.WXK_TAB
event.Skip()
## dbg(indent=0)
def _LostFocus(self):
## dbg('MaskedComboBox::LostFocus; Selection=%d, value="%s"' % (self.GetSelection(), self.GetValue()))
if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
"""
The "user-visible" masked combobox control, this class is
@@ -659,6 +746,19 @@ class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
__i = 0
## CHANGELOG:
## ====================
## Version 1.4
## 1. Added handler for EVT_COMBOBOX to address apparently inconsistent behavior
## of control when the dropdown control is used to do a selection.
## NOTE: due to misbehavior of wx.ComboBox re: losing all concept of the
## current selection index if SetInsertionPoint() is called, which is required
## to support masked .SetValue(), this control is flaky about retaining selection
## information. I can't truly fix this without major changes to the base control,
## but I've tried to compensate as best I can.
## TODO: investigate replacing base control with ComboCtrl instead...
## 2. Fixed navigation in readonly masked combobox, which was not working because
## the base control doesn't do navigation if style=CB_READONLY|WANTS_CHARS.
##
##
## Version 1.3
## 1. Made definition of "hack" GetMark conditional on base class not
## implementing it properly, to allow for migration in wx code base

View File

@@ -1,12 +1,12 @@
#----------------------------------------------------------------------------
# Name: maskededit.py
# Authors: Jeff Childers, Will Sadkin
# Email: jchilders_98@yahoo.com, wsadkin@parlancecorp.com
# Authors: Will Sadkin, Jeff Childers
# Email: wsadkin@parlancecorp.com, jchilders_98@yahoo.com
# Created: 02/11/2003
# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003
# Portions: (c) 2002 by Will Sadkin, 2002-2006
# Portions: (c) 2002 by Will Sadkin, 2002-2007
# RCS-ID: $Id$
# License: wxWindows license
# License: wxWidgets license
#----------------------------------------------------------------------------
# NOTE:
# MaskedEdit controls are based on a suggestion made on [wxPython-Users] by
@@ -27,7 +27,7 @@
#
#----------------------------------------------------------------------------
#
# 03/30/2004 - Will Sadkin (wsadkin@nameconnector.com)
# 03/30/2004 - Will Sadkin (wsadkin@parlancecorp.com)
#
# o Split out TextCtrl, ComboBox and IpAddrCtrl into their own files,
# o Reorganized code into masked package
@@ -337,7 +337,18 @@ to individual fields:
raiseOnInvalidPaste False by default; normally a bad paste simply is ignored with a bell;
if True, this will cause a ValueError exception to be thrown,
with the .value attribute of the exception containing the bad value.
===================== ==================================================================
stopFieldChangeIfInvalid
False by default; tries to prevent navigation out of a field if its
current value is invalid. Can be used to create a hybrid of validation
settings, allowing intermediate invalid values in a field without
sacrificing ability to limit values as with validRequired.
NOTE: It is possible to end up with an invalid value when using
this option if focus is switched to some other control via mousing.
To avoid this, consider deriving a class that defines _LostFocus()
function that returns the control to a valid value when the focus
shifts. (AFAICT, The change in focus is unpreventable.)
===================== =================================================================
Coloring Behavior
@@ -1327,12 +1338,14 @@ class Field:
'emptyInvalid': False, ## Set to True to make EMPTY = INVALID
'description': "", ## primarily for autoformats, but could be useful elsewhere
'raiseOnInvalidPaste': False, ## if True, paste into field will cause ValueError
'stopFieldChangeIfInvalid': False,## if True, disallow field navigation out of invalid field
}
# This list contains all parameters that when set at the control level should
# propagate down to each field:
propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives',
'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste')
'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste',
'stopFieldChangeIfInvalid')
def __init__(self, **kwargs):
"""
@@ -3021,7 +3034,7 @@ class MaskedEditMixin:
char = char.decode(self._defaultEncoding)
else:
char = unichr(event.GetUnicodeKey())
dbg('unicode char:', char)
## dbg('unicode char:', char)
excludes = u''
if type(field._excludeChars) != types.UnicodeType:
excludes += field._excludeChars.decode(self._defaultEncoding)
@@ -3767,6 +3780,21 @@ class MaskedEditMixin:
## dbg(indent=0)
return False
field = self._FindField(sel_to)
index = field._index
field_start, field_end = field._extent
slice = self._GetValue()[field_start:field_end]
## dbg('field._stopFieldChangeIfInvalid?', field._stopFieldChangeIfInvalid)
## dbg('field.IsValid(slice)?', field.IsValid(slice))
if field._stopFieldChangeIfInvalid and not field.IsValid(slice):
## dbg('field invalid; field change disallowed')
if not wx.Validator_IsSilent():
wx.Bell()
## dbg(indent=0)
return False
if event.ShiftDown():
@@ -3775,13 +3803,12 @@ class MaskedEditMixin:
# NOTE: doesn't yet work with SHIFT-tab under wx; the control
# never sees this event! (But I've coded for it should it ever work,
# and it *does* work for '.' in IpAddrCtrl.)
field = self._FindField(pos)
index = field._index
field_start = field._extent[0]
if pos < field_start:
## dbg('cursor before 1st field; cannot change to a previous field')
if not wx.Validator_IsSilent():
wx.Bell()
## dbg(indent=0)
return False
if event.ControlDown():
@@ -3821,8 +3848,6 @@ class MaskedEditMixin:
else:
# "Go forward"
field = self._FindField(sel_to)
field_start, field_end = field._extent
if event.ControlDown():
## dbg('queuing select to end of field:', pos, field_end)
wx.CallAfter(self._SetInsertionPoint, pos)
@@ -3888,10 +3913,19 @@ class MaskedEditMixin:
wx.CallAfter(self._SetInsertionPoint, next_pos)
## dbg(indent=0)
return False
## dbg(indent=0)
def _OnDecimalPoint(self, event):
## dbg('MaskedEditMixin::_OnDecimalPoint', indent=1)
field = self._FindField(self._GetInsertionPoint())
start, end = field._extent
slice = self._GetValue()[start:end]
if field._stopFieldChangeIfInvalid and not field.IsValid(slice):
if not wx.Validator_IsSilent():
wx.Bell()
return False
pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
@@ -4021,7 +4055,7 @@ class MaskedEditMixin:
def _findNextEntry(self,pos, adjustInsert=True):
""" Find the insertion point for the next valid entry character position."""
## dbg('MaskedEditMixin::_findNextEntry', indent=1)
## dbg('MaskedEditMixin::_findNextEntry', indent=1)
if self._isTemplateChar(pos) or pos in self._explicit_field_boundaries: # if changing fields, pay attn to flag
adjustInsert = adjustInsert
else: # else within a field; flag not relevant
@@ -4280,7 +4314,9 @@ class MaskedEditMixin:
#### dbg('field_len?', field_len)
#### dbg('pos==end; len (slice) < field_len?', len(slice) < field_len)
#### dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull)
if len(slice) == field_len and field._moveOnFieldFull:
if( len(slice) == field_len and field._moveOnFieldFull
and (not field._stopFieldChangeIfInvalid or
field._stopFieldChangeIfInvalid and field.IsValid(slice))):
# move cursor to next field:
pos = self._findNextEntry(pos)
self._SetInsertionPoint(pos)
@@ -4317,11 +4353,14 @@ class MaskedEditMixin:
# else make sure the user is not trying to type over a template character
# If they are, move them to the next valid entry position
elif self._isTemplateChar(pos):
if( not field._moveOnFieldFull
and (not self._signOk
or (self._signOk
and field._index == 0
and pos > 0) ) ): # don't move to next field without explicit cursor movement
if( (not field._moveOnFieldFull
and (not self._signOk
or (self._signOk and field._index == 0 and pos > 0) ) )
or (field._stopFieldChangeIfInvalid
and not field.IsValid(self._GetValue()[start:end]) ) ):
# don't move to next field without explicit cursor movement
pass
else:
# find next valid position
@@ -5092,7 +5131,11 @@ class MaskedEditMixin:
#### dbg('field._moveOnFieldFull?', field._moveOnFieldFull)
#### dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start)
if( field._moveOnFieldFull and pos == end
and len(fstr.lstrip()) == end-start): # if field now full
and len(fstr.lstrip()) == end-start # if field now full
and (not field._stopFieldChangeIfInvalid # and we either don't care about valid
or (field._stopFieldChangeIfInvalid # or we do and the current field value is valid
and field.IsValid(fstr)))):
newpos = self._findNextEntry(end) # go to next field
else:
newpos = pos # else keep cursor at current position
@@ -5165,7 +5208,11 @@ class MaskedEditMixin:
if( field._insertRight # if insert-right field (but we didn't start at right edge)
and field._moveOnFieldFull # and should move cursor when full
and len(newtext[start:end].strip()) == end-start): # and field now full
and len(newtext[start:end].strip()) == end-start # and field now full
and (not field._stopFieldChangeIfInvalid # and we either don't care about valid
or (field._stopFieldChangeIfInvalid # or we do and the current field value is valid
and field.IsValid(newtext[start:end].strip())))):
newpos = self._findNextEntry(end) # go to next field
## dbg('newpos = nextentry =', newpos)
else:
@@ -6723,6 +6770,12 @@ __i=0
## CHANGELOG:
## ====================
## Version 1.13
## 1. Added parameter option stopFieldChangeIfInvalid, which can be used to relax the
## validation rules for a control, but make best efforts to stop navigation out of
## that field should its current value be invalid. Note: this does not prevent the
## value from remaining invalid if focus for the control is lost, via mousing etc.
##
## Version 1.12
## 1. Added proper support for NUMPAD keypad keycodes for navigation and control.
##

View File

@@ -2,7 +2,7 @@
# Name: wxPython.lib.masked.numctrl.py
# Author: Will Sadkin
# Created: 09/06/2003
# Copyright: (c) 2003 by Will Sadkin
# Copyright: (c) 2003-2007 by Will Sadkin
# RCS-ID: $Id$
# License: wxWidgets license
#----------------------------------------------------------------------------
@@ -81,6 +81,7 @@ masked.NumCtrl:
min = None,
max = None,
limited = False,
limitOnFieldChange = False,
selectOnEntry = True,
foregroundColour = "Black",
signedForegroundColour = "Red",
@@ -155,6 +156,12 @@ masked.NumCtrl:
If False and bounds are set, out-of-bounds values will
result in a background colored with the current invalidBackgroundColour.
limitOnFieldChange
An alternative to limited, this boolean indicates whether or not a
field change should be allowed if the value in the control
is out of bounds. If True, and control focus is lost, this will also
cause the control to take on the nearest bound value.
selectOnEntry
Boolean indicating whether or not the value in each field of the
control should be automatically selected (for replacement) when
@@ -312,6 +319,18 @@ IsLimited()
Returns <I>True</I> if the control is currently limiting the
value to fall within the current bounds.
SetLimitOnFieldChange()
If called with a value of True, will cause the control to allow
out-of-bounds values, but will prevent field change if attempted
via navigation, and if the control loses focus, it will change
the value to the nearest bound.
GetLimitOnFieldChange()
IsLimitedOnFieldChange()
Returns <I>True</I> if the control is currently limiting the
value on field change.
SetAllowNone(bool)
If called with a value of True, this function will cause the control
@@ -390,7 +409,7 @@ MININT = -maxint-1
from wx.tools.dbg import Logger
from wx.lib.masked import MaskedEditMixin, Field, BaseMaskedTextCtrl
dbg = Logger()
##dbg = Logger()
##dbg(enable=1)
#----------------------------------------------------------------------------
@@ -442,6 +461,7 @@ class NumCtrlAccessorsMixin:
'emptyInvalid',
'validFunc',
'validRequired',
'stopFieldChangeIfInvalid',
)
for param in exposed_basectrl_params:
propname = param[0].upper() + param[1:]
@@ -478,6 +498,7 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
'min': None, # by default, no bounds set
'max': None,
'limited': False, # by default, no limiting even if bounds set
'limitOnFieldChange': False, # by default, don't limit if changing fields, even if bounds set
'allowNone': False, # by default, don't allow empty value
'selectOnEntry': True, # by default, select the value of each field on entry
'foregroundColour': "Black",
@@ -759,6 +780,12 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
maskededit_kwargs['validRequired'] = False
self._limited = kwargs['limited']
if kwargs.has_key('limitOnFieldChange'):
if kwargs['limitOnFieldChange'] and not self._limitOnFieldChange:
maskededit_kwargs['stopFieldChangeIfInvalid'] = True
elif kwargs['limitOnFieldChange'] and self._limitOnFieldChange:
maskededit_kwargs['stopFieldChangeIfInvalid'] = False
## dbg('maskededit_kwargs:', maskededit_kwargs)
if maskededit_kwargs.keys():
self.SetCtrlParameters(**maskededit_kwargs)
@@ -923,6 +950,43 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position
wx.CallAfter(self.SetSelection, sel_start, sel_to)
def _OnChangeField(self, event):
"""
This routine enhances the base masked control _OnFieldChange(). It's job
is to ensure limits are imposed if limitOnFieldChange is enabled.
"""
## dbg('NumCtrl::_OnFieldChange', indent=1)
if self._limitOnFieldChange and not (self._min <= self.GetValue() <= self._max):
self._disallowValue()
## dbg('oob - field change disallowed',indent=0)
return False
else:
## dbg(indent=0)
return MaskedEditMixin._OnChangeField(self, event) # call the baseclass function
def _LostFocus(self):
"""
On loss of focus, if limitOnFieldChange is set, ensure value conforms to limits.
"""
## dbg('NumCtrl::_LostFocus', indent=1)
if self._limitOnFieldChange:
## dbg("limiting on loss of focus")
value = self.GetValue()
if self._min is not None and value < self._min:
## dbg('Set to min value:', self._min)
self._SetValue(self._toGUI(self._min))
elif self._max is not None and value > self._max:
## dbg('Setting to max value:', self._max)
self._SetValue(self._toGUI(self._max))
# (else do nothing.)
# (else do nothing.)
## dbg(indent=0)
return True
def _SetValue(self, value):
"""
This routine supersedes the base masked control _SetValue(). It is
@@ -1346,7 +1410,32 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
def GetLimited(self):
""" (For regularization of property accessors) """
return self.IsLimited
return self.IsLimited()
def SetLimitOnFieldChange(self, limit):
"""
If called with a value of True, this function will cause the control
to prevent navigation out of the current field if its value is out-of-bounds,
and limit the value to fall within the bounds currently specified if the
control loses focus.
If called with a value of False, this function will disable value
limiting, but coloring of out-of-bounds values will still take
place if bounds have been set for the control.
"""
self.SetParameters(limitOnFieldChange = limit)
def IsLimitedOnFieldChange(self):
"""
Returns True if the control is currently limiting the
value to fall within the current bounds.
"""
return self._limitOnFieldChange
def GetLimitOnFieldChange(self):
""" (For regularization of property accessors) """
return self.IsLimitedOnFieldChange()
def IsInBounds(self, value=None):
@@ -1794,6 +1883,13 @@ __i=0
## 1. Add support for printf-style format specification.
## 2. Add option for repositioning on 'illegal' insertion point.
##
## Version 1.4
## 1. In response to user request, added limitOnFieldChange feature, so that
## out-of-bounds values can be temporarily added to the control, but should
## navigation be attempted out of an invalid field, it will not navigate,
## and if focus is lost on a control so limited with an invalid value, it
## will change the value to the nearest bound.
##
## Version 1.3
## 1. fixed to allow space for a group char.
##

View File

@@ -157,13 +157,25 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
Allow mixin to set the raw value of the control with this function.
REQUIRED by any class derived from MaskedEditMixin.
"""
## dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1)
## dbg('MaskedTextCtrl::_SetValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1)
# Record current selection and insertion point, for undo
self._prevSelection = self._GetSelection()
self._prevInsertionPoint = self._GetInsertionPoint()
wx.TextCtrl.SetValue(self, value)
## dbg(indent=0)
def _ChangeValue(self, value):
"""
Allow mixin to set the raw value of the control with this function without
generating an event as a result. (New for masked.TextCtrl as of 2.8.4)
"""
## dbg('MaskedTextCtrl::_ChangeValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1)
# Record current selection and insertion point, for undo
self._prevSelection = self._GetSelection()
self._prevInsertionPoint = self._GetInsertionPoint()
wx.TextCtrl.ChangeValue(self, value)
## dbg(indent=0)
def SetValue(self, value):
"""
This function redefines the externally accessible .SetValue() to be
@@ -171,10 +183,27 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
masked control. NOTE: this must be done in the class derived
from the base wx control.
"""
## dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1)
self.ModifyValue(value, use_change_value=False)
def ChangeValue(self, value):
"""
Provided to accomodate similar functionality added to base control in wxPython 2.7.1.1.
"""
self.ModifyValue(value, use_change_value=True)
def ModifyValue(self, value, use_change_value=False):
"""
This factored function of common code does the bulk of the work for SetValue
and ChangeValue.
"""
## dbg('MaskedTextCtrl::ModifyValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1)
if not self._mask:
wx.TextCtrl.SetValue(self, value) # revert to base control behavior
if use_change_value:
wx.TextCtrl.ChangeValue(self, value) # revert to base control behavior
else:
wx.TextCtrl.SetValue(self, value) # revert to base control behavior
return
# empty previous contents, replacing entire value:
@@ -198,7 +227,7 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
value = value[1:]
## dbg('padded value = "%s"' % value)
# make SetValue behave the same as if you had typed the value in:
# make Set/ChangeValue behave the same as if you had typed the value in:
try:
value, replace_to = self._Paste(value, raise_on_invalid=True, just_return_value=True)
if self._isFloat:
@@ -220,10 +249,12 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
else:
## dbg('exception thrown', indent=0)
raise
self._SetValue(value) # note: to preserve similar capability, .SetValue()
# does not change IsModified()
#### dbg('queuing insertion after .SetValue', replace_to)
if use_change_value:
self._ChangeValue(value)
else:
self._SetValue(value) # note: to preserve similar capability, .SetValue()
# does not change IsModified()
#### dbg('queuing insertion after ._Set/ChangeValue', replace_to)
# set selection to last char replaced by paste
wx.CallAfter(self._SetInsertionPoint, replace_to)
wx.CallAfter(self._SetSelection, replace_to, replace_to)
@@ -372,6 +403,10 @@ class PreMaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
__i=0
## CHANGELOG:
## ====================
## Version 1.3
## - Added support for ChangeValue() function, similar to that of the base
## control, added in wxPython 2.7.1.1.
##
## Version 1.2
## - Converted docstrings to reST format, added doc for ePyDoc.
## removed debugging override functions.

View File

@@ -1,4 +1,4 @@
'''
"""
treemixin.py
This module provides three mixin classes that can be used with tree
@@ -18,7 +18,7 @@ controls:
all items in the tree to restore it later.
All mixin classes work with wx.TreeCtrl, wx.gizmos.TreeListCtrl,
and wx.lib.customtree.CustomTreeCtrl. They can be used together or
and wx.lib.customtreectrl.CustomTreeCtrl. They can be used together or
separately.
The VirtualTree and DragAndDrop mixins force the wx.TR_HIDE_ROOT style.
@@ -30,15 +30,15 @@ Date: 15 April 2007
ExpansionState is based on code and ideas from Karsten Hilbert.
Andrea Gavana provided help with the CustomTreeCtrl integration.
'''
"""
import wx, wx.lib.customtreectrl
import wx
class TreeAPIHarmonizer(object):
''' This class attempts to hide the differences in API between the
different tree controls that are part of wxPython. '''
""" This class attempts to hide the differences in API between the
different tree controls that are part of wxPython. """
def __callSuper(self, methodName, default, *args, **kwargs):
# If our super class has a method called methodName, call it,
@@ -165,12 +165,12 @@ class TreeAPIHarmonizer(object):
**kwargs)
def HitTest(self, *args, **kwargs):
''' HitTest returns a two-tuple (item, flags) for tree controls
""" HitTest returns a two-tuple (item, flags) for tree controls
without columns and a three-tuple (item, flags, column) for tree
controls with columns. Our caller can indicate this method to
always return a three-tuple no matter what tree control we're mixed
in with by specifying the optional argument 'alwaysReturnColumn'
to be True. '''
to be True. """
alwaysReturnColumn = kwargs.pop('alwaysReturnColumn', False)
hitTestResult = super(TreeAPIHarmonizer, self).HitTest(*args, **kwargs)
if len(hitTestResult) == 2 and alwaysReturnColumn:
@@ -209,11 +209,11 @@ class TreeAPIHarmonizer(object):
class TreeHelper(object):
''' This class provides methods that are not part of the API of any
tree control, but are convenient to have available. '''
""" This class provides methods that are not part of the API of any
tree control, but are convenient to have available. """
def GetItemChildren(self, item=None, recursively=False):
''' Return the children of item as a list. '''
""" Return the children of item as a list. """
if not item:
item = self.GetRootItem()
if not item:
@@ -228,7 +228,7 @@ class TreeHelper(object):
return children
def GetIndexOfItem(self, item):
''' Return the index of item. '''
""" Return the index of item. """
parent = self.GetItemParent(item)
if parent:
parentIndices = self.GetIndexOfItem(parent)
@@ -238,7 +238,7 @@ class TreeHelper(object):
return ()
def GetItemByIndex(self, index):
''' Return the item specified by index. '''
""" Return the item specified by index. """
item = self.GetRootItem()
for i in index:
children = self.GetItemChildren(item)
@@ -247,7 +247,7 @@ class TreeHelper(object):
class VirtualTree(TreeAPIHarmonizer, TreeHelper):
''' This is a mixin class that can be used to allow for virtual tree
""" This is a mixin class that can be used to allow for virtual tree
controls. It can be mixed in with wx.TreeCtrl, wx.gizmos.TreeListCtrl,
wx.lib.customtree.CustomTreeCtrl.
@@ -279,7 +279,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper):
the fourth one. A tuple with two integers, e.g. (3,0), represents a
child of a visible root item, in this case the first child of the
fourth root item.
'''
"""
def __init__(self, *args, **kwargs):
kwargs['style'] = kwargs.get('style', wx.TR_DEFAULT_STYLE) | \
@@ -289,69 +289,69 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper):
self.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self.OnItemCollapsed)
def OnGetChildrenCount(self, index):
''' This function *must* be overloaded in the derived class.
""" This function *must* be overloaded in the derived class.
It should return the number of child items of the item with the
provided index. If index == () it should return the number of
root items. '''
root items. """
raise NotImplementedError
def OnGetItemText(self, index, column=0):
''' This function *must* be overloaded in the derived class. It
""" This function *must* be overloaded in the derived class. It
should return the string containing the text of the specified
item. '''
item. """
raise NotImplementedError
def OnGetItemFont(self, index):
''' This function may be overloaded in the derived class. It
should return the wx.Font to be used for the specified item. '''
""" This function may be overloaded in the derived class. It
should return the wx.Font to be used for the specified item. """
return wx.NullFont
def OnGetItemTextColour(self, index):
''' This function may be overloaded in the derived class. It
""" This function may be overloaded in the derived class. It
should return the wx.Colour to be used as text colour for the
specified item. '''
specified item. """
return wx.NullColour
def OnGetItemBackgroundColour(self, index):
''' This function may be overloaded in the derived class. It
""" This function may be overloaded in the derived class. It
should return the wx.Colour to be used as background colour for
the specified item. '''
the specified item. """
return wx.NullColour
def OnGetItemImage(self, index, which=wx.TreeItemIcon_Normal, column=0):
''' This function may be overloaded in the derived class. It
""" This function may be overloaded in the derived class. It
should return the index of the image to be used. Don't forget
to associate an ImageList with the tree control. '''
to associate an ImageList with the tree control. """
return -1
def OnGetItemType(self, index):
''' This function may be overloaded in the derived class, but
""" This function may be overloaded in the derived class, but
that only makes sense when this class is mixed in with a tree
control that supports checkable items, i.e. CustomTreeCtrl.
This method should return whether the item is to be normal (0,
the default), a checkbox (1) or a radiobutton (2).
Note that OnGetItemChecked needs to be implemented as well; it
should return whether the item is actually checked. '''
should return whether the item is actually checked. """
return 0
def OnGetItemChecked(self, index):
''' This function may be overloaded in the derived class, but
""" This function may be overloaded in the derived class, but
that only makes sense when this class is mixed in with a tree
control that supports checkable items, i.e. CustomTreeCtrl.
This method should return whether the item is to be checked.
Note that OnGetItemType should return 1 (checkbox) or 2
(radiobutton) for this item. '''
(radiobutton) for this item. """
return False
def RefreshItems(self):
''' Redraws all visible items. '''
""" Redraws all visible items. """
rootItem = self.GetRootItem()
if not rootItem:
rootItem = self.AddRoot('Hidden root')
self.RefreshChildrenRecursively(rootItem)
def RefreshItem(self, index):
''' Redraws the item with the specified index. '''
""" Redraws the item with the specified index. """
try:
item = self.GetItemByIndex(index)
except IndexError:
@@ -362,8 +362,8 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper):
self.DoRefreshItem(item, index, hasChildren)
def RefreshChildrenRecursively(self, item, itemIndex=None):
''' Refresh the children of item, reusing as much of the
existing items in the tree as possible. '''
""" Refresh the children of item, reusing as much of the
existing items in the tree as possible. """
if itemIndex is None:
itemIndex = self.GetIndexOfItem(item)
reusableChildren = self.GetItemChildren(item)
@@ -377,7 +377,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper):
self.Delete(child)
def RefreshItemRecursively(self, item, itemIndex):
''' Refresh the item and its children recursively. '''
""" Refresh the item and its children recursively. """
hasChildren = bool(self.OnGetChildrenCount(itemIndex))
item = self.DoRefreshItem(item, itemIndex, hasChildren)
# We need to refresh the children when the item is expanded and
@@ -388,7 +388,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper):
self.SetItemHasChildren(item, hasChildren)
def DoRefreshItem(self, item, index, hasChildren):
''' Refresh one item. '''
""" Refresh one item. """
item = self.RefreshItemType(item, index)
self.RefreshItemText(item, index)
self.RefreshColumns(item, index)
@@ -458,7 +458,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper):
event.Skip()
def __refreshAttribute(self, item, index, attribute, *args):
''' Refresh the specified attribute if necessary. '''
""" Refresh the specified attribute if necessary. """
value = getattr(self, 'OnGet%s'%attribute)(index, *args)
if getattr(self, 'Get%s'%attribute)(item, *args) != value:
return getattr(self, 'Set%s'%attribute)(item, value, *args)
@@ -467,7 +467,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper):
class DragAndDrop(TreeAPIHarmonizer, TreeHelper):
''' This is a mixin class that can be used to easily implement
""" This is a mixin class that can be used to easily implement
dragging and dropping of tree items. It can be mixed in with
wx.TreeCtrl, wx.gizmos.TreeListCtrl, or wx.lib.customtree.CustomTreeCtrl.
@@ -480,7 +480,7 @@ class DragAndDrop(TreeAPIHarmonizer, TreeHelper):
dropped an item on top of another item. It's up to you to decide how
to handle the drop. If you are using this mixin together with the
VirtualTree mixin, it makes sense to rearrange your underlying data
and then call RefreshItems to let the virtual tree refresh itself. '''
and then call RefreshItems to let the virtual tree refresh itself. """
def __init__(self, *args, **kwargs):
kwargs['style'] = kwargs.get('style', wx.TR_DEFAULT_STYLE) | \
@@ -489,11 +489,11 @@ class DragAndDrop(TreeAPIHarmonizer, TreeHelper):
self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnBeginDrag)
def OnDrop(self, dropItem, dragItem):
''' This function must be overloaded in the derived class.
""" This function must be overloaded in the derived class.
dragItem is the item being dragged by the user. dropItem is the
item dragItem is dropped upon. If the user doesn't drop dragItem
on another item, dropItem equals the (hidden) root item of the
tree control. '''
tree control. """
raise NotImplementedError
def OnBeginDrag(self, event):
@@ -567,7 +567,7 @@ class DragAndDrop(TreeAPIHarmonizer, TreeHelper):
class ExpansionState(TreeAPIHarmonizer, TreeHelper):
''' This is a mixin class that can be used to save and restore
""" This is a mixin class that can be used to save and restore
the expansion state (i.e. which items are expanded and which items
are collapsed) of a tree. It can be mixed in with wx.TreeCtrl,
wx.gizmos.TreeListCtrl, or wx.lib.customtree.CustomTreeCtrl.
@@ -590,20 +590,20 @@ class ExpansionState(TreeAPIHarmonizer, TreeHelper):
expansion doesn't depend on the position of items in the tree, but
rather on some more stable characteristic of the underlying domain
object, e.g. a social security number in case of persons or an isbn
number in case of books. '''
number in case of books. """
def GetItemIdentity(self, item):
''' Return a hashable object that represents the identity of the
""" Return a hashable object that represents the identity of the
item. By default this returns the position of the item in the
tree. You may want to override this to return the item label
(if you know that labels are unique and don't change), or return
something that represents the underlying domain object, e.g.
a database key. '''
a database key. """
return self.GetIndexOfItem(item)
def GetExpansionState(self):
''' GetExpansionState() -> list of expanded items. Expanded items
are coded as determined by the result of GetItemIdentity(item). '''
""" GetExpansionState() -> list of expanded items. Expanded items
are coded as determined by the result of GetItemIdentity(item). """
root = self.GetRootItem()
if not root:
return []
@@ -613,9 +613,9 @@ class ExpansionState(TreeAPIHarmonizer, TreeHelper):
return self.GetExpansionStateOfItem(root)
def SetExpansionState(self, listOfExpandedItems):
''' SetExpansionState(listOfExpandedItems). Expands all tree items
""" SetExpansionState(listOfExpandedItems). Expands all tree items
whose identity, as determined by GetItemIdentity(item), is present
in the list and collapses all other tree items. '''
in the list and collapses all other tree items. """
root = self.GetRootItem()
if not root:
return