diff --git a/wxPython/demo/Main.py b/wxPython/demo/Main.py index c951870a3b..152a858a44 100644 --- a/wxPython/demo/Main.py +++ b/wxPython/demo/Main.py @@ -50,6 +50,7 @@ _treeList = [ 'FoldPanelBar', 'GIFAnimationCtrl', 'HyperLinkCtrl', + 'MultiSplitterWindow', ]), # managed windows == things with a (optional) caption you can close @@ -160,6 +161,7 @@ _treeList = [ 'HyperLinkCtrl', 'IntCtrl', 'MediaCtrl', + 'MultiSplitterWindow', 'MVCTree', 'MaskedEditControls', 'MaskedNumCtrl', diff --git a/wxPython/demo/MultiSplitterWindow.py b/wxPython/demo/MultiSplitterWindow.py new file mode 100644 index 0000000000..e8a3f158bb --- /dev/null +++ b/wxPython/demo/MultiSplitterWindow.py @@ -0,0 +1,167 @@ + +import wx +from wx.lib.splitter import MultiSplitterWindow + +#---------------------------------------------------------------------- + +class SamplePane(wx.Panel): + """ + Just a simple test window to put into the splitter. + """ + def __init__(self, parent, colour, label): + wx.Panel.__init__(self, parent, style=wx.BORDER_SUNKEN) + self.SetBackgroundColour(colour) + wx.StaticText(self, -1, label, (5,5)) + + def SetOtherLabel(self, label): + wx.StaticText(self, -1, label, (5, 30)) + + + +class ControlPane(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + hvBox = wx.RadioBox(self, -1, "Orientation", + choices=["Horizontal", "Vertical"], + style=wx.RA_SPECIFY_COLS, + majorDimension=1) + hvBox.SetSelection(0) + self.Bind(wx.EVT_RADIOBOX, self.OnSetHV, hvBox) + + luCheck = wx.CheckBox(self, -1, "Live Update") + luCheck.SetValue(True) + self.Bind(wx.EVT_CHECKBOX, self.OnSetLiveUpdate, luCheck) + + btn = wx.Button(self, -1, "Swap 2 && 4") + self.Bind(wx.EVT_BUTTON, self.OnSwapButton, btn) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(hvBox) + sizer.Add(luCheck, 0, wx.TOP, 5) + sizer.Add(btn, 0, wx.TOP, 5) + border = wx.BoxSizer() + border.Add(sizer, 1, wx.EXPAND|wx.ALL, 5) + self.SetSizer(border) + + + def OnSetHV(self, evt): + rb = evt.GetEventObject() + self.GetParent().SetOrientation(rb.GetSelection()) + + + def OnSetLiveUpdate(self, evt): + check = evt.GetEventObject() + self.GetParent().SetLiveUpdate(check.GetValue()) + + + def OnSwapButton(self, evt): + self.GetParent().Swap2and4() + + + +class TestPanel(wx.Panel): + def __init__(self, parent, log): + self.log = log + wx.Panel.__init__(self, parent, -1) + + cp = ControlPane(self) + + splitter = MultiSplitterWindow(self, style=wx.SP_LIVE_UPDATE) + self.splitter = splitter + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(cp) + sizer.Add(splitter, 1, wx.EXPAND) + self.SetSizer(sizer) + + + p1 = SamplePane(splitter, "pink", "Panel One") + p1.SetOtherLabel( + "There are two sash\n" + "drag modes. Try\n" + "dragging with and\n" + "without the Shift\n" + "key held down." + ) + splitter.AppendWindow(p1, 140) + + p2 = SamplePane(splitter, "sky blue", "Panel Two") + p2.SetOtherLabel("This window\nhas a\nminsize.") + p2.SetMinSize(p2.GetBestSize()) + splitter.AppendWindow(p2, 150) + + p3 = SamplePane(splitter, "yellow", "Panel Three") + splitter.AppendWindow(p3, 125) + + p4 = SamplePane(splitter, "Lime Green", "Panel Four") + splitter.AppendWindow(p4) + + self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.OnChanged) + self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGING, self.OnChanging) + + + def OnChanging(self, evt): + self.log.write( "Changing sash:%d %s\n" % + (evt.GetSashIdx(), evt.GetSashPosition())) + # This is one way to control the sash limits + #if evt.GetSashPosition() < 50: + # evt.Veto() + + # Or you can reset the sash position to whatever you want + #if evt.GetSashPosition() < 5: + # evt.SetSashPosition(25) + + + def OnChanged(self, evt): + self.log.write( "Changed sash:%d %s\n" % + (evt.GetSashIdx(), evt.GetSashPosition())) + + + def SetOrientation(self, value): + if value: + self.splitter.SetOrientation(wx.VERTICAL) + else: + self.splitter.SetOrientation(wx.HORIZONTAL) + self.splitter.SizeWindows() + + + def SetLiveUpdate(self, enable): + if enable: + self.splitter.SetWindowStyle(wx.SP_LIVE_UPDATE) + else: + self.splitter.SetWindowStyle(0) + + + def Swap2and4(self): + win2 = self.splitter.GetWindow(1) + win4 = self.splitter.GetWindow(3) + self.splitter.ExchangeWindows(win2, win4) + +#---------------------------------------------------------------------- + +def runTest(frame, nb, log): + win = TestPanel(nb, log) + return win + +#---------------------------------------------------------------------- + + + +overview = """ +

MultiSplitterWindow

+ +This class is very similar to wx.SplitterWindow except that it +allows for more than two windows and more than one sash. Many of +the same styles, constants, and methods behave the same as in +wx.SplitterWindow. + + +""" + + + +if __name__ == '__main__': + import sys,os + import run + run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:]) + diff --git a/wxPython/docs/CHANGES.txt b/wxPython/docs/CHANGES.txt index 96c295af80..3a9e0789ce 100644 --- a/wxPython/docs/CHANGES.txt +++ b/wxPython/docs/CHANGES.txt @@ -13,6 +13,17 @@ platforms. Added wx.RendererNative class. +wxMSW: Always set flat toolbar style, even under XP with themes: this +is necessary or separators aren't shown at all. + +Fixes for bug #1217872, pydocview.DocService not correctly initialized + +Fix for bug #1217874, Error in parameter name in DocManager.CreateView + +Added the wx.lib.splitter module, which contains the +MultiSplitterWindow class. This class is much like the standard +wx.SplitterWindow class, except it allows more than one split, so it +can manage more than two child windows. diff --git a/wxPython/wx/lib/splitter.py b/wxPython/wx/lib/splitter.py new file mode 100644 index 0000000000..2930cbab21 --- /dev/null +++ b/wxPython/wx/lib/splitter.py @@ -0,0 +1,827 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.splitter +# Purpose: A class similar to wx.SplitterWindow but that allows more +# a single split +# +# Author: Robin Dunn +# +# Created: 9-June-2005 +# RCS-ID: $Id$ +# Copyright: (c) 2005 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +""" +This module provides the `MultiSplitterWindow` class, which is very +similar to the standard `wx.SplitterWindow` except it can be split +more than once. +""" + +import wx +import sys + +_RENDER_VER = (2,6,1,1) + +#---------------------------------------------------------------------- + +class MultiSplitterWindow(wx.PyPanel): + """ + This class is very similar to `wx.SplitterWindow` except that it + allows for more than two windows and more than one sash. Many of + the same styles, constants, and methods behave the same as in + wx.SplitterWindow. The key differences are seen in the methods + that deal with the child windows manage by the splitter, and also + those that deal with the sash positions. In most cases you will + need to pass an index value to tell the class which window or sash + you are refering to. + + The concept of the sash position is also different than in + wx.SplitterWindow. Since the wx.Splitterwindow has only one sash + you can think of it's position as either relative to the whole + splitter window, or as relative to the first window pane managed + by the splitter. Once there are more than one sash then the + distinciton between the two concepts needs to be clairified. I've + chosen to use the second definition, and sash positions are the + distance (either horizontally or vertically) from the origin of + the window just before the sash in the splitter stack. + + NOTE: These things are not yet supported: + + * Using negative sash positions to indicate a position offset + from the end. + + * User controlled unsplitting (with double clicks on the sash + or dragging a sash until the pane size is zero.) + + * Sash gravity + + """ + def __init__(self, parent, id=-1, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, name="multiSplitter"): + + # always turn on tab traversal + style |= wx.TAB_TRAVERSAL + + # and turn off any border styles + style &= ~wx.BORDER_MASK + style |= wx.BORDER_NONE + + # initialize the base class + wx.PyPanel.__init__(self, parent, id, pos, size, style, name) + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + + # initialize data members + self._windows = [] + self._sashes = [] + self._pending = {} + self._permitUnsplitAlways = self.HasFlag(wx.SP_PERMIT_UNSPLIT) + self._orient = wx.HORIZONTAL + self._dragMode = wx.SPLIT_DRAG_NONE + self._activeSash = -1 + self._oldX = 0 + self._oldY = 0 + self._checkRequestedSashPosition = False + self._minimumPaneSize = 0 + self._sashCursorWE = wx.StockCursor(wx.CURSOR_SIZEWE) + self._sashCursorNS = wx.StockCursor(wx.CURSOR_SIZENS) + self._sashTrackerPen = wx.Pen(wx.BLACK, 2, wx.SOLID) + self._needUpdating = False + self._isHot = False + + # Bind event handlers + self.Bind(wx.EVT_PAINT, self._OnPaint) + self.Bind(wx.EVT_IDLE, self._OnIdle) + self.Bind(wx.EVT_SIZE, self._OnSize) + self.Bind(wx.EVT_MOUSE_EVENTS, self._OnMouse) + + + + def SetOrientation(self, orient): + """ + Set whether the windows managed by the splitter will be + stacked vertically or horizontally. The default is + horizontal. + """ + assert orient in [ wx.VERTICAL, wx.HORIZONTAL ] + self._orient = orient + + def GetOrientation(self): + """ + Returns the current orientation of the splitter, either + wx.VERTICAL or wx.HORIZONTAL. + """ + return self._orient + + + def SetMinimumPaneSize(self, minSize): + """ + Set the smallest size that any pane will be allowed to be + resized to. + """ + self._minimumPaneSize = minSize + + def GetMinimumPaneSize(self): + """ + Returns the smallest allowed size for a window pane. + """ + return self._minimumPaneSize + + + + def AppendWindow(self, window, sashPos=-1): + """ + Add a new window to the splitter. If sashPos is given then it is the + """ + self.InsertWindow(sys.maxint, window, sashPos) + + + def InsertWindow(self, idx, window, sashPos=-1): + """ + """ + assert window not in self._windows, "A window can only be in the splitter once!" + self._windows.insert(idx, window) + self._sashes.insert(idx, -1) + if not window.IsShown(): + window.Show() + if sashPos != -1: + self._pending[window] = sashPos + self._checkRequestedSashPosition = False + self._SizeWindows() + + + def DetachWindow(self, window): + """ + Removes the window from the stack of windows managed by the + splitter. The window will still exist so you should `Hide` or + `Destroy` it as needed. + """ + assert window in self._windows, "Unknown window!" + idx = self._windows.index(window) + del self._windows[idx] + del self._sashes[idx] + self._SizeWindows() + + + def ReplaceWindow(self, oldWindow, newWindow): + """ + Replaces oldWindow (which is currently being managed by the + splitter) with newWindow. The oldWindow window will still + exist so you should `Hide` or `Destroy` it as needed. + """ + assert oldWindow in self._windows, "Unknown window!" + idx = self._windows.index(oldWindow) + self._windows[idx] = newWindow + if not newWindow.IsShown(): + newWindow.Show() + self._SizeWindows() + + + def ExchangeWindows(self, window1, window2): + """ + Trade the positions in the splitter of the two windows. + """ + assert window1 in self._windows, "Unknown window!" + assert window2 in self._windows, "Unknown window!" + idx1 = self._windows.index(window1) + idx2 = self._windows.index(window2) + self._windows[idx1] = window2 + self._windows[idx2] = window1 + self._SizeWindows() + + + def GetWindow(self, idx): + """ + Returns the idx'th window being managed by the splitter. + """ + assert idx < len(self._windows) + return self._windows[idx] + + + def GetSashPosition(self, idx): + """ + Returns the position of the idx'th sash, measured from the + left/top of the window preceding the sash. + """ + assert idx < len(self._sashes) + return self._sashes[idx] + + + def SizeWindows(self): + """ + Reposition and size the windows managed by the splitter. + Useful when windows have been added/removed or when styles + have been changed. + """ + self._SizeWindows() + + + def DoGetBestSize(self): + """ + Overridden base class virtual. Determines the best size of + the control based on the best sizes of the child windows. + """ + best = wx.Size(0,0) + if not self._windows: + best = wx.Size(10,10) + + sashsize = self._GetSashSize() + if self._orient == wx.HORIZONTAL: + for win in self._windows: + winbest = win.GetAdjustedBestSize() + best.width += max(self._minimumPaneSize, winbest.width) + best.height = max(best.height, winbest.height) + best.width += sashsize * (len(self._windows)-1) + + else: + for win in self._windows: + winbest = win.GetAdjustedBestSize() + best.height += max(self._minimumPaneSize, winbest.height) + best.width = max(best.width, winbest.width) + best.height += sashsize * (len(self._windows)-1) + + border = 2 * self._GetBorderSize() + best.width += border + best.height += border + return best + + # ------------------------------------- + # Event handlers + + def _OnPaint(self, evt): + dc = wx.PaintDC(self) + self._DrawSash(dc) + + + def _OnSize(self, evt): + parent = wx.GetTopLevelParent(self) + if parent.IsIconized(): + evt.Skip() + return + self._SizeWindows() + + + def _OnIdle(self, evt): + evt.Skip() + # if this is the first idle time after a sash position has + # potentially been set, allow _SizeWindows to check for a + # requested size. + if not self._checkRequestedSashPosition: + self._checkRequestedSashPosition = True + self._SizeWindows() + + if self._needUpdating: + self._SizeWindows() + + + + def _OnMouse(self, evt): + if self.HasFlag(wx.SP_NOSASH): + return + + x, y = evt.GetPosition() + isLive = self.HasFlag(wx.SP_LIVE_UPDATE) + adjustNeighbor = evt.ShiftDown() + + # LeftDown: set things up for dragging the sash + if evt.LeftDown() and self._SashHitTest(x, y) != -1: + self._activeSash = self._SashHitTest(x, y) + self._dragMode = wx.SPLIT_DRAG_DRAGGING + + self.CaptureMouse() + self._SetResizeCursor() + + if not isLive: + self._pendingPos = (self._sashes[self._activeSash], + self._sashes[self._activeSash+1]) + self._DrawSashTracker(x, y) + + self._oldX = x + self._oldY = y + return + + # LeftUp: Finsish the drag + elif evt.LeftUp() and self._dragMode == wx.SPLIT_DRAG_DRAGGING: + self._dragMode = wx.SPLIT_DRAG_NONE + self.ReleaseMouse() + self.SetCursor(wx.STANDARD_CURSOR) + + if not isLive: + # erase the old tracker + self._DrawSashTracker(self._oldX, self._oldY) + + diff = self._GetMotionDiff(x, y) + + # determine if we can change the position + if isLive: + oldPos1, oldPos2 = (self._sashes[self._activeSash], + self._sashes[self._activeSash+1]) + else: + oldPos1, oldPos2 = self._pendingPos + newPos1, newPos2 = self._OnSashPositionChanging(self._activeSash, + oldPos1 + diff, + oldPos2 - diff, + adjustNeighbor) + if newPos1 == -1: + # the change was not allowed + return + + # TODO: check for unsplit? + + self._SetSashPositionAndNotify(self._activeSash, newPos1, newPos2, adjustNeighbor) + self._activeSash = -1 + self._pendingPos = (-1, -1) + self._SizeWindows() + + # Entering or Leaving a sash: Change the cursor + elif (evt.Moving() or evt.Leaving() or evt.Entering()) and self._dragMode == wx.SPLIT_DRAG_NONE: + if evt.Leaving() or self._SashHitTest(x, y) == -1: + self._OnLeaveSash() + else: + self._OnEnterSash() + + # Dragging the sash + elif evt.Dragging() and self._dragMode == wx.SPLIT_DRAG_DRAGGING: + diff = self._GetMotionDiff(x, y) + if not diff: + return # mouse didn't move far enough + + # determine if we can change the position + if isLive: + oldPos1, oldPos2 = (self._sashes[self._activeSash], + self._sashes[self._activeSash+1]) + else: + oldPos1, oldPos2 = self._pendingPos + newPos1, newPos2 = self._OnSashPositionChanging(self._activeSash, + oldPos1 + diff, + oldPos2 - diff, + adjustNeighbor) + if newPos1 == -1: + # the change was not allowed + return + + if newPos1 == self._sashes[self._activeSash]: + return # nothing was changed + + if not isLive: + # erase the old tracker + self._DrawSashTracker(self._oldX, self._oldY) + + if self._orient == wx.HORIZONTAL: + x = self._SashToCoord(self._activeSash, newPos1) + else: + y = self._SashToCoord(self._activeSash, newPos1) + + # Remember old positions + self._oldX = x + self._oldY = y + + if not isLive: + # draw a new tracker + self._pendingPos = (newPos1, newPos2) + self._DrawSashTracker(self._oldX, self._oldY) + else: + self._DoSetSashPosition(self._activeSash, newPos1, newPos2, adjustNeighbor) + self._needUpdating = True + + + # ------------------------------------- + # Internal helpers + + def _RedrawIfHotSensitive(self, isHot): + if not wx.VERSION >= _RENDER_VER: + return + if wx.RendererNative.Get().GetSplitterParams(self).isHotSensitive: + self._isHot = isHot + dc = wx.ClientDC(self) + self._DrawSash(dc) + + + def _OnEnterSash(self): + self._SetResizeCursor() + self._RedrawIfHotSensitive(True) + + + def _OnLeaveSash(self): + self.SetCursor(wx.STANDARD_CURSOR) + self._RedrawIfHotSensitive(False) + + + def _SetResizeCursor(self): + if self._orient == wx.HORIZONTAL: + self.SetCursor(self._sashCursorWE) + else: + self.SetCursor(self._sashCursorNS) + + + def _OnSashPositionChanging(self, idx, newPos1, newPos2, adjustNeighbor): + # TODO: check for possibility of unsplit (pane size becomes zero) + + # make sure that minsizes are honored + newPos1, newPos2 = self._AdjustSashPosition(idx, newPos1, newPos2, adjustNeighbor) + + # sanity check + if newPos1 <= 0: + newPos2 += newPos1 + newPos1 = 0 + + # send the events + evt = MultiSplitterEvent( + wx.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGING, self) + evt.SetSashIdx(idx) + evt.SetSashPosition(newPos1) + if not self._DoSendEvent(evt): + # the event handler vetoed the change + newPos1 = -1 + else: + # or it might have changed the value + newPos1 = evt.GetSashPosition() + + if adjustNeighbor and newPos1 != -1: + evt.SetSashIdx(idx+1) + evt.SetSashPosition(newPos2) + if not self._DoSendEvent(evt): + # the event handler vetoed the change + newPos2 = -1 + else: + # or it might have changed the value + newPos2 = evt.GetSashPosition() + if newPos2 == -1: + newPos1 = -1 + + return (newPos1, newPos2) + + + def _AdjustSashPosition(self, idx, newPos1, newPos2=-1, adjustNeighbor=False): + total = newPos1 + newPos2 + + # these are the windows on either side of the sash + win1 = self._windows[idx] + win2 = self._windows[idx+1] + + # make adjustments for window min sizes + minSize = self._GetWindowMin(win1) + if minSize == -1 or self._minimumPaneSize > minSize: + minSize = self._minimumPaneSize + minSize += self._GetBorderSize() + if newPos1 < minSize: + newPos1 = minSize + newPos2 = total - newPos1 + + if adjustNeighbor: + minSize = self._GetWindowMin(win2) + if minSize == -1 or self._minimumPaneSize > minSize: + minSize = self._minimumPaneSize + minSize += self._GetBorderSize() + if newPos2 < minSize: + newPos2 = minSize + newPos1 = total - newPos2 + + return (newPos1, newPos2) + + + def _DoSetSashPosition(self, idx, newPos1, newPos2=-1, adjustNeighbor=False): + newPos1, newPos2 = self._AdjustSashPosition(idx, newPos1, newPos2, adjustNeighbor) + if newPos1 == self._sashes[idx]: + return False + self._sashes[idx] = newPos1 + if adjustNeighbor: + self._sashes[idx+1] = newPos2 + return True + + + def _SetSashPositionAndNotify(self, idx, newPos1, newPos2=-1, adjustNeighbor=False): + # TODO: what is the thing about _requestedSashPosition for? + + self._DoSetSashPosition(idx, newPos1, newPos2, adjustNeighbor) + + evt = MultiSplitterEvent( + wx.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGED, self) + evt.SetSashIdx(idx) + evt.SetSashPosition(newPos1) + self._DoSendEvent(evt) + + if adjustNeighbor: + evt.SetSashIdx(idx+1) + evt.SetSashPosition(newPos2) + self._DoSendEvent(evt) + + + def _GetMotionDiff(self, x, y): + # find the diff from the old pos + if self._orient == wx.HORIZONTAL: + diff = x - self._oldX + else: + diff = y - self._oldY + return diff + + + def _SashToCoord(self, idx, sashPos): + coord = 0 + for i in range(idx): + coord += self._sashes[i] + coord += self._GetSashSize() + coord += sashPos + return coord + + + def _GetWindowMin(self, window): + if self._orient == wx.HORIZONTAL: + return window.GetMinWidth() + else: + return window.GetMinHeight() + + + def _GetSashSize(self): + if self.HasFlag(wx.SP_NOSASH): + return 0 + if wx.VERSION >= _RENDER_VER: + return wx.RendererNative.Get().GetSplitterParams(self).widthSash + else: + return 5 + + + def _GetBorderSize(self): + if wx.VERSION >= _RENDER_VER: + return wx.RendererNative.Get().GetSplitterParams(self).border + else: + return 0 + + + def _DrawSash(self, dc): + if wx.VERSION >= _RENDER_VER: + if self.HasFlag(wx.SP_3DBORDER): + wx.RendererNative.Get().DrawSplitterBorder( + self, dc, self.GetClientRect()) + + # if there are no splits then we're done. + if len(self._windows) < 2: + return + + # if we are not supposed to use a sash then we're done. + if self.HasFlag(wx.SP_NOSASH): + return + + # Reverse the sense of the orientation, in this case it refers + # to the direction to draw the sash not the direction that + # windows are stacked. + orient = { wx.HORIZONTAL : wx.VERTICAL, + wx.VERTICAL : wx.HORIZONTAL }[self._orient] + + flag = 0 + if self._isHot: + flag = wx.CONTROL_CURRENT + + pos = 0 + for sash in self._sashes[:-1]: + pos += sash + if wx.VERSION >= _RENDER_VER: + wx.RendererNative.Get().DrawSplitterSash(self, dc, + self.GetClientSize(), + pos, orient, flag) + else: + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetBrush(wx.Brush(self.GetBackgroundColour())) + sashsize = self._GetSashSize() + if orient == wx.VERTICAL: + x = pos + y = 0 + w = sashsize + h = self.GetClientSize().height + else: + x = 0 + y = pos + w = self.GetClientSize().width + h = sashsize + dc.DrawRectangle(x, y, w, h) + + pos += self._GetSashSize() + + + def _DrawSashTracker(self, x, y): + # Draw a line to represent the dragging sash, for when not + # doing live updates + w, h = self.GetClientSize() + dc = wx.ScreenDC() + + if self._orient == wx.HORIZONTAL: + x1 = x + y1 = 2 + x2 = x + y2 = h-2 + if x1 > w: + x1 = w + x2 = w + elif x1 < 0: + x1 = 0 + x2 = 0 + else: + x1 = 2 + y1 = y + x2 = w-2 + y2 = y + if y1 > h: + y1 = h + y2 = h + elif y1 < 0: + y1 = 0 + y2 = 0 + + x1, y1 = self.ClientToScreenXY(x1, y1) + x2, y2 = self.ClientToScreenXY(x2, y2) + + dc.SetLogicalFunction(wx.INVERT) + dc.SetPen(self._sashTrackerPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawLine(x1, y1, x2, y2) + dc.SetLogicalFunction(wx.COPY) + + + def _SashHitTest(self, x, y, tolerance=5): + # if there are no splits then we're done. + if len(self._windows) < 2: + return -1 + + if self._orient == wx.HORIZONTAL: + z = x + else: + z = y + + pos = 0 + for idx, sash in enumerate(self._sashes[:-1]): + pos += sash + hitMin = pos - tolerance + hitMax = pos + self._GetSashSize() + tolerance + + if z >= hitMin and z <= hitMax: + return idx + + pos += self._GetSashSize() + + return -1 + + + def _SizeWindows(self): + # no windows yet? + if not self._windows: + return + + # are there any pending size settings? + for window, spos in self._pending.items(): + idx = self._windows.index(window) + # TODO: this may need adjusted to make sure they all fit + # in the current client size + self._sashes[idx] = spos + del self._pending[window] + + # are there any that still have a -1? + for idx, spos in enumerate(self._sashes[:-1]): + if spos == -1: + # TODO: this should also be adjusted + self._sashes[idx] = 100 + + cw, ch = self.GetClientSize() + border = self._GetBorderSize() + sash = self._GetSashSize() + + if len(self._windows) == 1: + # there's only one, it's an easy layout + self._windows[0].SetDimensions(border, border, + cw - 2*border, ch - 2*border) + else: + for win in self._windows: + win.Freeze() + if self._orient == wx.HORIZONTAL: + x = y = border + h = ch - 2*border + for idx, spos in enumerate(self._sashes[:-1]): + self._windows[idx].SetDimensions(x, y, spos, h) + x += spos + sash + # last one takes the rest of the space. TODO make this configurable + last = cw - 2*border - x + self._windows[idx+1].SetDimensions(x, y, last, h) + if last > 0: + self._sashes[idx+1] = last + else: + x = y = border + w = cw - 2*border + for idx, spos in enumerate(self._sashes[:-1]): + self._windows[idx].SetDimensions(x, y, w, spos) + y += spos + sash + # last one takes the rest of the space. TODO make this configurable + last = ch - 2*border - y + self._windows[idx+1].SetDimensions(x, y, w, last) + if last > 0: + self._sashes[idx+1] = last + for win in self._windows: + win.Thaw() + + self._DrawSash(wx.ClientDC(self)) + self._needUpdating = False + + + def _DoSendEvent(self, evt): + return not self.GetEventHandler().ProcessEvent(evt) or evt.IsAllowed() + +#---------------------------------------------------------------------- + +class MultiSplitterEvent(wx.PyCommandEvent): + """ + This event class is almost the same as `wx.SplitterEvent` except + it adds an accessor for the sash index that is being changed. The + same event type IDs and event binders are used as with + `wx.SplitterEvent`. + """ + def __init__(self, type=wx.wxEVT_NULL, splitter=None): + wx.PyCommandEvent.__init__(self, type) + if splitter: + self.SetEventObject(splitter) + self.SetId(splitter.GetId()) + self.sashIdx = -1 + self.sashPos = -1 + self.isAllowed = True + + def SetSashIdx(self, idx): + self.sashIdx = idx + + def SetSashPosition(self, pos): + self.sashPos = pos + + def GetSashIdx(self): + return self.sashIdx + + def GetSashPosition(self): + return self.sashPos + + # methods from wx.NotifyEvent + def Veto(self): + self.isAllowed = False + def Allow(self): + self.isAllowed = True + def IsAllowed(self): + return self.isAllowed + + + +#---------------------------------------------------------------------- + + + + +if __name__ == "__main__": + + def OnChanged(evt): + print "Changed:", evt.GetSashIdx(), evt.GetSashPosition() + def OnChanging(evt): + print "Changing:", evt.GetSashIdx(), evt.GetSashPosition() + + + app = wx.App(0) + frm = wx.Frame(None, title="tester", size=(640,480)) + + #frm.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, OnChanged) + #frm.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGING, OnChanging) + + #sty = 0 + #sty = wx.SP_3DBORDER + #sty = wx.SP_NOSASH + sty = wx.SP_LIVE_UPDATE + + splitter = MultiSplitterWindow(frm, style=sty) + #splitter.SetOrientation(wx.VERTICAL) + splitter.SetMinimumPaneSize(25) + + #sty = wx.BORDER_NONE + #sty = wx.BORDER_SIMPLE + sty = wx.BORDER_SUNKEN + + p1 = wx.Window(splitter, style=sty) + p1.SetBackgroundColour("pink") + wx.StaticText(p1, -1, "Panel One", (5,5)) + + p2 = wx.Window(splitter, style=sty) + p2.SetBackgroundColour("sky blue") + wx.StaticText(p2, -1, "Panel Two", (5,5)) + p2.SetMinSize((50,50)) + + p3 = wx.Window(splitter, style=sty) + p3.SetBackgroundColour("yellow") + wx.StaticText(p3, -1, "Panel Three", (5,5)) + + splitter.AppendWindow(p1, 100) + splitter.AppendWindow(p2, 200) + splitter.AppendWindow(p3) + + for x in range(3): + p = wx.Window(splitter, style=sty) + p.SetBackgroundColour("white") + wx.StaticText(p, -1, str(4+x), (5,5)) + splitter.AppendWindow(p, 50) + + #sizer = wx.BoxSizer() + #sizer.Add(splitter, 1, wx.EXPAND) + #frm.SetSizerAndFit(sizer) + + frm.Show() + app.MainLoop() + +