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 manage 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 are 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.  If sashPos is given then it is the  
 135         self
.InsertWindow(sys
.maxint
, window
, sashPos
) 
 138     def InsertWindow(self
, idx
, window
, sashPos
=-1): 
 141         assert window 
not in self
._windows
, "A window can only be in the splitter once!" 
 142         self
._windows
.insert(idx
, window
) 
 143         self
._sashes
.insert(idx
, -1) 
 144         if not window
.IsShown(): 
 147             self
._pending
[window
] = sashPos
 
 148         self
._checkRequestedSashPosition 
= False 
 152     def DetachWindow(self
, window
): 
 154         Removes the window from the stack of windows managed by the 
 155         splitter.  The window will still exist so you should `Hide` or 
 156         `Destroy` it as needed. 
 158         assert window 
in self
._windows
, "Unknown window!" 
 159         idx 
= self
._windows
.index(window
) 
 160         del self
._windows
[idx
] 
 161         del self
._sashes
[idx
] 
 165     def ReplaceWindow(self
, oldWindow
, newWindow
): 
 167         Replaces oldWindow (which is currently being managed by the 
 168         splitter) with newWindow.  The oldWindow window will still 
 169         exist so you should `Hide` or `Destroy` it as needed. 
 171         assert oldWindow 
in self
._windows
, "Unknown window!" 
 172         idx 
= self
._windows
.index(oldWindow
) 
 173         self
._windows
[idx
] = newWindow
 
 174         if not newWindow
.IsShown(): 
 179     def ExchangeWindows(self
, window1
, window2
): 
 181         Trade the positions in the splitter of the two windows. 
 183         assert window1 
in self
._windows
, "Unknown window!" 
 184         assert window2 
in self
._windows
, "Unknown window!" 
 185         idx1 
= self
._windows
.index(window1
) 
 186         idx2 
= self
._windows
.index(window2
) 
 187         self
._windows
[idx1
] = window2
 
 188         self
._windows
[idx2
] = window1
 
 192     def GetWindow(self
, idx
): 
 194         Returns the idx'th window being managed by the splitter. 
 196         assert idx 
< len(self
._windows
) 
 197         return self
._windows
[idx
] 
 200     def GetSashPosition(self
, idx
): 
 202         Returns the position of the idx'th sash, measured from the 
 203         left/top of the window preceding the sash. 
 205         assert idx 
< len(self
._sashes
) 
 206         return self
._sashes
[idx
] 
 209     def SizeWindows(self
): 
 211         Reposition and size the windows managed by the splitter. 
 212         Useful when windows have been added/removed or when styles 
 218     def DoGetBestSize(self
): 
 220         Overridden base class virtual.  Determines the best size of 
 221         the control based on the best sizes of the child windows. 
 224         if not self
._windows
: 
 225             best 
= wx
.Size(10,10) 
 227         sashsize 
= self
._GetSashSize
() 
 228         if self
._orient 
== wx
.HORIZONTAL
: 
 229             for win 
in self
._windows
: 
 230                 winbest 
= win
.GetAdjustedBestSize() 
 231                 best
.width 
+= max(self
._minimumPaneSize
, winbest
.width
) 
 232                 best
.height 
= max(best
.height
, winbest
.height
) 
 233             best
.width 
+= sashsize 
* (len(self
._windows
)-1) 
 236             for win 
in self
._windows
: 
 237                 winbest 
= win
.GetAdjustedBestSize() 
 238                 best
.height 
+= max(self
._minimumPaneSize
, winbest
.height
) 
 239                 best
.width 
= max(best
.width
, winbest
.width
) 
 240             best
.height 
+= sashsize 
* (len(self
._windows
)-1) 
 242         border 
= 2 * self
._GetBorderSize
() 
 244         best
.height 
+= border
 
 247     # ------------------------------------- 
 250     def _OnPaint(self
, evt
): 
 251         dc 
= wx
.PaintDC(self
) 
 255     def _OnSize(self
, evt
): 
 256         parent 
= wx
.GetTopLevelParent(self
) 
 257         if parent
.IsIconized(): 
 263     def _OnIdle(self
, evt
): 
 265         # if this is the first idle time after a sash position has 
 266         # potentially been set, allow _SizeWindows to check for a 
 268         if not self
._checkRequestedSashPosition
: 
 269             self
._checkRequestedSashPosition 
= True 
 272         if self
._needUpdating
: 
 277     def _OnMouse(self
, evt
): 
 278         if self
.HasFlag(wx
.SP_NOSASH
): 
 281         x
, y 
= evt
.GetPosition() 
 282         isLive 
= self
.HasFlag(wx
.SP_LIVE_UPDATE
) 
 283         adjustNeighbor 
= evt
.ShiftDown() 
 285         # LeftDown: set things up for dragging the sash 
 286         if evt
.LeftDown() and self
._SashHitTest
(x
, y
) != -1: 
 287             self
._activeSash 
= self
._SashHitTest
(x
, y
) 
 288             self
._dragMode 
= wx
.SPLIT_DRAG_DRAGGING
 
 291             self
._SetResizeCursor
() 
 294                 self
._pendingPos 
= (self
._sashes
[self
._activeSash
], 
 295                                     self
._sashes
[self
._activeSash
+1]) 
 296                 self
._DrawSashTracker
(x
, y
) 
 302         # LeftUp: Finsish the drag 
 303         elif evt
.LeftUp() and self
._dragMode 
== wx
.SPLIT_DRAG_DRAGGING
: 
 304             self
._dragMode 
= wx
.SPLIT_DRAG_NONE
 
 306             self
.SetCursor(wx
.STANDARD_CURSOR
) 
 309                 # erase the old tracker 
 310                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 312             diff 
= self
._GetMotionDiff
(x
, y
) 
 314             # determine if we can change the position 
 316                 oldPos1
, oldPos2 
= (self
._sashes
[self
._activeSash
], 
 317                                     self
._sashes
[self
._activeSash
+1]) 
 319                 oldPos1
, oldPos2 
= self
._pendingPos
 
 320             newPos1
, newPos2 
= self
._OnSashPositionChanging
(self
._activeSash
, 
 325                 # the change was not allowed 
 328             # TODO: check for unsplit? 
 330             self
._SetSashPositionAndNotify
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
) 
 331             self
._activeSash 
= -1 
 332             self
._pendingPos 
= (-1, -1) 
 335         # Entering or Leaving a sash: Change the cursor 
 336         elif (evt
.Moving() or evt
.Leaving() or evt
.Entering()) and self
._dragMode 
== wx
.SPLIT_DRAG_NONE
: 
 337             if evt
.Leaving() or self
._SashHitTest
(x
, y
) == -1: 
 343         elif evt
.Dragging() and self
._dragMode 
== wx
.SPLIT_DRAG_DRAGGING
: 
 344             diff 
= self
._GetMotionDiff
(x
, y
) 
 346                 return  # mouse didn't move far enough 
 348             # determine if we can change the position 
 350                 oldPos1
, oldPos2 
= (self
._sashes
[self
._activeSash
], 
 351                                     self
._sashes
[self
._activeSash
+1]) 
 353                 oldPos1
, oldPos2 
= self
._pendingPos
 
 354             newPos1
, newPos2 
= self
._OnSashPositionChanging
(self
._activeSash
, 
 359                 # the change was not allowed 
 362             if newPos1 
== self
._sashes
[self
._activeSash
]: 
 363                 return  # nothing was changed 
 366                 # erase the old tracker 
 367                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 369             if self
._orient 
== wx
.HORIZONTAL
: 
 370                  x 
= self
._SashToCoord
(self
._activeSash
, newPos1
) 
 372                  y 
= self
._SashToCoord
(self
._activeSash
, newPos1
) 
 374             # Remember old positions 
 380                 self
._pendingPos 
= (newPos1
, newPos2
) 
 381                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 383                 self
._DoSetSashPosition
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
) 
 384                 self
._needUpdating 
= True 
 387     # ------------------------------------- 
 390     def _RedrawIfHotSensitive(self
, isHot
): 
 391         if not wx
.VERSION 
>= _RENDER_VER
: 
 393         if wx
.RendererNative
.Get().GetSplitterParams(self
).isHotSensitive
: 
 395             dc 
= wx
.ClientDC(self
) 
 399     def _OnEnterSash(self
): 
 400         self
._SetResizeCursor
() 
 401         self
._RedrawIfHotSensitive
(True) 
 404     def _OnLeaveSash(self
): 
 405         self
.SetCursor(wx
.STANDARD_CURSOR
) 
 406         self
._RedrawIfHotSensitive
(False) 
 409     def _SetResizeCursor(self
): 
 410         if self
._orient 
== wx
.HORIZONTAL
: 
 411             self
.SetCursor(self
._sashCursorWE
) 
 413             self
.SetCursor(self
._sashCursorNS
) 
 416     def _OnSashPositionChanging(self
, idx
, newPos1
, newPos2
, adjustNeighbor
): 
 417         # TODO: check for possibility of unsplit (pane size becomes zero) 
 419         # make sure that minsizes are honored 
 420         newPos1
, newPos2 
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 428         evt 
= MultiSplitterEvent( 
 429             wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGING
, self
) 
 431         evt
.SetSashPosition(newPos1
) 
 432         if not self
._DoSendEvent
(evt
): 
 433             # the event handler vetoed the change 
 436             # or it might have changed the value 
 437             newPos1 
= evt
.GetSashPosition() 
 439         if adjustNeighbor 
and newPos1 
!= -1: 
 440             evt
.SetSashIdx(idx
+1) 
 441             evt
.SetSashPosition(newPos2
) 
 442             if not self
._DoSendEvent
(evt
): 
 443                 # the event handler vetoed the change 
 446                 # or it might have changed the value 
 447                 newPos2 
= evt
.GetSashPosition() 
 451         return (newPos1
, newPos2
) 
 454     def _AdjustSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 455         total 
= newPos1 
+ newPos2
 
 457         # these are the windows on either side of the sash 
 458         win1 
= self
._windows
[idx
] 
 459         win2 
= self
._windows
[idx
+1] 
 461         # make adjustments for window min sizes 
 462         minSize 
= self
._GetWindowMin
(win1
) 
 463         if minSize 
== -1 or self
._minimumPaneSize 
> minSize
: 
 464             minSize 
= self
._minimumPaneSize
 
 465         minSize 
+= self
._GetBorderSize
() 
 466         if newPos1 
< minSize
: 
 468             newPos2 
= total 
- newPos1
 
 471             minSize 
= self
._GetWindowMin
(win2
) 
 472             if minSize 
== -1 or self
._minimumPaneSize 
> minSize
: 
 473                 minSize 
= self
._minimumPaneSize
 
 474             minSize 
+= self
._GetBorderSize
() 
 475             if newPos2 
< minSize
: 
 477                 newPos1 
= total 
- newPos2
 
 479         return (newPos1
, newPos2
) 
 482     def _DoSetSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 483         newPos1
, newPos2 
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 484         if newPos1 
== self
._sashes
[idx
]: 
 486         self
._sashes
[idx
] = newPos1
 
 488             self
._sashes
[idx
+1] = newPos2
 
 492     def _SetSashPositionAndNotify(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 493         # TODO:  what is the thing about _requestedSashPosition for? 
 495         self
._DoSetSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 497         evt 
= MultiSplitterEvent( 
 498             wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGED
, self
) 
 500         evt
.SetSashPosition(newPos1
) 
 501         self
._DoSendEvent
(evt
) 
 504             evt
.SetSashIdx(idx
+1) 
 505             evt
.SetSashPosition(newPos2
) 
 506             self
._DoSendEvent
(evt
) 
 509     def _GetMotionDiff(self
, x
, y
): 
 510         # find the diff from the old pos 
 511         if self
._orient 
== wx
.HORIZONTAL
: 
 512             diff 
= x 
- self
._oldX
 
 514             diff 
= y 
- self
._oldY
 
 518     def _SashToCoord(self
, idx
, sashPos
): 
 521             coord 
+= self
._sashes
[i
] 
 522             coord 
+= self
._GetSashSize
() 
 527     def _GetWindowMin(self
, window
): 
 528         if self
._orient 
== wx
.HORIZONTAL
: 
 529             return window
.GetMinWidth() 
 531             return window
.GetMinHeight() 
 534     def _GetSashSize(self
): 
 535         if self
.HasFlag(wx
.SP_NOSASH
): 
 537         if wx
.VERSION 
>= _RENDER_VER
: 
 538             return wx
.RendererNative
.Get().GetSplitterParams(self
).widthSash
 
 543     def _GetBorderSize(self
): 
 544         if wx
.VERSION 
>= _RENDER_VER
: 
 545             return wx
.RendererNative
.Get().GetSplitterParams(self
).border
 
 550     def _DrawSash(self
, dc
): 
 551         if wx
.VERSION 
>= _RENDER_VER
: 
 552             if self
.HasFlag(wx
.SP_3DBORDER
): 
 553                 wx
.RendererNative
.Get().DrawSplitterBorder( 
 554                     self
, dc
, self
.GetClientRect()) 
 556         # if there are no splits then we're done. 
 557         if len(self
._windows
) < 2: 
 560         # if we are not supposed to use a sash then we're done. 
 561         if self
.HasFlag(wx
.SP_NOSASH
): 
 564         # Reverse the sense of the orientation, in this case it refers 
 565         # to the direction to draw the sash not the direction that 
 566         # windows are stacked. 
 567         orient 
= { wx
.HORIZONTAL 
: wx
.VERTICAL
, 
 568                    wx
.VERTICAL 
: wx
.HORIZONTAL 
}[self
._orient
] 
 572             flag 
= wx
.CONTROL_CURRENT
 
 575         for sash 
in self
._sashes
[:-1]: 
 577             if wx
.VERSION 
>= _RENDER_VER
: 
 578                 wx
.RendererNative
.Get().DrawSplitterSash(self
, dc
, 
 579                                                          self
.GetClientSize(), 
 582                 dc
.SetPen(wx
.TRANSPARENT_PEN
) 
 583                 dc
.SetBrush(wx
.Brush(self
.GetBackgroundColour())) 
 584                 sashsize 
= self
._GetSashSize
() 
 585                 if orient 
== wx
.VERTICAL
: 
 589                     h 
= self
.GetClientSize().height
 
 593                     w 
= self
.GetClientSize().width
 
 595                 dc
.DrawRectangle(x
, y
, w
, h
) 
 597             pos 
+= self
._GetSashSize
() 
 600     def _DrawSashTracker(self
, x
, y
): 
 601         # Draw a line to represent the dragging sash, for when not 
 603         w
, h 
= self
.GetClientSize() 
 606         if self
._orient 
== wx
.HORIZONTAL
: 
 629         x1
, y1 
= self
.ClientToScreenXY(x1
, y1
) 
 630         x2
, y2 
= self
.ClientToScreenXY(x2
, y2
) 
 632         dc
.SetLogicalFunction(wx
.INVERT
) 
 633         dc
.SetPen(self
._sashTrackerPen
) 
 634         dc
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
 635         dc
.DrawLine(x1
, y1
, x2
, y2
) 
 636         dc
.SetLogicalFunction(wx
.COPY
) 
 639     def _SashHitTest(self
, x
, y
, tolerance
=5): 
 640         # if there are no splits then we're done. 
 641         if len(self
._windows
) < 2: 
 644         if self
._orient 
== wx
.HORIZONTAL
: 
 650         for idx
, sash 
in enumerate(self
._sashes
[:-1]): 
 652             hitMin 
= pos 
- tolerance
 
 653             hitMax 
= pos 
+ self
._GetSashSize
() + tolerance
 
 655             if z 
>= hitMin 
and z 
<= hitMax
: 
 658             pos 
+= self
._GetSashSize
()  
 663     def _SizeWindows(self
): 
 665         if not self
._windows
: 
 668         # are there any pending size settings? 
 669         for window
, spos 
in self
._pending
.items(): 
 670             idx 
= self
._windows
.index(window
) 
 671             # TODO: this may need adjusted to make sure they all fit 
 672             # in the current client size 
 673             self
._sashes
[idx
] = spos
 
 674             del self
._pending
[window
] 
 676         # are there any that still have a -1? 
 677         for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 679                 # TODO: this should also be adjusted 
 680                 self
._sashes
[idx
] = 100 
 682         cw
, ch 
= self
.GetClientSize() 
 683         border 
= self
._GetBorderSize
() 
 684         sash   
= self
._GetSashSize
() 
 686         if len(self
._windows
) == 1: 
 687             # there's only one, it's an easy layout 
 688             self
._windows
[0].SetDimensions(border
, border
, 
 689                                            cw 
- 2*border
, ch 
- 2*border
) 
 691             if 'wxMSW' in wx
.PlatformInfo
: 
 693             if self
._orient 
== wx
.HORIZONTAL
: 
 696                 for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 697                     self
._windows
[idx
].SetDimensions(x
, y
, spos
, h
) 
 699                 # last one takes the rest of the space. TODO make this configurable 
 700                 last 
= cw 
- 2*border 
- x
 
 701                 self
._windows
[idx
+1].SetDimensions(x
, y
, last
, h
) 
 703                     self
._sashes
[idx
+1] = last
 
 707                 for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 708                     self
._windows
[idx
].SetDimensions(x
, y
, w
, spos
) 
 710                 # last one takes the rest of the space. TODO make this configurable 
 711                 last 
= ch 
- 2*border 
- y
 
 712                 self
._windows
[idx
+1].SetDimensions(x
, y
, w
, last
) 
 714                     self
._sashes
[idx
+1] = last
 
 715             if 'wxMSW' in wx
.PlatformInfo
: 
 718         self
._DrawSash
(wx
.ClientDC(self
)) 
 719         self
._needUpdating 
= False 
 722     def _DoSendEvent(self
, evt
): 
 723         return not self
.GetEventHandler().ProcessEvent(evt
) or evt
.IsAllowed() 
 725 #---------------------------------------------------------------------- 
 727 class MultiSplitterEvent(wx
.PyCommandEvent
): 
 729     This event class is almost the same as `wx.SplitterEvent` except 
 730     it adds an accessor for the sash index that is being changed.  The 
 731     same event type IDs and event binders are used as with 
 734     def __init__(self
, type=wx
.wxEVT_NULL
, splitter
=None): 
 735         wx
.PyCommandEvent
.__init
__(self
, type) 
 737             self
.SetEventObject(splitter
) 
 738             self
.SetId(splitter
.GetId()) 
 741         self
.isAllowed 
= True 
 743     def SetSashIdx(self
, idx
): 
 746     def SetSashPosition(self
, pos
): 
 749     def GetSashIdx(self
): 
 752     def GetSashPosition(self
): 
 755     # methods from wx.NotifyEvent 
 757         self
.isAllowed 
= False 
 759         self
.isAllowed 
= True 
 761         return self
.isAllowed
 
 765 #----------------------------------------------------------------------