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 
  21 _RENDER_VER 
= (2,6,1,1) 
  23 #---------------------------------------------------------------------- 
  25 class MultiSplitterWindow(wx
.PyPanel
): 
  27     This class is very similar to `wx.SplitterWindow` except that it 
  28     allows for more than two windows and more than one sash.  Many of 
  29     the same styles, constants, and methods behave the same as in 
  30     wx.SplitterWindow.  The key differences are seen in the methods 
  31     that deal with the child windows managed by the splitter, and also 
  32     those that deal with the sash positions.  In most cases you will 
  33     need to pass an index value to tell the class which window or sash 
  36     The concept of the sash position is also different than in 
  37     wx.SplitterWindow.  Since the wx.Splitterwindow has only one sash 
  38     you can think of it's position as either relative to the whole 
  39     splitter window, or as relative to the first window pane managed 
  40     by the splitter.  Once there is more than one sash then the 
  41     distinciton between the two concepts needs to be clairified.  I've 
  42     chosen to use the second definition, and sash positions are the 
  43     distance (either horizontally or vertically) from the origin of 
  44     the window just before the sash in the splitter stack. 
  46     NOTE: These things are not yet supported: 
  48         * Using negative sash positions to indicate a position offset 
  51         * User controlled unsplitting (with double clicks on the sash 
  52           or dragging a sash until the pane size is zero.) 
  57     def __init__(self
, parent
, id=-1, 
  58                  pos 
= wx
.DefaultPosition
, size 
= wx
.DefaultSize
, 
  59                  style 
= 0, name
="multiSplitter"): 
  61         # always turn on tab traversal 
  62         style |
= wx
.TAB_TRAVERSAL
 
  64         # and turn off any border styles 
  65         style 
&= ~wx
.BORDER_MASK
 
  66         style |
= wx
.BORDER_NONE
 
  68         # initialize the base class 
  69         wx
.PyPanel
.__init
__(self
, parent
, id, pos
, size
, style
, name
) 
  70         self
.SetBackgroundStyle(wx
.BG_STYLE_CUSTOM
) 
  72         # initialize data members 
  76         self
._permitUnsplitAlways 
= self
.HasFlag(wx
.SP_PERMIT_UNSPLIT
) 
  77         self
._orient 
= wx
.HORIZONTAL
 
  78         self
._dragMode 
= wx
.SPLIT_DRAG_NONE
 
  82         self
._checkRequestedSashPosition 
= False 
  83         self
._minimumPaneSize 
= 0 
  84         self
._sashCursorWE 
= wx
.StockCursor(wx
.CURSOR_SIZEWE
) 
  85         self
._sashCursorNS 
= wx
.StockCursor(wx
.CURSOR_SIZENS
) 
  86         self
._sashTrackerPen 
= wx
.Pen(wx
.BLACK
, 2, wx
.SOLID
) 
  87         self
._needUpdating 
= False 
  91         self
.Bind(wx
.EVT_PAINT
,        self
._OnPaint
) 
  92         self
.Bind(wx
.EVT_IDLE
,         self
._OnIdle
) 
  93         self
.Bind(wx
.EVT_SIZE
,         self
._OnSize
) 
  94         self
.Bind(wx
.EVT_MOUSE_EVENTS
, self
._OnMouse
) 
  98     def SetOrientation(self
, orient
): 
 100         Set whether the windows managed by the splitter will be 
 101         stacked vertically or horizontally.  The default is 
 104         assert orient 
in [ wx
.VERTICAL
, wx
.HORIZONTAL 
] 
 105         self
._orient 
= orient
 
 107     def GetOrientation(self
): 
 109         Returns the current orientation of the splitter, either 
 110         wx.VERTICAL or wx.HORIZONTAL. 
 115     def SetMinimumPaneSize(self
, minSize
): 
 117         Set the smallest size that any pane will be allowed to be 
 120         self
._minimumPaneSize 
= minSize
 
 122     def GetMinimumPaneSize(self
): 
 124         Returns the smallest allowed size for a window pane. 
 126         return self
._minimumPaneSize
 
 130     def AppendWindow(self
, window
, sashPos
=-1): 
 132         Add a new window to the splitter at the right side or bottom 
 133         of the window stack.  If sashPos is given then it is used to 
 136         self
.InsertWindow(len(self
._windows
), window
, sashPos
) 
 139     def InsertWindow(self
, idx
, window
, sashPos
=-1): 
 141         Insert a new window into the splitter at the position given in 
 144         assert window 
not in self
._windows
, "A window can only be in the splitter once!" 
 145         self
._windows
.insert(idx
, window
) 
 146         self
._sashes
.insert(idx
, -1) 
 147         if not window
.IsShown(): 
 150             self
._pending
[window
] = sashPos
 
 151         self
._checkRequestedSashPosition 
= False 
 155     def DetachWindow(self
, window
): 
 157         Removes the window from the stack of windows managed by the 
 158         splitter.  The window will still exist so you should `Hide` or 
 159         `Destroy` it as needed. 
 161         assert window 
in self
._windows
, "Unknown window!" 
 162         idx 
= self
._windows
.index(window
) 
 163         del self
._windows
[idx
] 
 164         del self
._sashes
[idx
] 
 168     def ReplaceWindow(self
, oldWindow
, newWindow
): 
 170         Replaces oldWindow (which is currently being managed by the 
 171         splitter) with newWindow.  The oldWindow window will still 
 172         exist so you should `Hide` or `Destroy` it as needed. 
 174         assert oldWindow 
in self
._windows
, "Unknown window!" 
 175         idx 
= self
._windows
.index(oldWindow
) 
 176         self
._windows
[idx
] = newWindow
 
 177         if not newWindow
.IsShown(): 
 182     def ExchangeWindows(self
, window1
, window2
): 
 184         Trade the positions in the splitter of the two windows. 
 186         assert window1 
in self
._windows
, "Unknown window!" 
 187         assert window2 
in self
._windows
, "Unknown window!" 
 188         idx1 
= self
._windows
.index(window1
) 
 189         idx2 
= self
._windows
.index(window2
) 
 190         self
._windows
[idx1
] = window2
 
 191         self
._windows
[idx2
] = window1
 
 195     def GetWindow(self
, idx
): 
 197         Returns the idx'th window being managed by the splitter. 
 199         assert idx 
< len(self
._windows
) 
 200         return self
._windows
[idx
] 
 203     def GetSashPosition(self
, idx
): 
 205         Returns the position of the idx'th sash, measured from the 
 206         left/top of the window preceding the sash. 
 208         assert idx 
< len(self
._sashes
) 
 209         return self
._sashes
[idx
] 
 212     def SetSashPosition(self
, idx
, pos
): 
 214         Set the psition of the idx'th sash, measured from the left/top 
 215         of the window preceding the sash. 
 217         assert idx 
< len(self
._sashes
) 
 218         self
._sashes
[idx
] = pos
 
 222     def SizeWindows(self
): 
 224         Reposition and size the windows managed by the splitter. 
 225         Useful when windows have been added/removed or when styles 
 231     def DoGetBestSize(self
): 
 233         Overridden base class virtual.  Determines the best size of 
 234         the control based on the best sizes of the child windows. 
 237         if not self
._windows
: 
 238             best 
= wx
.Size(10,10) 
 240         sashsize 
= self
._GetSashSize
() 
 241         if self
._orient 
== wx
.HORIZONTAL
: 
 242             for win 
in self
._windows
: 
 243                 winbest 
= win
.GetEffectiveMinSize() 
 244                 best
.width 
+= max(self
._minimumPaneSize
, winbest
.width
) 
 245                 best
.height 
= max(best
.height
, winbest
.height
) 
 246             best
.width 
+= sashsize 
* (len(self
._windows
)-1) 
 249             for win 
in self
._windows
: 
 250                 winbest 
= win
.GetEffectiveMinSize() 
 251                 best
.height 
+= max(self
._minimumPaneSize
, winbest
.height
) 
 252                 best
.width 
= max(best
.width
, winbest
.width
) 
 253             best
.height 
+= sashsize 
* (len(self
._windows
)-1) 
 255         border 
= 2 * self
._GetBorderSize
() 
 257         best
.height 
+= border
 
 260     # ------------------------------------- 
 263     def _OnPaint(self
, evt
): 
 264         dc 
= wx
.PaintDC(self
) 
 268     def _OnSize(self
, evt
): 
 269         parent 
= wx
.GetTopLevelParent(self
) 
 270         if parent
.IsIconized(): 
 276     def _OnIdle(self
, evt
): 
 278         # if this is the first idle time after a sash position has 
 279         # potentially been set, allow _SizeWindows to check for a 
 281         if not self
._checkRequestedSashPosition
: 
 282             self
._checkRequestedSashPosition 
= True 
 285         if self
._needUpdating
: 
 290     def _OnMouse(self
, evt
): 
 291         if self
.HasFlag(wx
.SP_NOSASH
): 
 294         x
, y 
= evt
.GetPosition() 
 295         isLive 
= self
.HasFlag(wx
.SP_LIVE_UPDATE
) 
 296         adjustNeighbor 
= evt
.ShiftDown() 
 298         # LeftDown: set things up for dragging the sash 
 299         if evt
.LeftDown() and self
._SashHitTest
(x
, y
) != -1: 
 300             self
._activeSash 
= self
._SashHitTest
(x
, y
) 
 301             self
._dragMode 
= wx
.SPLIT_DRAG_DRAGGING
 
 304             self
._SetResizeCursor
() 
 307                 self
._pendingPos 
= (self
._sashes
[self
._activeSash
], 
 308                                     self
._sashes
[self
._activeSash
+1]) 
 309                 self
._DrawSashTracker
(x
, y
) 
 315         # LeftUp: Finsish the drag 
 316         elif evt
.LeftUp() and self
._dragMode 
== wx
.SPLIT_DRAG_DRAGGING
: 
 317             self
._dragMode 
= wx
.SPLIT_DRAG_NONE
 
 319             self
.SetCursor(wx
.STANDARD_CURSOR
) 
 322                 # erase the old tracker 
 323                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 325             diff 
= self
._GetMotionDiff
(x
, y
) 
 327             # determine if we can change the position 
 329                 oldPos1
, oldPos2 
= (self
._sashes
[self
._activeSash
], 
 330                                     self
._sashes
[self
._activeSash
+1]) 
 332                 oldPos1
, oldPos2 
= self
._pendingPos
 
 333             newPos1
, newPos2 
= self
._OnSashPositionChanging
(self
._activeSash
, 
 338                 # the change was not allowed 
 341             # TODO: check for unsplit? 
 343             self
._SetSashPositionAndNotify
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
) 
 344             self
._activeSash 
= -1 
 345             self
._pendingPos 
= (-1, -1) 
 348         # Entering or Leaving a sash: Change the cursor 
 349         elif (evt
.Moving() or evt
.Leaving() or evt
.Entering()) and self
._dragMode 
== wx
.SPLIT_DRAG_NONE
: 
 350             if evt
.Leaving() or self
._SashHitTest
(x
, y
) == -1: 
 356         elif evt
.Dragging() and self
._dragMode 
== wx
.SPLIT_DRAG_DRAGGING
: 
 357             diff 
= self
._GetMotionDiff
(x
, y
) 
 359                 return  # mouse didn't move far enough 
 361             # determine if we can change the position 
 363                 oldPos1
, oldPos2 
= (self
._sashes
[self
._activeSash
], 
 364                                     self
._sashes
[self
._activeSash
+1]) 
 366                 oldPos1
, oldPos2 
= self
._pendingPos
 
 367             newPos1
, newPos2 
= self
._OnSashPositionChanging
(self
._activeSash
, 
 372                 # the change was not allowed 
 375             if newPos1 
== self
._sashes
[self
._activeSash
]: 
 376                 return  # nothing was changed 
 379                 # erase the old tracker 
 380                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 382             if self
._orient 
== wx
.HORIZONTAL
: 
 383                  x 
= self
._SashToCoord
(self
._activeSash
, newPos1
) 
 385                  y 
= self
._SashToCoord
(self
._activeSash
, newPos1
) 
 387             # Remember old positions 
 393                 self
._pendingPos 
= (newPos1
, newPos2
) 
 394                 self
._DrawSashTracker
(self
._oldX
, self
._oldY
) 
 396                 self
._DoSetSashPosition
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
) 
 397                 self
._needUpdating 
= True 
 400     # ------------------------------------- 
 403     def _RedrawIfHotSensitive(self
, isHot
): 
 404         if not wx
.VERSION 
>= _RENDER_VER
: 
 406         if wx
.RendererNative
.Get().GetSplitterParams(self
).isHotSensitive
: 
 408             dc 
= wx
.ClientDC(self
) 
 412     def _OnEnterSash(self
): 
 413         self
._SetResizeCursor
() 
 414         self
._RedrawIfHotSensitive
(True) 
 417     def _OnLeaveSash(self
): 
 418         self
.SetCursor(wx
.STANDARD_CURSOR
) 
 419         self
._RedrawIfHotSensitive
(False) 
 422     def _SetResizeCursor(self
): 
 423         if self
._orient 
== wx
.HORIZONTAL
: 
 424             self
.SetCursor(self
._sashCursorWE
) 
 426             self
.SetCursor(self
._sashCursorNS
) 
 429     def _OnSashPositionChanging(self
, idx
, newPos1
, newPos2
, adjustNeighbor
): 
 430         # TODO: check for possibility of unsplit (pane size becomes zero) 
 432         # make sure that minsizes are honored 
 433         newPos1
, newPos2 
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 441         evt 
= MultiSplitterEvent( 
 442             wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGING
, self
) 
 444         evt
.SetSashPosition(newPos1
) 
 445         if not self
._DoSendEvent
(evt
): 
 446             # the event handler vetoed the change 
 449             # or it might have changed the value 
 450             newPos1 
= evt
.GetSashPosition() 
 452         if adjustNeighbor 
and newPos1 
!= -1: 
 453             evt
.SetSashIdx(idx
+1) 
 454             evt
.SetSashPosition(newPos2
) 
 455             if not self
._DoSendEvent
(evt
): 
 456                 # the event handler vetoed the change 
 459                 # or it might have changed the value 
 460                 newPos2 
= evt
.GetSashPosition() 
 464         return (newPos1
, newPos2
) 
 467     def _AdjustSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 468         total 
= newPos1 
+ newPos2
 
 470         # these are the windows on either side of the sash 
 471         win1 
= self
._windows
[idx
] 
 472         win2 
= self
._windows
[idx
+1] 
 474         # make adjustments for window min sizes 
 475         minSize 
= self
._GetWindowMin
(win1
) 
 476         if minSize 
== -1 or self
._minimumPaneSize 
> minSize
: 
 477             minSize 
= self
._minimumPaneSize
 
 478         minSize 
+= self
._GetBorderSize
() 
 479         if newPos1 
< minSize
: 
 481             newPos2 
= total 
- newPos1
 
 484             minSize 
= self
._GetWindowMin
(win2
) 
 485             if minSize 
== -1 or self
._minimumPaneSize 
> minSize
: 
 486                 minSize 
= self
._minimumPaneSize
 
 487             minSize 
+= self
._GetBorderSize
() 
 488             if newPos2 
< minSize
: 
 490                 newPos1 
= total 
- newPos2
 
 492         return (newPos1
, newPos2
) 
 495     def _DoSetSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 496         newPos1
, newPos2 
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 497         if newPos1 
== self
._sashes
[idx
]: 
 499         self
._sashes
[idx
] = newPos1
 
 501             self
._sashes
[idx
+1] = newPos2
 
 505     def _SetSashPositionAndNotify(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False): 
 506         # TODO:  what is the thing about _requestedSashPosition for? 
 508         self
._DoSetSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
) 
 510         evt 
= MultiSplitterEvent( 
 511             wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGED
, self
) 
 513         evt
.SetSashPosition(newPos1
) 
 514         self
._DoSendEvent
(evt
) 
 517             evt
.SetSashIdx(idx
+1) 
 518             evt
.SetSashPosition(newPos2
) 
 519             self
._DoSendEvent
(evt
) 
 522     def _GetMotionDiff(self
, x
, y
): 
 523         # find the diff from the old pos 
 524         if self
._orient 
== wx
.HORIZONTAL
: 
 525             diff 
= x 
- self
._oldX
 
 527             diff 
= y 
- self
._oldY
 
 531     def _SashToCoord(self
, idx
, sashPos
): 
 534             coord 
+= self
._sashes
[i
] 
 535             coord 
+= self
._GetSashSize
() 
 540     def _GetWindowMin(self
, window
): 
 541         if self
._orient 
== wx
.HORIZONTAL
: 
 542             return window
.GetMinWidth() 
 544             return window
.GetMinHeight() 
 547     def _GetSashSize(self
): 
 548         if self
.HasFlag(wx
.SP_NOSASH
): 
 550         if wx
.VERSION 
>= _RENDER_VER
: 
 551             return wx
.RendererNative
.Get().GetSplitterParams(self
).widthSash
 
 556     def _GetBorderSize(self
): 
 557         if wx
.VERSION 
>= _RENDER_VER
: 
 558             return wx
.RendererNative
.Get().GetSplitterParams(self
).border
 
 563     def _DrawSash(self
, dc
): 
 564         if wx
.VERSION 
>= _RENDER_VER
: 
 565             if self
.HasFlag(wx
.SP_3DBORDER
): 
 566                 wx
.RendererNative
.Get().DrawSplitterBorder( 
 567                     self
, dc
, self
.GetClientRect()) 
 569         # if there are no splits then we're done. 
 570         if len(self
._windows
) < 2: 
 573         # if we are not supposed to use a sash then we're done. 
 574         if self
.HasFlag(wx
.SP_NOSASH
): 
 577         # Reverse the sense of the orientation, in this case it refers 
 578         # to the direction to draw the sash not the direction that 
 579         # windows are stacked. 
 580         orient 
= { wx
.HORIZONTAL 
: wx
.VERTICAL
, 
 581                    wx
.VERTICAL 
: wx
.HORIZONTAL 
}[self
._orient
] 
 585             flag 
= wx
.CONTROL_CURRENT
 
 588         for sash 
in self
._sashes
[:-1]: 
 590             if wx
.VERSION 
>= _RENDER_VER
: 
 591                 wx
.RendererNative
.Get().DrawSplitterSash(self
, dc
, 
 592                                                          self
.GetClientSize(), 
 595                 dc
.SetPen(wx
.TRANSPARENT_PEN
) 
 596                 dc
.SetBrush(wx
.Brush(self
.GetBackgroundColour())) 
 597                 sashsize 
= self
._GetSashSize
() 
 598                 if orient 
== wx
.VERTICAL
: 
 602                     h 
= self
.GetClientSize().height
 
 606                     w 
= self
.GetClientSize().width
 
 608                 dc
.DrawRectangle(x
, y
, w
, h
) 
 610             pos 
+= self
._GetSashSize
() 
 613     def _DrawSashTracker(self
, x
, y
): 
 614         # Draw a line to represent the dragging sash, for when not 
 616         w
, h 
= self
.GetClientSize() 
 619         if self
._orient 
== wx
.HORIZONTAL
: 
 642         x1
, y1 
= self
.ClientToScreenXY(x1
, y1
) 
 643         x2
, y2 
= self
.ClientToScreenXY(x2
, y2
) 
 645         dc
.SetLogicalFunction(wx
.INVERT
) 
 646         dc
.SetPen(self
._sashTrackerPen
) 
 647         dc
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
 648         dc
.DrawLine(x1
, y1
, x2
, y2
) 
 649         dc
.SetLogicalFunction(wx
.COPY
) 
 652     def _SashHitTest(self
, x
, y
, tolerance
=5): 
 653         # if there are no splits then we're done. 
 654         if len(self
._windows
) < 2: 
 657         if self
._orient 
== wx
.HORIZONTAL
: 
 663         for idx
, sash 
in enumerate(self
._sashes
[:-1]): 
 665             hitMin 
= pos 
- tolerance
 
 666             hitMax 
= pos 
+ self
._GetSashSize
() + tolerance
 
 668             if z 
>= hitMin 
and z 
<= hitMax
: 
 671             pos 
+= self
._GetSashSize
()  
 676     def _SizeWindows(self
): 
 678         if not self
._windows
: 
 681         # are there any pending size settings? 
 682         for window
, spos 
in self
._pending
.items(): 
 683             idx 
= self
._windows
.index(window
) 
 684             # TODO: this may need adjusted to make sure they all fit 
 685             # in the current client size 
 686             self
._sashes
[idx
] = spos
 
 687             del self
._pending
[window
] 
 689         # are there any that still have a -1? 
 690         for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 692                 # TODO: this should also be adjusted 
 693                 self
._sashes
[idx
] = 100 
 695         cw
, ch 
= self
.GetClientSize() 
 696         border 
= self
._GetBorderSize
() 
 697         sash   
= self
._GetSashSize
() 
 699         if len(self
._windows
) == 1: 
 700             # there's only one, it's an easy layout 
 701             self
._windows
[0].SetDimensions(border
, border
, 
 702                                            cw 
- 2*border
, ch 
- 2*border
) 
 704             if 'wxMSW' in wx
.PlatformInfo
: 
 706             if self
._orient 
== wx
.HORIZONTAL
: 
 709                 for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 710                     self
._windows
[idx
].SetDimensions(x
, y
, spos
, h
) 
 712                 # last one takes the rest of the space. TODO make this configurable 
 713                 last 
= cw 
- 2*border 
- x
 
 714                 self
._windows
[idx
+1].SetDimensions(x
, y
, last
, h
) 
 716                     self
._sashes
[idx
+1] = last
 
 720                 for idx
, spos 
in enumerate(self
._sashes
[:-1]): 
 721                     self
._windows
[idx
].SetDimensions(x
, y
, w
, spos
) 
 723                 # last one takes the rest of the space. TODO make this configurable 
 724                 last 
= ch 
- 2*border 
- y
 
 725                 self
._windows
[idx
+1].SetDimensions(x
, y
, w
, last
) 
 727                     self
._sashes
[idx
+1] = last
 
 728             if 'wxMSW' in wx
.PlatformInfo
: 
 731         self
._DrawSash
(wx
.ClientDC(self
)) 
 732         self
._needUpdating 
= False 
 735     def _DoSendEvent(self
, evt
): 
 736         return not self
.GetEventHandler().ProcessEvent(evt
) or evt
.IsAllowed() 
 738 #---------------------------------------------------------------------- 
 740 class MultiSplitterEvent(wx
.PyCommandEvent
): 
 742     This event class is almost the same as `wx.SplitterEvent` except 
 743     it adds an accessor for the sash index that is being changed.  The 
 744     same event type IDs and event binders are used as with 
 747     def __init__(self
, type=wx
.wxEVT_NULL
, splitter
=None): 
 748         wx
.PyCommandEvent
.__init
__(self
, type) 
 750             self
.SetEventObject(splitter
) 
 751             self
.SetId(splitter
.GetId()) 
 754         self
.isAllowed 
= True 
 756     def SetSashIdx(self
, idx
): 
 759     def SetSashPosition(self
, pos
): 
 762     def GetSashIdx(self
): 
 765     def GetSashPosition(self
): 
 768     # methods from wx.NotifyEvent 
 770         self
.isAllowed 
= False 
 772         self
.isAllowed 
= True 
 774         return self
.isAllowed
 
 778 #----------------------------------------------------------------------