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
:
692 for win
in self
._windows
:
694 if self
._orient
== wx
.HORIZONTAL
:
697 for idx
, spos
in enumerate(self
._sashes
[:-1]):
698 self
._windows
[idx
].SetDimensions(x
, y
, spos
, h
)
700 # last one takes the rest of the space. TODO make this configurable
701 last
= cw
- 2*border
- x
702 self
._windows
[idx
+1].SetDimensions(x
, y
, last
, h
)
704 self
._sashes
[idx
+1] = last
708 for idx
, spos
in enumerate(self
._sashes
[:-1]):
709 self
._windows
[idx
].SetDimensions(x
, y
, w
, spos
)
711 # last one takes the rest of the space. TODO make this configurable
712 last
= ch
- 2*border
- y
713 self
._windows
[idx
+1].SetDimensions(x
, y
, w
, last
)
715 self
._sashes
[idx
+1] = last
716 if 'wxMSW' in wx
.PlatformInfo
:
717 for win
in self
._windows
:
720 self
._DrawSash
(wx
.ClientDC(self
))
721 self
._needUpdating
= False
724 def _DoSendEvent(self
, evt
):
725 return not self
.GetEventHandler().ProcessEvent(evt
) or evt
.IsAllowed()
727 #----------------------------------------------------------------------
729 class MultiSplitterEvent(wx
.PyCommandEvent
):
731 This event class is almost the same as `wx.SplitterEvent` except
732 it adds an accessor for the sash index that is being changed. The
733 same event type IDs and event binders are used as with
736 def __init__(self
, type=wx
.wxEVT_NULL
, splitter
=None):
737 wx
.PyCommandEvent
.__init
__(self
, type)
739 self
.SetEventObject(splitter
)
740 self
.SetId(splitter
.GetId())
743 self
.isAllowed
= True
745 def SetSashIdx(self
, idx
):
748 def SetSashPosition(self
, pos
):
751 def GetSashIdx(self
):
754 def GetSashPosition(self
):
757 # methods from wx.NotifyEvent
759 self
.isAllowed
= False
761 self
.isAllowed
= True
763 return self
.isAllowed
767 #----------------------------------------------------------------------