1 #---------------------------------------------------------------------- 
   2 # Name:        wx.lib.splitter 
   3 # Purpose:     A class similar to wx.SplitterWindow but that allows more 
  10 # Copyright:   (c) 2005 by Total Control Software 
  11 # Licence:     wxWindows license 
  12 #---------------------------------------------------------------------- 
  14 This module provides the `MultiSplitterWindow` class, which is very 
  15 similar to the standard `wx.SplitterWindow` except it can be split 
  22 _RENDER_VER 
= (2,6,1,1) 
  24 #---------------------------------------------------------------------- 
  26 class MultiSplitterWindow(wx
.PyPanel
): 
  28     This class is very similar to `wx.SplitterWindow` except that it 
  29     allows for more than two windows and more than one sash.  Many of 
  30     the same styles, constants, and methods behave the same as in 
  31     wx.SplitterWindow.  The key differences are seen in the methods 
  32     that deal with the child windows managed by the splitter, and also 
  33     those that deal with the sash positions.  In most cases you will 
  34     need to pass an index value to tell the class which window or sash 
  37     The concept of the sash position is also different than in 
  38     wx.SplitterWindow.  Since the wx.Splitterwindow has only one sash 
  39     you can think of it's position as either relative to the whole 
  40     splitter window, or as relative to the first window pane managed 
  41     by the splitter.  Once there is more than one sash then the 
  42     distinciton between the two concepts needs to be clairified.  I've 
  43     chosen to use the second definition, and sash positions are the 
  44     distance (either horizontally or vertically) from the origin of 
  45     the window just before the sash in the splitter stack. 
  47     NOTE: These things are not yet supported: 
  49         * Using negative sash positions to indicate a position offset 
  52         * User controlled unsplitting (with double clicks on the sash 
  53           or dragging a sash until the pane size is zero.) 
  58     def __init__(self
, parent
, id=-1, 
  59                  pos 
= wx
.DefaultPosition
, size 
= wx
.DefaultSize
, 
  60                  style 
= 0, name
="multiSplitter"): 
  62         # always turn on tab traversal 
  63         style |
= wx
.TAB_TRAVERSAL
 
  65         # and turn off any border styles 
  66         style 
&= ~wx
.BORDER_MASK
 
  67         style |
= wx
.BORDER_NONE
 
  69         # initialize the base class 
  70         wx
.PyPanel
.__init
__(self
, parent
, id, pos
, size
, style
, name
) 
  71         self
.SetBackgroundStyle(wx
.BG_STYLE_CUSTOM
) 
  73         # initialize data members 
  77         self
._permitUnsplitAlways 
= self
.HasFlag(wx
.SP_PERMIT_UNSPLIT
) 
  78         self
._orient 
= wx
.HORIZONTAL
 
  79         self
._dragMode 
= wx
.SPLIT_DRAG_NONE
 
  83         self
._checkRequestedSashPosition 
= False 
  84         self
._minimumPaneSize 
= 0 
  85         self
._sashCursorWE 
= wx
.StockCursor(wx
.CURSOR_SIZEWE
) 
  86         self
._sashCursorNS 
= wx
.StockCursor(wx
.CURSOR_SIZENS
) 
  87         self
._sashTrackerPen 
= wx
.Pen(wx
.BLACK
, 2, wx
.SOLID
) 
  88         self
._needUpdating 
= False 
  92         self
.Bind(wx
.EVT_PAINT
,        self
._OnPaint
) 
  93         self
.Bind(wx
.EVT_IDLE
,         self
._OnIdle
) 
  94         self
.Bind(wx
.EVT_SIZE
,         self
._OnSize
) 
  95         self
.Bind(wx
.EVT_MOUSE_EVENTS
, self
._OnMouse
) 
  99     def SetOrientation(self
, orient
): 
 101         Set whether the windows managed by the splitter will be 
 102         stacked vertically or horizontally.  The default is 
 105         assert orient 
in [ wx
.VERTICAL
, wx
.HORIZONTAL 
] 
 106         self
._orient 
= orient
 
 108     def GetOrientation(self
): 
 110         Returns the current orientation of the splitter, either 
 111         wx.VERTICAL or wx.HORIZONTAL. 
 116     def SetMinimumPaneSize(self
, minSize
): 
 118         Set the smallest size that any pane will be allowed to be 
 121         self
._minimumPaneSize 
= minSize
 
 123     def GetMinimumPaneSize(self
): 
 125         Returns the smallest allowed size for a window pane. 
 127         return self
._minimumPaneSize
 
 131     def AppendWindow(self
, window
, sashPos
=-1): 
 133         Add a new window to the splitter at the right side or bottom 
 134         of the window stack.  If sashPos is given then it is used to 
 137         self
.InsertWindow(sys
.maxint
, window
, sashPos
) 
 140     def InsertWindow(self
, idx
, window
, sashPos
=-1): 
 142         Insert a new window into the splitter at the position given in 
 145         assert window 
not in self
._windows
, "A window can only be in the splitter once!" 
 146         self
._windows
.insert(idx
, window
) 
 147         self
._sashes
.insert(idx
, -1) 
 148         if not window
.IsShown(): 
 151             self
._pending
[window
] = sashPos
 
 152         self
._checkRequestedSashPosition 
= False 
 156     def DetachWindow(self
, window
): 
 158         Removes the window from the stack of windows managed by the 
 159         splitter.  The window will still exist so you should `Hide` or 
 160         `Destroy` it as needed. 
 162         assert window 
in self
._windows
, "Unknown window!" 
 163         idx 
= self
._windows
.index(window
) 
 164         del self
._windows
[idx
] 
 165         del self
._sashes
[idx
] 
 169     def ReplaceWindow(self
, oldWindow
, newWindow
): 
 171         Replaces oldWindow (which is currently being managed by the 
 172         splitter) with newWindow.  The oldWindow window will still 
 173         exist so you should `Hide` or `Destroy` it as needed. 
 175         assert oldWindow 
in self
._windows
, "Unknown window!" 
 176         idx 
= self
._windows
.index(oldWindow
) 
 177         self
._windows
[idx
] = newWindow
 
 178         if not newWindow
.IsShown(): 
 183     def ExchangeWindows(self
, window1
, window2
): 
 185         Trade the positions in the splitter of the two windows. 
 187         assert window1 
in self
._windows
, "Unknown window!" 
 188         assert window2 
in self
._windows
, "Unknown window!" 
 189         idx1 
= self
._windows
.index(window1
) 
 190         idx2 
= self
._windows
.index(window2
) 
 191         self
._windows
[idx1
] = window2
 
 192         self
._windows
[idx2
] = window1
 
 196     def GetWindow(self
, idx
): 
 198         Returns the idx'th window being managed by the splitter. 
 200         assert idx 
< len(self
._windows
) 
 201         return self
._windows
[idx
] 
 204     def GetSashPosition(self
, idx
): 
 206         Returns the position of the idx'th sash, measured from the 
 207         left/top of the window preceding the sash. 
 209         assert idx 
< len(self
._sashes
) 
 210         return self
._sashes
[idx
] 
 213     def SizeWindows(self
): 
 215         Reposition and size the windows managed by the splitter. 
 216         Useful when windows have been added/removed or when styles 
 222     def DoGetBestSize(self
): 
 224         Overridden base class virtual.  Determines the best size of 
 225         the control based on the best sizes of the child windows. 
 228         if not self
._windows
: 
 229             best 
= wx
.Size(10,10) 
 231         sashsize 
= self
._GetSashSize
() 
 232         if self
._orient 
== wx
.HORIZONTAL
: 
 233             for win 
in self
._windows
: 
 234                 winbest 
= win
.GetAdjustedBestSize() 
 235                 best
.width 
+= max(self
._minimumPaneSize
, winbest
.width
) 
 236                 best
.height 
= max(best
.height
, winbest
.height
) 
 237             best
.width 
+= sashsize 
* (len(self
._windows
)-1) 
 240             for win 
in self
._windows
: 
 241                 winbest 
= win
.GetAdjustedBestSize() 
 242                 best
.height 
+= max(self
._minimumPaneSize
, winbest
.height
) 
 243                 best
.width 
= max(best
.width
, winbest
.width
) 
 244             best
.height 
+= sashsize 
* (len(self
._windows
)-1) 
 246         border 
= 2 * self
._GetBorderSize
() 
 248         best
.height 
+= border
 
 251     # ------------------------------------- 
 254     def _OnPaint(self
, evt
): 
 255         dc 
= wx
.PaintDC(self
) 
 259     def _OnSize(self
, evt
): 
 260         parent 
= wx
.GetTopLevelParent(self
) 
 261         if parent
.IsIconized(): 
 267     def _OnIdle(self
, evt
): 
 269         # if this is the first idle time after a sash position has 
 270         # potentially been set, allow _SizeWindows to check for a 
 272         if not self
._checkRequestedSashPosition
: 
 273             self
._checkRequestedSashPosition 
= True 
 276         if self
._needUpdating
: 
 281     def _OnMouse(self
, evt
): 
 282         if self
.HasFlag(wx
.SP_NOSASH
): 
 285         x
, y 
= evt
.GetPosition() 
 286         isLive 
= self
.HasFlag(wx
.SP_LIVE_UPDATE
) 
 287         adjustNeighbor 
= evt
.ShiftDown() 
 289         # LeftDown: set things up for dragging the sash 
 290         if evt
.LeftDown() and self
._SashHitTest
(x
, y
) != -1: 
 291             self
._activeSash 
= self
._SashHitTest
(x
, y
) 
 292             self
._dragMode 
= wx
.SPLIT_DRAG_DRAGGING
 
 295             self
._SetResizeCursor
() 
 298                 self
._pendingPos 
= (self
._sashes
[self
._activeSash
], 
 299                                     self
._sashes
[self
._activeSash
+1]) 
 300                 self
._DrawSashTracker
(x
, y
) 
 306         # LeftUp: Finsish the drag 
 307         elif evt
.LeftUp() and self
._dragMode 
== wx
.SPLIT_DRAG_DRAGGING
: 
 308             self
._dragMode 
= wx
.SPLIT_DRAG_NONE
 
 310             self
.SetCursor(wx
.STANDARD_CURSOR
) 
 313                 # erase the old tracker 
 314                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 316             diff 
= self
._GetMotionDiff
(x
, y
) 
 318             # determine if we can change the position 
 320                 oldPos1
, oldPos2 
= (self
._sashes
[self
._activeSash
], 
 321                                     self
._sashes
[self
._activeSash
+1]) 
 323                 oldPos1
, oldPos2 
= self
._pendingPos
 
 324             newPos1
, newPos2 
= self
._OnSashPositionChanging
(self
._activeSash
, 
 329                 # the change was not allowed 
 332             # TODO: check for unsplit? 
 334             self
._SetSashPositionAndNotify
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
) 
 335             self
._activeSash 
= -1 
 336             self
._pendingPos 
= (-1, -1) 
 339         # Entering or Leaving a sash: Change the cursor 
 340         elif (evt
.Moving() or evt
.Leaving() or evt
.Entering()) and self
._dragMode 
== wx
.SPLIT_DRAG_NONE
: 
 341             if evt
.Leaving() or self
._SashHitTest
(x
, y
) == -1: 
 347         elif evt
.Dragging() and self
._dragMode 
== wx
.SPLIT_DRAG_DRAGGING
: 
 348             diff 
= self
._GetMotionDiff
(x
, y
) 
 350                 return  # mouse didn't move far enough 
 352             # determine if we can change the position 
 354                 oldPos1
, oldPos2 
= (self
._sashes
[self
._activeSash
], 
 355                                     self
._sashes
[self
._activeSash
+1]) 
 357                 oldPos1
, oldPos2 
= self
._pendingPos
 
 358             newPos1
, newPos2 
= self
._OnSashPositionChanging
(self
._activeSash
, 
 363                 # the change was not allowed 
 366             if newPos1 
== self
._sashes
[self
._activeSash
]: 
 367                 return  # nothing was changed 
 370                 # erase the old tracker 
 371                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 373             if self
._orient 
== wx
.HORIZONTAL
: 
 374                  x 
= self
._SashToCoord
(self
._activeSash
, newPos1
) 
 376                  y 
= self
._SashToCoord
(self
._activeSash
, newPos1
) 
 378             # Remember old positions 
 384                 self
._pendingPos 
= (newPos1
, newPos2
) 
 385                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 387                 self
._DoSetSashPosition
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
) 
 388                 self
._needUpdating 
= True 
 391     # ------------------------------------- 
 394     def _RedrawIfHotSensitive(self
, isHot
): 
 395         if not wx
.VERSION 
>= _RENDER_VER
: 
 397         if wx
.RendererNative
.Get().GetSplitterParams(self
).isHotSensitive
: 
 399             dc 
= wx
.ClientDC(self
) 
 403     def _OnEnterSash(self
): 
 404         self
._SetResizeCursor
() 
 405         self
._RedrawIfHotSensitive
(True) 
 408     def _OnLeaveSash(self
): 
 409         self
.SetCursor(wx
.STANDARD_CURSOR
) 
 410         self
._RedrawIfHotSensitive
(False) 
 413     def _SetResizeCursor(self
): 
 414         if self
._orient 
== wx
.HORIZONTAL
: 
 415             self
.SetCursor(self
._sashCursorWE
) 
 417             self
.SetCursor(self
._sashCursorNS
) 
 420     def _OnSashPositionChanging(self
, idx
, newPos1
, newPos2
, adjustNeighbor
): 
 421         # TODO: check for possibility of unsplit (pane size becomes zero) 
 423         # make sure that minsizes are honored 
 424         newPos1
, newPos2 
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 432         evt 
= MultiSplitterEvent( 
 433             wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGING
, self
) 
 435         evt
.SetSashPosition(newPos1
) 
 436         if not self
._DoSendEvent
(evt
): 
 437             # the event handler vetoed the change 
 440             # or it might have changed the value 
 441             newPos1 
= evt
.GetSashPosition() 
 443         if adjustNeighbor 
and newPos1 
!= -1: 
 444             evt
.SetSashIdx(idx
+1) 
 445             evt
.SetSashPosition(newPos2
) 
 446             if not self
._DoSendEvent
(evt
): 
 447                 # the event handler vetoed the change 
 450                 # or it might have changed the value 
 451                 newPos2 
= evt
.GetSashPosition() 
 455         return (newPos1
, newPos2
) 
 458     def _AdjustSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 459         total 
= newPos1 
+ newPos2
 
 461         # these are the windows on either side of the sash 
 462         win1 
= self
._windows
[idx
] 
 463         win2 
= self
._windows
[idx
+1] 
 465         # make adjustments for window min sizes 
 466         minSize 
= self
._GetWindowMin
(win1
) 
 467         if minSize 
== -1 or self
._minimumPaneSize 
> minSize
: 
 468             minSize 
= self
._minimumPaneSize
 
 469         minSize 
+= self
._GetBorderSize
() 
 470         if newPos1 
< minSize
: 
 472             newPos2 
= total 
- newPos1
 
 475             minSize 
= self
._GetWindowMin
(win2
) 
 476             if minSize 
== -1 or self
._minimumPaneSize 
> minSize
: 
 477                 minSize 
= self
._minimumPaneSize
 
 478             minSize 
+= self
._GetBorderSize
() 
 479             if newPos2 
< minSize
: 
 481                 newPos1 
= total 
- newPos2
 
 483         return (newPos1
, newPos2
) 
 486     def _DoSetSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 487         newPos1
, newPos2 
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 488         if newPos1 
== self
._sashes
[idx
]: 
 490         self
._sashes
[idx
] = newPos1
 
 492             self
._sashes
[idx
+1] = newPos2
 
 496     def _SetSashPositionAndNotify(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 497         # TODO:  what is the thing about _requestedSashPosition for? 
 499         self
._DoSetSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 501         evt 
= MultiSplitterEvent( 
 502             wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGED
, self
) 
 504         evt
.SetSashPosition(newPos1
) 
 505         self
._DoSendEvent
(evt
) 
 508             evt
.SetSashIdx(idx
+1) 
 509             evt
.SetSashPosition(newPos2
) 
 510             self
._DoSendEvent
(evt
) 
 513     def _GetMotionDiff(self
, x
, y
): 
 514         # find the diff from the old pos 
 515         if self
._orient 
== wx
.HORIZONTAL
: 
 516             diff 
= x 
- self
._oldX
 
 518             diff 
= y 
- self
._oldY
 
 522     def _SashToCoord(self
, idx
, sashPos
): 
 525             coord 
+= self
._sashes
[i
] 
 526             coord 
+= self
._GetSashSize
() 
 531     def _GetWindowMin(self
, window
): 
 532         if self
._orient 
== wx
.HORIZONTAL
: 
 533             return window
.GetMinWidth() 
 535             return window
.GetMinHeight() 
 538     def _GetSashSize(self
): 
 539         if self
.HasFlag(wx
.SP_NOSASH
): 
 541         if wx
.VERSION 
>= _RENDER_VER
: 
 542             return wx
.RendererNative
.Get().GetSplitterParams(self
).widthSash
 
 547     def _GetBorderSize(self
): 
 548         if wx
.VERSION 
>= _RENDER_VER
: 
 549             return wx
.RendererNative
.Get().GetSplitterParams(self
).border
 
 554     def _DrawSash(self
, dc
): 
 555         if wx
.VERSION 
>= _RENDER_VER
: 
 556             if self
.HasFlag(wx
.SP_3DBORDER
): 
 557                 wx
.RendererNative
.Get().DrawSplitterBorder( 
 558                     self
, dc
, self
.GetClientRect()) 
 560         # if there are no splits then we're done. 
 561         if len(self
._windows
) < 2: 
 564         # if we are not supposed to use a sash then we're done. 
 565         if self
.HasFlag(wx
.SP_NOSASH
): 
 568         # Reverse the sense of the orientation, in this case it refers 
 569         # to the direction to draw the sash not the direction that 
 570         # windows are stacked. 
 571         orient 
= { wx
.HORIZONTAL 
: wx
.VERTICAL
, 
 572                    wx
.VERTICAL 
: wx
.HORIZONTAL 
}[self
._orient
] 
 576             flag 
= wx
.CONTROL_CURRENT
 
 579         for sash 
in self
._sashes
[:-1]: 
 581             if wx
.VERSION 
>= _RENDER_VER
: 
 582                 wx
.RendererNative
.Get().DrawSplitterSash(self
, dc
, 
 583                                                          self
.GetClientSize(), 
 586                 dc
.SetPen(wx
.TRANSPARENT_PEN
) 
 587                 dc
.SetBrush(wx
.Brush(self
.GetBackgroundColour())) 
 588                 sashsize 
= self
._GetSashSize
() 
 589                 if orient 
== wx
.VERTICAL
: 
 593                     h 
= self
.GetClientSize().height
 
 597                     w 
= self
.GetClientSize().width
 
 599                 dc
.DrawRectangle(x
, y
, w
, h
) 
 601             pos 
+= self
._GetSashSize
() 
 604     def _DrawSashTracker(self
, x
, y
): 
 605         # Draw a line to represent the dragging sash, for when not 
 607         w
, h 
= self
.GetClientSize() 
 610         if self
._orient 
== wx
.HORIZONTAL
: 
 633         x1
, y1 
= self
.ClientToScreenXY(x1
, y1
) 
 634         x2
, y2 
= self
.ClientToScreenXY(x2
, y2
) 
 636         dc
.SetLogicalFunction(wx
.INVERT
) 
 637         dc
.SetPen(self
._sashTrackerPen
) 
 638         dc
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
 639         dc
.DrawLine(x1
, y1
, x2
, y2
) 
 640         dc
.SetLogicalFunction(wx
.COPY
) 
 643     def _SashHitTest(self
, x
, y
, tolerance
=5): 
 644         # if there are no splits then we're done. 
 645         if len(self
._windows
) < 2: 
 648         if self
._orient 
== wx
.HORIZONTAL
: 
 654         for idx
, sash 
in enumerate(self
._sashes
[:-1]): 
 656             hitMin 
= pos 
- tolerance
 
 657             hitMax 
= pos 
+ self
._GetSashSize
() + tolerance
 
 659             if z 
>= hitMin 
and z 
<= hitMax
: 
 662             pos 
+= self
._GetSashSize
()  
 667     def _SizeWindows(self
): 
 669         if not self
._windows
: 
 672         # are there any pending size settings? 
 673         for window
, spos 
in self
._pending
.items(): 
 674             idx 
= self
._windows
.index(window
) 
 675             # TODO: this may need adjusted to make sure they all fit 
 676             # in the current client size 
 677             self
._sashes
[idx
] = spos
 
 678             del self
._pending
[window
] 
 680         # are there any that still have a -1? 
 681         for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 683                 # TODO: this should also be adjusted 
 684                 self
._sashes
[idx
] = 100 
 686         cw
, ch 
= self
.GetClientSize() 
 687         border 
= self
._GetBorderSize
() 
 688         sash   
= self
._GetSashSize
() 
 690         if len(self
._windows
) == 1: 
 691             # there's only one, it's an easy layout 
 692             self
._windows
[0].SetDimensions(border
, border
, 
 693                                            cw 
- 2*border
, ch 
- 2*border
) 
 695             if 'wxMSW' in wx
.PlatformInfo
: 
 697             if self
._orient 
== wx
.HORIZONTAL
: 
 700                 for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 701                     self
._windows
[idx
].SetDimensions(x
, y
, spos
, h
) 
 703                 # last one takes the rest of the space. TODO make this configurable 
 704                 last 
= cw 
- 2*border 
- x
 
 705                 self
._windows
[idx
+1].SetDimensions(x
, y
, last
, h
) 
 707                     self
._sashes
[idx
+1] = last
 
 711                 for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 712                     self
._windows
[idx
].SetDimensions(x
, y
, w
, spos
) 
 714                 # last one takes the rest of the space. TODO make this configurable 
 715                 last 
= ch 
- 2*border 
- y
 
 716                 self
._windows
[idx
+1].SetDimensions(x
, y
, w
, last
) 
 718                     self
._sashes
[idx
+1] = last
 
 719             if 'wxMSW' in wx
.PlatformInfo
: 
 722         self
._DrawSash
(wx
.ClientDC(self
)) 
 723         self
._needUpdating 
= False 
 726     def _DoSendEvent(self
, evt
): 
 727         return not self
.GetEventHandler().ProcessEvent(evt
) or evt
.IsAllowed() 
 729 #---------------------------------------------------------------------- 
 731 class MultiSplitterEvent(wx
.PyCommandEvent
): 
 733     This event class is almost the same as `wx.SplitterEvent` except 
 734     it adds an accessor for the sash index that is being changed.  The 
 735     same event type IDs and event binders are used as with 
 738     def __init__(self
, type=wx
.wxEVT_NULL
, splitter
=None): 
 739         wx
.PyCommandEvent
.__init
__(self
, type) 
 741             self
.SetEventObject(splitter
) 
 742             self
.SetId(splitter
.GetId()) 
 745         self
.isAllowed 
= True 
 747     def SetSashIdx(self
, idx
): 
 750     def SetSashPosition(self
, pos
): 
 753     def GetSashIdx(self
): 
 756     def GetSashPosition(self
): 
 759     # methods from wx.NotifyEvent 
 761         self
.isAllowed 
= False 
 763         self
.isAllowed 
= True 
 765         return self
.isAllowed
 
 769 #----------------------------------------------------------------------