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 SizeWindows(self
):
214 Reposition and size the windows managed by the splitter.
215 Useful when windows have been added/removed or when styles
221 def DoGetBestSize(self
):
223 Overridden base class virtual. Determines the best size of
224 the control based on the best sizes of the child windows.
227 if not self
._windows
:
228 best
= wx
.Size(10,10)
230 sashsize
= self
._GetSashSize
()
231 if self
._orient
== wx
.HORIZONTAL
:
232 for win
in self
._windows
:
233 winbest
= win
.GetAdjustedBestSize()
234 best
.width
+= max(self
._minimumPaneSize
, winbest
.width
)
235 best
.height
= max(best
.height
, winbest
.height
)
236 best
.width
+= sashsize
* (len(self
._windows
)-1)
239 for win
in self
._windows
:
240 winbest
= win
.GetAdjustedBestSize()
241 best
.height
+= max(self
._minimumPaneSize
, winbest
.height
)
242 best
.width
= max(best
.width
, winbest
.width
)
243 best
.height
+= sashsize
* (len(self
._windows
)-1)
245 border
= 2 * self
._GetBorderSize
()
247 best
.height
+= border
250 # -------------------------------------
253 def _OnPaint(self
, evt
):
254 dc
= wx
.PaintDC(self
)
258 def _OnSize(self
, evt
):
259 parent
= wx
.GetTopLevelParent(self
)
260 if parent
.IsIconized():
266 def _OnIdle(self
, evt
):
268 # if this is the first idle time after a sash position has
269 # potentially been set, allow _SizeWindows to check for a
271 if not self
._checkRequestedSashPosition
:
272 self
._checkRequestedSashPosition
= True
275 if self
._needUpdating
:
280 def _OnMouse(self
, evt
):
281 if self
.HasFlag(wx
.SP_NOSASH
):
284 x
, y
= evt
.GetPosition()
285 isLive
= self
.HasFlag(wx
.SP_LIVE_UPDATE
)
286 adjustNeighbor
= evt
.ShiftDown()
288 # LeftDown: set things up for dragging the sash
289 if evt
.LeftDown() and self
._SashHitTest
(x
, y
) != -1:
290 self
._activeSash
= self
._SashHitTest
(x
, y
)
291 self
._dragMode
= wx
.SPLIT_DRAG_DRAGGING
294 self
._SetResizeCursor
()
297 self
._pendingPos
= (self
._sashes
[self
._activeSash
],
298 self
._sashes
[self
._activeSash
+1])
299 self
._DrawSashTracker
(x
, y
)
305 # LeftUp: Finsish the drag
306 elif evt
.LeftUp() and self
._dragMode
== wx
.SPLIT_DRAG_DRAGGING
:
307 self
._dragMode
= wx
.SPLIT_DRAG_NONE
309 self
.SetCursor(wx
.STANDARD_CURSOR
)
312 # erase the old tracker
313 self
._DrawSashTracker
(self
._oldX
, self
._oldY
)
315 diff
= self
._GetMotionDiff
(x
, y
)
317 # determine if we can change the position
319 oldPos1
, oldPos2
= (self
._sashes
[self
._activeSash
],
320 self
._sashes
[self
._activeSash
+1])
322 oldPos1
, oldPos2
= self
._pendingPos
323 newPos1
, newPos2
= self
._OnSashPositionChanging
(self
._activeSash
,
328 # the change was not allowed
331 # TODO: check for unsplit?
333 self
._SetSashPositionAndNotify
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
)
334 self
._activeSash
= -1
335 self
._pendingPos
= (-1, -1)
338 # Entering or Leaving a sash: Change the cursor
339 elif (evt
.Moving() or evt
.Leaving() or evt
.Entering()) and self
._dragMode
== wx
.SPLIT_DRAG_NONE
:
340 if evt
.Leaving() or self
._SashHitTest
(x
, y
) == -1:
346 elif evt
.Dragging() and self
._dragMode
== wx
.SPLIT_DRAG_DRAGGING
:
347 diff
= self
._GetMotionDiff
(x
, y
)
349 return # mouse didn't move far enough
351 # determine if we can change the position
353 oldPos1
, oldPos2
= (self
._sashes
[self
._activeSash
],
354 self
._sashes
[self
._activeSash
+1])
356 oldPos1
, oldPos2
= self
._pendingPos
357 newPos1
, newPos2
= self
._OnSashPositionChanging
(self
._activeSash
,
362 # the change was not allowed
365 if newPos1
== self
._sashes
[self
._activeSash
]:
366 return # nothing was changed
369 # erase the old tracker
370 self
._DrawSashTracker
(self
._oldX
, self
._oldY
)
372 if self
._orient
== wx
.HORIZONTAL
:
373 x
= self
._SashToCoord
(self
._activeSash
, newPos1
)
375 y
= self
._SashToCoord
(self
._activeSash
, newPos1
)
377 # Remember old positions
383 self
._pendingPos
= (newPos1
, newPos2
)
384 self
._DrawSashTracker
(self
._oldX
, self
._oldY
)
386 self
._DoSetSashPosition
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
)
387 self
._needUpdating
= True
390 # -------------------------------------
393 def _RedrawIfHotSensitive(self
, isHot
):
394 if not wx
.VERSION
>= _RENDER_VER
:
396 if wx
.RendererNative
.Get().GetSplitterParams(self
).isHotSensitive
:
398 dc
= wx
.ClientDC(self
)
402 def _OnEnterSash(self
):
403 self
._SetResizeCursor
()
404 self
._RedrawIfHotSensitive
(True)
407 def _OnLeaveSash(self
):
408 self
.SetCursor(wx
.STANDARD_CURSOR
)
409 self
._RedrawIfHotSensitive
(False)
412 def _SetResizeCursor(self
):
413 if self
._orient
== wx
.HORIZONTAL
:
414 self
.SetCursor(self
._sashCursorWE
)
416 self
.SetCursor(self
._sashCursorNS
)
419 def _OnSashPositionChanging(self
, idx
, newPos1
, newPos2
, adjustNeighbor
):
420 # TODO: check for possibility of unsplit (pane size becomes zero)
422 # make sure that minsizes are honored
423 newPos1
, newPos2
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
)
431 evt
= MultiSplitterEvent(
432 wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGING
, self
)
434 evt
.SetSashPosition(newPos1
)
435 if not self
._DoSendEvent
(evt
):
436 # the event handler vetoed the change
439 # or it might have changed the value
440 newPos1
= evt
.GetSashPosition()
442 if adjustNeighbor
and newPos1
!= -1:
443 evt
.SetSashIdx(idx
+1)
444 evt
.SetSashPosition(newPos2
)
445 if not self
._DoSendEvent
(evt
):
446 # the event handler vetoed the change
449 # or it might have changed the value
450 newPos2
= evt
.GetSashPosition()
454 return (newPos1
, newPos2
)
457 def _AdjustSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False):
458 total
= newPos1
+ newPos2
460 # these are the windows on either side of the sash
461 win1
= self
._windows
[idx
]
462 win2
= self
._windows
[idx
+1]
464 # make adjustments for window min sizes
465 minSize
= self
._GetWindowMin
(win1
)
466 if minSize
== -1 or self
._minimumPaneSize
> minSize
:
467 minSize
= self
._minimumPaneSize
468 minSize
+= self
._GetBorderSize
()
469 if newPos1
< minSize
:
471 newPos2
= total
- newPos1
474 minSize
= self
._GetWindowMin
(win2
)
475 if minSize
== -1 or self
._minimumPaneSize
> minSize
:
476 minSize
= self
._minimumPaneSize
477 minSize
+= self
._GetBorderSize
()
478 if newPos2
< minSize
:
480 newPos1
= total
- newPos2
482 return (newPos1
, newPos2
)
485 def _DoSetSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False):
486 newPos1
, newPos2
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
)
487 if newPos1
== self
._sashes
[idx
]:
489 self
._sashes
[idx
] = newPos1
491 self
._sashes
[idx
+1] = newPos2
495 def _SetSashPositionAndNotify(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False):
496 # TODO: what is the thing about _requestedSashPosition for?
498 self
._DoSetSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
)
500 evt
= MultiSplitterEvent(
501 wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGED
, self
)
503 evt
.SetSashPosition(newPos1
)
504 self
._DoSendEvent
(evt
)
507 evt
.SetSashIdx(idx
+1)
508 evt
.SetSashPosition(newPos2
)
509 self
._DoSendEvent
(evt
)
512 def _GetMotionDiff(self
, x
, y
):
513 # find the diff from the old pos
514 if self
._orient
== wx
.HORIZONTAL
:
515 diff
= x
- self
._oldX
517 diff
= y
- self
._oldY
521 def _SashToCoord(self
, idx
, sashPos
):
524 coord
+= self
._sashes
[i
]
525 coord
+= self
._GetSashSize
()
530 def _GetWindowMin(self
, window
):
531 if self
._orient
== wx
.HORIZONTAL
:
532 return window
.GetMinWidth()
534 return window
.GetMinHeight()
537 def _GetSashSize(self
):
538 if self
.HasFlag(wx
.SP_NOSASH
):
540 if wx
.VERSION
>= _RENDER_VER
:
541 return wx
.RendererNative
.Get().GetSplitterParams(self
).widthSash
546 def _GetBorderSize(self
):
547 if wx
.VERSION
>= _RENDER_VER
:
548 return wx
.RendererNative
.Get().GetSplitterParams(self
).border
553 def _DrawSash(self
, dc
):
554 if wx
.VERSION
>= _RENDER_VER
:
555 if self
.HasFlag(wx
.SP_3DBORDER
):
556 wx
.RendererNative
.Get().DrawSplitterBorder(
557 self
, dc
, self
.GetClientRect())
559 # if there are no splits then we're done.
560 if len(self
._windows
) < 2:
563 # if we are not supposed to use a sash then we're done.
564 if self
.HasFlag(wx
.SP_NOSASH
):
567 # Reverse the sense of the orientation, in this case it refers
568 # to the direction to draw the sash not the direction that
569 # windows are stacked.
570 orient
= { wx
.HORIZONTAL
: wx
.VERTICAL
,
571 wx
.VERTICAL
: wx
.HORIZONTAL
}[self
._orient
]
575 flag
= wx
.CONTROL_CURRENT
578 for sash
in self
._sashes
[:-1]:
580 if wx
.VERSION
>= _RENDER_VER
:
581 wx
.RendererNative
.Get().DrawSplitterSash(self
, dc
,
582 self
.GetClientSize(),
585 dc
.SetPen(wx
.TRANSPARENT_PEN
)
586 dc
.SetBrush(wx
.Brush(self
.GetBackgroundColour()))
587 sashsize
= self
._GetSashSize
()
588 if orient
== wx
.VERTICAL
:
592 h
= self
.GetClientSize().height
596 w
= self
.GetClientSize().width
598 dc
.DrawRectangle(x
, y
, w
, h
)
600 pos
+= self
._GetSashSize
()
603 def _DrawSashTracker(self
, x
, y
):
604 # Draw a line to represent the dragging sash, for when not
606 w
, h
= self
.GetClientSize()
609 if self
._orient
== wx
.HORIZONTAL
:
632 x1
, y1
= self
.ClientToScreenXY(x1
, y1
)
633 x2
, y2
= self
.ClientToScreenXY(x2
, y2
)
635 dc
.SetLogicalFunction(wx
.INVERT
)
636 dc
.SetPen(self
._sashTrackerPen
)
637 dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
638 dc
.DrawLine(x1
, y1
, x2
, y2
)
639 dc
.SetLogicalFunction(wx
.COPY
)
642 def _SashHitTest(self
, x
, y
, tolerance
=5):
643 # if there are no splits then we're done.
644 if len(self
._windows
) < 2:
647 if self
._orient
== wx
.HORIZONTAL
:
653 for idx
, sash
in enumerate(self
._sashes
[:-1]):
655 hitMin
= pos
- tolerance
656 hitMax
= pos
+ self
._GetSashSize
() + tolerance
658 if z
>= hitMin
and z
<= hitMax
:
661 pos
+= self
._GetSashSize
()
666 def _SizeWindows(self
):
668 if not self
._windows
:
671 # are there any pending size settings?
672 for window
, spos
in self
._pending
.items():
673 idx
= self
._windows
.index(window
)
674 # TODO: this may need adjusted to make sure they all fit
675 # in the current client size
676 self
._sashes
[idx
] = spos
677 del self
._pending
[window
]
679 # are there any that still have a -1?
680 for idx
, spos
in enumerate(self
._sashes
[:-1]):
682 # TODO: this should also be adjusted
683 self
._sashes
[idx
] = 100
685 cw
, ch
= self
.GetClientSize()
686 border
= self
._GetBorderSize
()
687 sash
= self
._GetSashSize
()
689 if len(self
._windows
) == 1:
690 # there's only one, it's an easy layout
691 self
._windows
[0].SetDimensions(border
, border
,
692 cw
- 2*border
, ch
- 2*border
)
694 if 'wxMSW' in wx
.PlatformInfo
:
696 if self
._orient
== wx
.HORIZONTAL
:
699 for idx
, spos
in enumerate(self
._sashes
[:-1]):
700 self
._windows
[idx
].SetDimensions(x
, y
, spos
, h
)
702 # last one takes the rest of the space. TODO make this configurable
703 last
= cw
- 2*border
- x
704 self
._windows
[idx
+1].SetDimensions(x
, y
, last
, h
)
706 self
._sashes
[idx
+1] = last
710 for idx
, spos
in enumerate(self
._sashes
[:-1]):
711 self
._windows
[idx
].SetDimensions(x
, y
, w
, spos
)
713 # last one takes the rest of the space. TODO make this configurable
714 last
= ch
- 2*border
- y
715 self
._windows
[idx
+1].SetDimensions(x
, y
, w
, last
)
717 self
._sashes
[idx
+1] = last
718 if 'wxMSW' in wx
.PlatformInfo
:
721 self
._DrawSash
(wx
.ClientDC(self
))
722 self
._needUpdating
= False
725 def _DoSendEvent(self
, evt
):
726 return not self
.GetEventHandler().ProcessEvent(evt
) or evt
.IsAllowed()
728 #----------------------------------------------------------------------
730 class MultiSplitterEvent(wx
.PyCommandEvent
):
732 This event class is almost the same as `wx.SplitterEvent` except
733 it adds an accessor for the sash index that is being changed. The
734 same event type IDs and event binders are used as with
737 def __init__(self
, type=wx
.wxEVT_NULL
, splitter
=None):
738 wx
.PyCommandEvent
.__init
__(self
, type)
740 self
.SetEventObject(splitter
)
741 self
.SetId(splitter
.GetId())
744 self
.isAllowed
= True
746 def SetSashIdx(self
, idx
):
749 def SetSashPosition(self
, pos
):
752 def GetSashIdx(self
):
755 def GetSashPosition(self
):
758 # methods from wx.NotifyEvent
760 self
.isAllowed
= False
762 self
.isAllowed
= True
764 return self
.isAllowed
768 #----------------------------------------------------------------------