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 managed 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 is 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 at the right side or bottom
134 of the window stack. If sashPos is given then it is used to
137 self
.InsertWindow(sys
.maxint
, window
, sashPos
)
140 def InsertWindow(self
, idx
, window
, sashPos
=-1):
142 Insert a new window into the splitter at the position given in
145 assert window
not in self
._windows
, "A window can only be in the splitter once!"
146 self
._windows
.insert(idx
, window
)
147 self
._sashes
.insert(idx
, -1)
148 if not window
.IsShown():
151 self
._pending
[window
] = sashPos
152 self
._checkRequestedSashPosition
= False
156 def DetachWindow(self
, window
):
158 Removes the window from the stack of windows managed by the
159 splitter. The window will still exist so you should `Hide` or
160 `Destroy` it as needed.
162 assert window
in self
._windows
, "Unknown window!"
163 idx
= self
._windows
.index(window
)
164 del self
._windows
[idx
]
165 del self
._sashes
[idx
]
169 def ReplaceWindow(self
, oldWindow
, newWindow
):
171 Replaces oldWindow (which is currently being managed by the
172 splitter) with newWindow. The oldWindow window will still
173 exist so you should `Hide` or `Destroy` it as needed.
175 assert oldWindow
in self
._windows
, "Unknown window!"
176 idx
= self
._windows
.index(oldWindow
)
177 self
._windows
[idx
] = newWindow
178 if not newWindow
.IsShown():
183 def ExchangeWindows(self
, window1
, window2
):
185 Trade the positions in the splitter of the two windows.
187 assert window1
in self
._windows
, "Unknown window!"
188 assert window2
in self
._windows
, "Unknown window!"
189 idx1
= self
._windows
.index(window1
)
190 idx2
= self
._windows
.index(window2
)
191 self
._windows
[idx1
] = window2
192 self
._windows
[idx2
] = window1
196 def GetWindow(self
, idx
):
198 Returns the idx'th window being managed by the splitter.
200 assert idx
< len(self
._windows
)
201 return self
._windows
[idx
]
204 def GetSashPosition(self
, idx
):
206 Returns the position of the idx'th sash, measured from the
207 left/top of the window preceding the sash.
209 assert idx
< len(self
._sashes
)
210 return self
._sashes
[idx
]
213 def SizeWindows(self
):
215 Reposition and size the windows managed by the splitter.
216 Useful when windows have been added/removed or when styles
222 def DoGetBestSize(self
):
224 Overridden base class virtual. Determines the best size of
225 the control based on the best sizes of the child windows.
228 if not self
._windows
:
229 best
= wx
.Size(10,10)
231 sashsize
= self
._GetSashSize
()
232 if self
._orient
== wx
.HORIZONTAL
:
233 for win
in self
._windows
:
234 winbest
= win
.GetAdjustedBestSize()
235 best
.width
+= max(self
._minimumPaneSize
, winbest
.width
)
236 best
.height
= max(best
.height
, winbest
.height
)
237 best
.width
+= sashsize
* (len(self
._windows
)-1)
240 for win
in self
._windows
:
241 winbest
= win
.GetAdjustedBestSize()
242 best
.height
+= max(self
._minimumPaneSize
, winbest
.height
)
243 best
.width
= max(best
.width
, winbest
.width
)
244 best
.height
+= sashsize
* (len(self
._windows
)-1)
246 border
= 2 * self
._GetBorderSize
()
248 best
.height
+= border
251 # -------------------------------------
254 def _OnPaint(self
, evt
):
255 dc
= wx
.PaintDC(self
)
259 def _OnSize(self
, evt
):
260 parent
= wx
.GetTopLevelParent(self
)
261 if parent
.IsIconized():
267 def _OnIdle(self
, evt
):
269 # if this is the first idle time after a sash position has
270 # potentially been set, allow _SizeWindows to check for a
272 if not self
._checkRequestedSashPosition
:
273 self
._checkRequestedSashPosition
= True
276 if self
._needUpdating
:
281 def _OnMouse(self
, evt
):
282 if self
.HasFlag(wx
.SP_NOSASH
):
285 x
, y
= evt
.GetPosition()
286 isLive
= self
.HasFlag(wx
.SP_LIVE_UPDATE
)
287 adjustNeighbor
= evt
.ShiftDown()
289 # LeftDown: set things up for dragging the sash
290 if evt
.LeftDown() and self
._SashHitTest
(x
, y
) != -1:
291 self
._activeSash
= self
._SashHitTest
(x
, y
)
292 self
._dragMode
= wx
.SPLIT_DRAG_DRAGGING
295 self
._SetResizeCursor
()
298 self
._pendingPos
= (self
._sashes
[self
._activeSash
],
299 self
._sashes
[self
._activeSash
+1])
300 self
._DrawSashTracker
(x
, y
)
306 # LeftUp: Finsish the drag
307 elif evt
.LeftUp() and self
._dragMode
== wx
.SPLIT_DRAG_DRAGGING
:
308 self
._dragMode
= wx
.SPLIT_DRAG_NONE
310 self
.SetCursor(wx
.STANDARD_CURSOR
)
313 # erase the old tracker
314 self
._DrawSashTracker
(self
._oldX
, self
._oldY
)
316 diff
= self
._GetMotionDiff
(x
, y
)
318 # determine if we can change the position
320 oldPos1
, oldPos2
= (self
._sashes
[self
._activeSash
],
321 self
._sashes
[self
._activeSash
+1])
323 oldPos1
, oldPos2
= self
._pendingPos
324 newPos1
, newPos2
= self
._OnSashPositionChanging
(self
._activeSash
,
329 # the change was not allowed
332 # TODO: check for unsplit?
334 self
._SetSashPositionAndNotify
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
)
335 self
._activeSash
= -1
336 self
._pendingPos
= (-1, -1)
339 # Entering or Leaving a sash: Change the cursor
340 elif (evt
.Moving() or evt
.Leaving() or evt
.Entering()) and self
._dragMode
== wx
.SPLIT_DRAG_NONE
:
341 if evt
.Leaving() or self
._SashHitTest
(x
, y
) == -1:
347 elif evt
.Dragging() and self
._dragMode
== wx
.SPLIT_DRAG_DRAGGING
:
348 diff
= self
._GetMotionDiff
(x
, y
)
350 return # mouse didn't move far enough
352 # determine if we can change the position
354 oldPos1
, oldPos2
= (self
._sashes
[self
._activeSash
],
355 self
._sashes
[self
._activeSash
+1])
357 oldPos1
, oldPos2
= self
._pendingPos
358 newPos1
, newPos2
= self
._OnSashPositionChanging
(self
._activeSash
,
363 # the change was not allowed
366 if newPos1
== self
._sashes
[self
._activeSash
]:
367 return # nothing was changed
370 # erase the old tracker
371 self
._DrawSashTracker
(self
._oldX
, self
._oldY
)
373 if self
._orient
== wx
.HORIZONTAL
:
374 x
= self
._SashToCoord
(self
._activeSash
, newPos1
)
376 y
= self
._SashToCoord
(self
._activeSash
, newPos1
)
378 # Remember old positions
384 self
._pendingPos
= (newPos1
, newPos2
)
385 self
._DrawSashTracker
(self
._oldX
, self
._oldY
)
387 self
._DoSetSashPosition
(self
._activeSash
, newPos1
, newPos2
, adjustNeighbor
)
388 self
._needUpdating
= True
391 # -------------------------------------
394 def _RedrawIfHotSensitive(self
, isHot
):
395 if not wx
.VERSION
>= _RENDER_VER
:
397 if wx
.RendererNative
.Get().GetSplitterParams(self
).isHotSensitive
:
399 dc
= wx
.ClientDC(self
)
403 def _OnEnterSash(self
):
404 self
._SetResizeCursor
()
405 self
._RedrawIfHotSensitive
(True)
408 def _OnLeaveSash(self
):
409 self
.SetCursor(wx
.STANDARD_CURSOR
)
410 self
._RedrawIfHotSensitive
(False)
413 def _SetResizeCursor(self
):
414 if self
._orient
== wx
.HORIZONTAL
:
415 self
.SetCursor(self
._sashCursorWE
)
417 self
.SetCursor(self
._sashCursorNS
)
420 def _OnSashPositionChanging(self
, idx
, newPos1
, newPos2
, adjustNeighbor
):
421 # TODO: check for possibility of unsplit (pane size becomes zero)
423 # make sure that minsizes are honored
424 newPos1
, newPos2
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
)
432 evt
= MultiSplitterEvent(
433 wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGING
, self
)
435 evt
.SetSashPosition(newPos1
)
436 if not self
._DoSendEvent
(evt
):
437 # the event handler vetoed the change
440 # or it might have changed the value
441 newPos1
= evt
.GetSashPosition()
443 if adjustNeighbor
and newPos1
!= -1:
444 evt
.SetSashIdx(idx
+1)
445 evt
.SetSashPosition(newPos2
)
446 if not self
._DoSendEvent
(evt
):
447 # the event handler vetoed the change
450 # or it might have changed the value
451 newPos2
= evt
.GetSashPosition()
455 return (newPos1
, newPos2
)
458 def _AdjustSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False):
459 total
= newPos1
+ newPos2
461 # these are the windows on either side of the sash
462 win1
= self
._windows
[idx
]
463 win2
= self
._windows
[idx
+1]
465 # make adjustments for window min sizes
466 minSize
= self
._GetWindowMin
(win1
)
467 if minSize
== -1 or self
._minimumPaneSize
> minSize
:
468 minSize
= self
._minimumPaneSize
469 minSize
+= self
._GetBorderSize
()
470 if newPos1
< minSize
:
472 newPos2
= total
- newPos1
475 minSize
= self
._GetWindowMin
(win2
)
476 if minSize
== -1 or self
._minimumPaneSize
> minSize
:
477 minSize
= self
._minimumPaneSize
478 minSize
+= self
._GetBorderSize
()
479 if newPos2
< minSize
:
481 newPos1
= total
- newPos2
483 return (newPos1
, newPos2
)
486 def _DoSetSashPosition(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False):
487 newPos1
, newPos2
= self
._AdjustSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
)
488 if newPos1
== self
._sashes
[idx
]:
490 self
._sashes
[idx
] = newPos1
492 self
._sashes
[idx
+1] = newPos2
496 def _SetSashPositionAndNotify(self
, idx
, newPos1
, newPos2
=-1, adjustNeighbor
=False):
497 # TODO: what is the thing about _requestedSashPosition for?
499 self
._DoSetSashPosition
(idx
, newPos1
, newPos2
, adjustNeighbor
)
501 evt
= MultiSplitterEvent(
502 wx
.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGED
, self
)
504 evt
.SetSashPosition(newPos1
)
505 self
._DoSendEvent
(evt
)
508 evt
.SetSashIdx(idx
+1)
509 evt
.SetSashPosition(newPos2
)
510 self
._DoSendEvent
(evt
)
513 def _GetMotionDiff(self
, x
, y
):
514 # find the diff from the old pos
515 if self
._orient
== wx
.HORIZONTAL
:
516 diff
= x
- self
._oldX
518 diff
= y
- self
._oldY
522 def _SashToCoord(self
, idx
, sashPos
):
525 coord
+= self
._sashes
[i
]
526 coord
+= self
._GetSashSize
()
531 def _GetWindowMin(self
, window
):
532 if self
._orient
== wx
.HORIZONTAL
:
533 return window
.GetMinWidth()
535 return window
.GetMinHeight()
538 def _GetSashSize(self
):
539 if self
.HasFlag(wx
.SP_NOSASH
):
541 if wx
.VERSION
>= _RENDER_VER
:
542 return wx
.RendererNative
.Get().GetSplitterParams(self
).widthSash
547 def _GetBorderSize(self
):
548 if wx
.VERSION
>= _RENDER_VER
:
549 return wx
.RendererNative
.Get().GetSplitterParams(self
).border
554 def _DrawSash(self
, dc
):
555 if wx
.VERSION
>= _RENDER_VER
:
556 if self
.HasFlag(wx
.SP_3DBORDER
):
557 wx
.RendererNative
.Get().DrawSplitterBorder(
558 self
, dc
, self
.GetClientRect())
560 # if there are no splits then we're done.
561 if len(self
._windows
) < 2:
564 # if we are not supposed to use a sash then we're done.
565 if self
.HasFlag(wx
.SP_NOSASH
):
568 # Reverse the sense of the orientation, in this case it refers
569 # to the direction to draw the sash not the direction that
570 # windows are stacked.
571 orient
= { wx
.HORIZONTAL
: wx
.VERTICAL
,
572 wx
.VERTICAL
: wx
.HORIZONTAL
}[self
._orient
]
576 flag
= wx
.CONTROL_CURRENT
579 for sash
in self
._sashes
[:-1]:
581 if wx
.VERSION
>= _RENDER_VER
:
582 wx
.RendererNative
.Get().DrawSplitterSash(self
, dc
,
583 self
.GetClientSize(),
586 dc
.SetPen(wx
.TRANSPARENT_PEN
)
587 dc
.SetBrush(wx
.Brush(self
.GetBackgroundColour()))
588 sashsize
= self
._GetSashSize
()
589 if orient
== wx
.VERTICAL
:
593 h
= self
.GetClientSize().height
597 w
= self
.GetClientSize().width
599 dc
.DrawRectangle(x
, y
, w
, h
)
601 pos
+= self
._GetSashSize
()
604 def _DrawSashTracker(self
, x
, y
):
605 # Draw a line to represent the dragging sash, for when not
607 w
, h
= self
.GetClientSize()
610 if self
._orient
== wx
.HORIZONTAL
:
633 x1
, y1
= self
.ClientToScreenXY(x1
, y1
)
634 x2
, y2
= self
.ClientToScreenXY(x2
, y2
)
636 dc
.SetLogicalFunction(wx
.INVERT
)
637 dc
.SetPen(self
._sashTrackerPen
)
638 dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
639 dc
.DrawLine(x1
, y1
, x2
, y2
)
640 dc
.SetLogicalFunction(wx
.COPY
)
643 def _SashHitTest(self
, x
, y
, tolerance
=5):
644 # if there are no splits then we're done.
645 if len(self
._windows
) < 2:
648 if self
._orient
== wx
.HORIZONTAL
:
654 for idx
, sash
in enumerate(self
._sashes
[:-1]):
656 hitMin
= pos
- tolerance
657 hitMax
= pos
+ self
._GetSashSize
() + tolerance
659 if z
>= hitMin
and z
<= hitMax
:
662 pos
+= self
._GetSashSize
()
667 def _SizeWindows(self
):
669 if not self
._windows
:
672 # are there any pending size settings?
673 for window
, spos
in self
._pending
.items():
674 idx
= self
._windows
.index(window
)
675 # TODO: this may need adjusted to make sure they all fit
676 # in the current client size
677 self
._sashes
[idx
] = spos
678 del self
._pending
[window
]
680 # are there any that still have a -1?
681 for idx
, spos
in enumerate(self
._sashes
[:-1]):
683 # TODO: this should also be adjusted
684 self
._sashes
[idx
] = 100
686 cw
, ch
= self
.GetClientSize()
687 border
= self
._GetBorderSize
()
688 sash
= self
._GetSashSize
()
690 if len(self
._windows
) == 1:
691 # there's only one, it's an easy layout
692 self
._windows
[0].SetDimensions(border
, border
,
693 cw
- 2*border
, ch
- 2*border
)
695 if 'wxMSW' in wx
.PlatformInfo
:
697 if self
._orient
== wx
.HORIZONTAL
:
700 for idx
, spos
in enumerate(self
._sashes
[:-1]):
701 self
._windows
[idx
].SetDimensions(x
, y
, spos
, h
)
703 # last one takes the rest of the space. TODO make this configurable
704 last
= cw
- 2*border
- x
705 self
._windows
[idx
+1].SetDimensions(x
, y
, last
, h
)
707 self
._sashes
[idx
+1] = last
711 for idx
, spos
in enumerate(self
._sashes
[:-1]):
712 self
._windows
[idx
].SetDimensions(x
, y
, w
, spos
)
714 # last one takes the rest of the space. TODO make this configurable
715 last
= ch
- 2*border
- y
716 self
._windows
[idx
+1].SetDimensions(x
, y
, w
, last
)
718 self
._sashes
[idx
+1] = last
719 if 'wxMSW' in wx
.PlatformInfo
:
722 self
._DrawSash
(wx
.ClientDC(self
))
723 self
._needUpdating
= False
726 def _DoSendEvent(self
, evt
):
727 return not self
.GetEventHandler().ProcessEvent(evt
) or evt
.IsAllowed()
729 #----------------------------------------------------------------------
731 class MultiSplitterEvent(wx
.PyCommandEvent
):
733 This event class is almost the same as `wx.SplitterEvent` except
734 it adds an accessor for the sash index that is being changed. The
735 same event type IDs and event binders are used as with
738 def __init__(self
, type=wx
.wxEVT_NULL
, splitter
=None):
739 wx
.PyCommandEvent
.__init
__(self
, type)
741 self
.SetEventObject(splitter
)
742 self
.SetId(splitter
.GetId())
745 self
.isAllowed
= True
747 def SetSashIdx(self
, idx
):
750 def SetSashPosition(self
, pos
):
753 def GetSashIdx(self
):
756 def GetSashPosition(self
):
759 # methods from wx.NotifyEvent
761 self
.isAllowed
= False
763 self
.isAllowed
= True
765 return self
.isAllowed
769 #----------------------------------------------------------------------