Source code for enaml.wx.wx_splitter

#------------------------------------------------------------------------------
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#------------------------------------------------------------------------------
import wx
from wx.lib.splitter import MultiSplitterWindow

from .wx_constraints_widget import WxConstraintsWidget
from .wx_split_item import WxSplitItem


_ORIENTATION_MAP = {
    'horizontal': wx.HORIZONTAL,
    'vertical': wx.VERTICAL,
}


class wxSplitter(MultiSplitterWindow):
    """ A wx.lib.splitter.MultiSplitterWindow subclass that changes
    the behavior of resizing neighbors to be consistent with Qt.

    """
    def _OnMouse(self, event):
        """ Overriden parent class mouse event handler which fakes the
        state of the keyboard so that resize behavior is consistent
        between wx and Qt.

        """
        # We modify the mouse event to "fake" like the shift key is
        # always down. This causes the splitter to not adjust its
        # neighbor when dragging the sash. This behavior is consistent
        # with Qt's behavior. This is not *the best* way to handle this,
        # but it's the easiest and quickest at the moment. The proper
        # way would be to reimplement this method in its entirety and
        # allow the adjustNeighbor computation to be based on keyboard
        # state as well as attribute flags.
        #
        # TODO implement this properly (or just rewrite this entire
        # control, because like everything else in Wx, it's crap).
        event.m_shiftDown = True
        return super(wxSplitter, self)._OnMouse(event)

    def _GetWindowMin(self, window):
        """ Overriden parent class method which properly computes the
        window min size.

        """
        size = window.GetEffectiveMinSize()
        if self._orient == wx.HORIZONTAL:
            res = size.GetWidth()
        else:
            res = size.GetHeight()
        return res

    def _GetSashSize(self):
        """ Overridden parent class method to return a proper sash size
        for the custom sash painting.

        """
        return 4

    def _DrawSash(self, dc):
        """ Overridden parent class method which draws a custom sash.

        On Windows, the default themed sash drawing causes the sash to
        not be visible; this method corrects that problem and draws a
        sash which is visibly similar to Enaml's Qt Windows version.

        """
        sash_size = self._GetSashSize()
        width, height = self.GetClientSize()
        light_pen = wx.WHITE_PEN
        dark_pen = wx.GREY_PEN
        brush = wx.Brush(self.GetBackgroundColour())
        if self._orient == wx.HORIZONTAL:
            pos = 0
            for sash in self._sashes[:-1]:
                pos += sash
                dc.SetPen(wx.TRANSPARENT_PEN)
                dc.SetBrush(brush)
                dc.DrawRectangle(pos, 0, sash_size, height)
                dc.SetPen(light_pen)
                dc.DrawLine(pos + 1, 0, pos + 1, height)
                dc.SetPen(dark_pen)
                dc.DrawLine(pos + 2, 0, pos + 2, height)
                pos += sash_size
        else:
            pos = 0
            for sash in self._sashes[:-1]:
                pos += sash
                dc.SetPen(wx.TRANSPARENT_PEN)
                dc.SetBrush(brush)
                dc.DrawRectangle(0, pos, width, sash_size)
                dc.SetPen(light_pen)
                dc.DrawLine(0, pos + 1, width, pos + 1)
                dc.SetPen(dark_pen)
                dc.DrawLine(0, pos + 2, width, pos + 2)
                pos += sash_size

    def _OnSize(self, event):
        """ Overridden parent class method which resizes the sashes.

        The default Wx behavior allocates all extra space to the last
        split item, and it will clip the items when the window size is
        reduced. This override uses a weighted algorithm to allocate
        the free space among the items and will not allow the items
        to be clipped by a window resize.

        """
        # Pre-fetch some commonly used objects
        get_min = self._GetWindowMin
        windows = self._windows
        sashes = self._sashes

        # Compute the total space available for the sashes
        sash_widths = self._GetSashSize() * (len(windows) - 1)
        offset = sash_widths + 2 * self._GetBorderSize()
        if self._orient == wx.HORIZONTAL:
            free_space = self.GetClientSize().GetWidth() - offset
        else:
            free_space = self.GetClientSize().GetHeight() - offset

        # Compute the effective stretch factors for each window. The
        # effective stretch factor is the greater of the current or
        # minimum width of the window, multiplied by the window's
        # stretch factor.
        parts = []
        total_stretch = 0
        for idx, (sash, window) in enumerate(zip(sashes, windows)):
            minw = get_min(window)
            if sash < minw:
                sash = sashes[idx] = minw
            stretch = window.GetStretch() * sash
            parts.append((stretch, idx, minw, window))
            total_stretch += stretch

        # Add (or remove) the extra space by fairly allocating it to
        # each window based on their effective stretch factor.
        diff_space = free_space - sum(sashes)
        for stretch, idx, minw, window in parts:
            if stretch > 0:
                d = diff_space * stretch / total_stretch
                new = max(sashes[idx] + d, minw)
                sashes[idx] = new

        # Since the windows are clipped to their minimum width, it's
        # possible that the current space occupied by the windows will
        # be too large. In that case, the overage is distributed to the
        # windows fairly, based on their relative capacity for shrink.
        curr_space = sum(sashes)
        if curr_space > free_space:
            diffs = []
            total_diff = 0
            for stretch, idx, minw, window in parts:
                diff = sashes[idx] - minw
                if diff > 0:
                    diffs.append((diff, window, idx, minw))
                    total_diff += diff
            remaining = curr_space - free_space
            diffs.sort()
            for diff, window, idx, minw in reversed(diffs):
                delta = remaining * diff / total_diff
                old = sashes[idx]
                new = max(old - delta, minw)
                actual_diff = old - new
                remaining -= actual_diff
                total_diff -= actual_diff
                sashes[idx] = new

        # The superclass handler which will actually perform the layout.
        super(wxSplitter, self)._OnSize(event)


[docs]class WxSplitter(WxConstraintsWidget): """ A Wx implementation of an Enaml Splitter. """ #-------------------------------------------------------------------------- # Setup methods #--------------------------------------------------------------------------
[docs] def create_widget(self, parent, tree): """ Creates the underlying wxSplitter widget. """ return wxSplitter(parent)
[docs] def create(self, tree): """ Create and initialize the splitter control. """ super(WxSplitter, self).create(tree) self.set_orientation(tree['orientation']) self.set_live_drag(tree['live_drag'])
[docs] def init_layout(self): """ Handle the layout initialization for the splitter. """ super(WxSplitter, self).init_layout() widget = self.widget() for child in self.children(): if isinstance(child, WxSplitItem): widget.AppendWindow(child.widget()) #-------------------------------------------------------------------------- # Child Events #--------------------------------------------------------------------------
[docs] def child_removed(self, child): """ Handle the child removed event for a WxSplitter. """ if isinstance(child, WxSplitItem): widget = child.widget() self.widget().DetachWindow(widget) widget.Hide() self.size_hint_updated()
[docs] def child_added(self, child): """ Handle the child added event for a WxSplitter. """ if isinstance(child, WxSplitItem): index = self.index_of(child) if index != -1: self.widget().InsertWindow(index, child.widget()) self.size_hint_updated() #-------------------------------------------------------------------------- # Message Handler Methods #--------------------------------------------------------------------------
[docs] def on_action_set_orientation(self, content): """ Handle the 'set_orientation' action from the Enaml widget. """ self.set_orientation(content['orientation']) self.size_hint_updated()
[docs] def on_action_set_live_drag(self, content): """ Handle the 'set_live_drag' action from the Enaml widget. """ self.set_live_drag(content['live_drag']) #-------------------------------------------------------------------------- # Widget Update Methods #--------------------------------------------------------------------------
[docs] def set_orientation(self, orientation): """ Update the orientation of the splitter. """ wx_orientation = _ORIENTATION_MAP[orientation] widget = self.widget() widget.SetOrientation(wx_orientation) widget.SizeWindows()
[docs] def set_live_drag(self, live_drag): """ Updates the drag state of the splitter. """ widget = self.widget() if live_drag: widget.WindowStyle |= wx.SP_LIVE_UPDATE else: widget.WindowStyle &= ~wx.SP_LIVE_UPDATE