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 #----------------------------------------------------------------------