Source code for enaml.qt.qt_container

#------------------------------------------------------------------------------
#  Copyright (c) 2012, Enthought, Inc.
#  All rights reserved.
#------------------------------------------------------------------------------
from collections import deque

from casuarius import weak
from enaml.layout.layout_manager import LayoutManager

from .qt.QtCore import QSize, Signal
from .qt.QtGui import QFrame
from .qt_constraints_widget import (
    QtConstraintsWidget, LayoutBox, size_hint_guard,
)


def _convert_cn_info(info, owners):
    """ Converts the lhs or rhs of a linear constraint info dict into
    its corresponding casuarius object.

    """
    cn_type = info['type']
    if cn_type == 'linear_expression':
        const = info['constant']
        terms = info['terms']
        convert = _convert_cn_info
        res = sum(convert(t, owners) for t in terms) + const
    elif cn_type == 'term':
        coeff = info['coeff']
        var = info['var']
        res = coeff * _convert_cn_info(var, owners)
    elif cn_type == 'linear_symbolic':
        sym_name = info['name']
        owner_id = info['owner']
        owner = owners.get(owner_id, None)
        if owner is None:
            owner = owners[owner_id] = LayoutBox('_virtual', owner_id)
        res = owner.primitive(sym_name)
    else:
        msg = 'Unhandled constraint info type `%s`' % cn_type
        raise ValueError(msg)
    return res


def as_linear_constraint(info, owners):
    """ Converts a constraint info dict into a casuarius linear
    constraint.

    For constraints specified in the info dict which do not have a
    corresponding owner (e.g. those created by box helpers) a
    constraint variable will be synthesized.

    Parameters
    ----------
    info : dict
        A dictionary sent from an Enaml widget which specifies the
        information for a linear constraint.

    owners : dict
        A mapping from constraint id to an owner object which holds
        the actual casuarius constraint variables as attributes.

    Returns
    -------
    result : LinearConstraint
        A casuarius linear constraint for the given dict.

    """
    if info['type'] != 'linear_constraint':
        msg = 'The info dict does not specify a linear constraint.'
        raise ValueError(msg)
    convert = _convert_cn_info
    lhs = convert(info['lhs'], owners)
    rhs = convert(info['rhs'], owners)
    op = info['op']
    if op == '==':
        cn = lhs == rhs
    elif op == '<=':
        cn = lhs <= rhs
    elif op == '>=':
        cn = lhs >= rhs
    else:
        msg = 'Unhandled constraint operator `%s`' % op
        raise ValueError(msg)
    return cn | info['strength'] | info['weight']


class QContainer(QFrame):
    """ A subclass of QFrame which behaves as a container.

    """
    #: A signal which is emitted on a resize event.
    resized = Signal()

    #: The internally cached size hint.
    _size_hint = QSize()

    def resizeEvent(self, event):
        """ Converts a resize event into a signal.

        """
        super(QContainer, self).resizeEvent(event)
        self.resized.emit()

    def sizeHint(self):
        """ Returns the previously set size hint. If that size hint is
        invalid, the superclass' sizeHint will be used.

        """
        hint = self._size_hint
        if not hint.isValid():
            hint = super(QContainer, self).sizeHint()
        return QSize(hint)

    def setSizeHint(self, hint):
        """ Sets the size hint to use for this widget.

        """
        self._size_hint = QSize(hint)

    def minimumSizeHint(self):
        """ Returns the minimum size hint for the widget.

        For a QContainer, the minimum size hint is equivalent to the
        minimum size as computed by the layout manager.

        """
        return self.minimumSize()


[docs]class QtContainer(QtConstraintsWidget): """ A Qt implementation of an Enaml Container. """ #: Whether or not this container should share its layout with a #: parent container. _share_layout = False #: The padding to use when constraining the layout. _padding = (10, 10, 10, 10) #: Whether or not this container owns its layout. A container which #: does not own its layout is not responsible for laying out its #: children on a resize event, and will proxy the call to its owner. _owns_layout = True #: The object which has taken ownership of the layout for this #: container, if any. _layout_owner = None #: The LayoutManager instance to use for solving the layout system #: for this container. _layout_manager = None #: The function to use for refreshing the layout on a resize event. _refresh = lambda *args, **kwargs: None #: The table of offsets to use during a layout pass. _offset_table = [] #: The table of (index, updater) pairs to use during a layout pass. _layout_table = [] #: A dict mapping constraint owner id to associated LayoutBox _cn_owners = {} #: A list of the current contents constraints for the widget. _contents_cns = [] #: A list of the current size hint constraints for the widget. _size_hint_cns = [] #-------------------------------------------------------------------------- # Setup Methods #--------------------------------------------------------------------------
[docs] def create_widget(self, parent, tree): """ Creates the underlying QContainer widget. """ return QContainer(parent)
[docs] def create(self, tree): """ Create and initialize the underlying widget. """ super(QtContainer, self).create(tree) layout = tree['layout'] self._share_layout = layout['share_layout'] self._padding = layout['padding'] # The resized signal is connected directly to the refresh # method to save the overhead of the extra function call. self.widget().resized.connect(self.refresh)
[docs] def init_layout(self): """ Initializes the layout for the container. """ super(QtContainer, self).init_layout() # Layout ownership can only be transferred *after* this init # layout method is called, since layout occurs bottom up. So, # we only initialize a layout manager if we are not going to # transfer ownership at some point. if not self.will_transfer(): offset_table, layout_table = self._build_layout_table() cns = self._generate_constraints(layout_table) # Initializing the layout manager can fail if the objective # function is unbounded. We let that failure occur so it can # be logged. Nothing is stored until it succeeds. manager = LayoutManager() manager.initialize(cns) self._offset_table = offset_table self._layout_table = layout_table self._layout_manager = manager self._refresh = self._build_refresher(manager) self.refresh_sizes() #-------------------------------------------------------------------------- # Public Layout Handling #--------------------------------------------------------------------------
[docs] def relayout(self): """ Rebuilds the constraints layout for this widget if it owns the responsibility for laying out its descendents. """ if self._owns_layout: item = self.widget_item() old_hint = item.sizeHint() self.init_layout() self.refresh() new_hint = item.sizeHint() # If the size hint constraints are empty, it indicates that # they were previously cleared. In this case, the layout # system must be notified to rebuild its constraints, even # if the numeric size hint hasn't changed. if old_hint != new_hint or not self._size_hint_cns: self.size_hint_updated() else: self._layout_owner.relayout()
[docs] def refresh(self): """ Makes a layout pass over the descendents if this widget owns the responsibility for their layout. """ # The _refresh function is generated on every relayout and has # already taken into account whether or not the container owns # the layout. self._refresh()
[docs] def refresh_sizes(self): """ Refresh the min/max/best sizes for the underlying widget. This method is normally called automatically at the proper times. It should not normally need to be called by user code. """ widget = self.widget() widget.setSizeHint(self.compute_best_size()) widget.setMinimumSize(self.compute_min_size()) widget.setMaximumSize(self.compute_max_size())
[docs] def replace_constraints(self, old_cns, new_cns): """ Replace constraints in the given layout. This method can be used to selectively add/remove/replace constraints in the layout system, when it is more efficient than performing a full relayout. Parameters ---------- old_cns : list The list of casuarius constraints to remove from the the current layout system. new_cns : list The list of casuarius constraints to add to the current layout system. """ if self._owns_layout: manager = self._layout_manager if manager is not None: with size_hint_guard(self): manager.replace_constraints(old_cns, new_cns) self.refresh_sizes() self.refresh() else: self._layout_owner.replace_constraints(old_cns, new_cns)
[docs] def clear_constraints(self, cns): """ Clear the given constraints from the current layout. Parameters ---------- cns : list The list of casuarius constraints to remove from the current layout system. """ if self._owns_layout: manager = self._layout_manager if manager is not None: manager.replace_constraints(cns, []) else: self._layout_owner.clear_constraints(cns)
[docs] def layout(self): """ The callback invoked by the layout manager when there are new layout values available. This iterates over the layout table and calls the geometry updater functions. """ # We explicitly don't use enumerate() to generate the running # index because this method is on the code path of the resize # event and hence called *often*. The entire code path for a # resize event is micro optimized and justified with profiling. offset_table = self._offset_table layout_table = self._layout_table running_index = 1 for offset_index, updater in layout_table: dx, dy = offset_table[offset_index] new_offset = updater(dx, dy) offset_table[running_index] = new_offset running_index += 1
[docs] def contents_margins(self): """ Get the contents margins for the container. The contents margins are added to the user provided padding to determine the final offset from a layout box boundary to the corresponding content line. The default content margins are zero. This method can be reimplemented by subclasses to supply different margins. Returns ------- result : tuple A tuple of 'top', 'right', 'bottom', 'left' contents margins to use for computing the contents constraints. """ return (0, 0, 0, 0)
[docs] def contents_margins_updated(self): """ Notify the layout system that the contents margins of this widget have been updated. """ old_cns = self._contents_cns self._contents_cns = [] new_cns = self.contents_constraints() self.replace_constraints(old_cns, new_cns)
[docs] def contents_constraints(self): """ Create the contents constraints for the container. The contents contraints are generated by combining the user padding with the margins returned by 'contents_margins' method. Returns ------- result : list The list of casuarius constraints for the content. """ cns = self._contents_cns if not cns: padding = self._padding margins = self.contents_margins() tval, rval, bval, lval = map(sum, zip(padding, margins)) primitive = self.layout_box.primitive top = primitive('top') left = primitive('left') width = primitive('width') height = primitive('height') contents_top = primitive('contents_top') contents_left = primitive('contents_left') contents_right = primitive('contents_right') contents_bottom = primitive('contents_bottom') cns = [ contents_top == (top + tval), contents_left == (left + lval), contents_right == (left + width - rval), contents_bottom == (top + height - bval), ] self._contents_cns = cns return cns #-------------------------------------------------------------------------- # Private Layout Handling #--------------------------------------------------------------------------
def _build_refresher(self, manager): """ A private method which will build a function which, when called, will refresh the layout for the container. Parameters ---------- manager : LayoutManager The layout manager to use when refreshing the layout. """ # The return function is a hyper optimized (for Python) closure # in order minimize the amount of work which is performed on the # code path of the resize event. This is explicitly not idiomatic # Python code. It exists purely for the sake of efficiency, # justified with profiling. mgr_layout = manager.layout layout = self.layout primitive = self.layout_box.primitive width_var = primitive('width') height_var = primitive('height') widget = self._widget width = widget.width height = widget.height def refresher(): mgr_layout(layout, width_var, height_var, (width(), height())) return refresher def _build_layout_table(self): """ A private method which will build the layout table for this container. A layout table is a pair of flat lists which hold the required objects for laying out the child widgets of this container. The flat table is built in advance (and rebuilt if and when the tree structure changes) so that it's not necessary to perform an expensive tree traversal to layout the children on every resize event. Returns ------- result : (list, list) The offset table and layout table to use during a resize event. """ # The offset table is a list of (dx, dy) tuples which are the # x, y offsets of children expressed in the coordinates of the # layout owner container. This owner container may be different # from the parent of the widget, and so the delta offset must # be subtracted from the computed geometry values during layout. # The offset table is updated during a layout pass in breadth # first order. # # The layout table is a flat list of (idx, updater) tuples. The # idx is an index into the offset table where the given child # can find the offset to use for its layout. The updater is a # callable provided by the widget which accepts the dx, dy # offset and will update the layout geometry of the widget. zero_offset = (0, 0) offset_table = [zero_offset] layout_table = [] queue = deque((0, child) for child in self.children()) # Micro-optimization: pre-fetch bound methods and store globals # as locals. This method is not on the code path of a resize # event, but it is on the code path of a relayout. If there # are many children, the queue could potentially grow large. push_offset = offset_table.append push_item = layout_table.append push = queue.append pop = queue.popleft QtConstraintsWidget_ = QtConstraintsWidget QtContainer_ = QtContainer isinst = isinstance # The queue yields the items in the tree in breadth-first order # starting with the immediate children of this container. If a # given child is a container that will share its layout, then # the children of that container are added to the queue to be # added to the layout table. running_index = 0 while queue: offset_index, item = pop() if isinst(item, QtConstraintsWidget_): push_item((offset_index, item.geometry_updater())) push_offset(zero_offset) running_index += 1 if isinst(item, QtContainer_): if item.transfer_layout_ownership(self): for child in item.children(): push((running_index, child)) return offset_table, layout_table def _generate_constraints(self, layout_table): """ Creates the list of casuarius LinearConstraint objects for the widgets for which this container owns the layout. This method walks over the items in the given layout table and aggregates their constraints into a single list of casuarius LinearConstraint objects which can be given to the layout manager. Parameters ---------- layout_table : list The layout table created by a call to _build_layout_table. Returns ------- result : list The list of casuarius LinearConstraints instances to pass to the layout manager. """ # The mapping of constraint owners and the list of constraint # info dictionaries provided by the Enaml widgets. box = self.layout_box cn_owners = {self.object_id(): box} cn_dicts = list(self.user_constraints()) cn_dicts_extend = cn_dicts.extend # The list of raw casuarius constraints which will be returned # from this method to be added to the casuarius solver. raw_cns = self.hard_constraints() + self.contents_constraints() raw_cns_extend = raw_cns.extend # The first element in a layout table item is its offset index # which is not relevant to constraints generation. isinst = isinstance QtContainer_ = QtContainer for _, updater in layout_table: child = updater.item cn_owners[child.object_id()] = child.layout_box raw_cns_extend(child.hard_constraints()) if isinst(child, QtContainer_): if child.transfer_layout_ownership(self): cn_dicts_extend(child.user_constraints()) raw_cns_extend(child.contents_constraints()) else: raw_cns_extend(child.size_hint_constraints()) else: raw_cns_extend(child.size_hint_constraints()) cn_dicts_extend(child.user_constraints()) # Convert the list of Enaml constraints info dicts to actual # casuarius LinearConstraint objects for the solver. add_cn = raw_cns.append as_cn = as_linear_constraint for info in cn_dicts: add_cn(as_cn(info, cn_owners)) # We keep a strong reference to the constraint owners dict, # since it may include instances of LayoutBox which were # created on-the-fly and hold constraint variables which # should not be deleted. self._cn_owners = cn_owners return raw_cns #-------------------------------------------------------------------------- # Auxiliary Methods #--------------------------------------------------------------------------
[docs] def transfer_layout_ownership(self, owner): """ A method which can be called by other components in the hierarchy to gain ownership responsibility for the layout of the children of this container. By default, the transfer is allowed and is the mechanism which allows constraints to cross widget boundaries. Subclasses should reimplement this method if different behavior is desired. Parameters ---------- owner : Declarative The component which has taken ownership responsibility for laying out the children of this component. All relayout and refresh requests will be forwarded to this component. Returns ------- results : bool True if the transfer was allowed, False otherwise. """ if not self._share_layout: return False self._owns_layout = False self._layout_owner = owner self._layout_manager = None self._refresh = owner.refresh self._offset_table = [] self._layout_table = [] self._cn_owners = {} return True
[docs] def will_transfer(self): """ Whether or not the container expects to transfer its layout ownership to its parent. This method is predictive in nature and exists so that layout managers are not senslessly created during the bottom-up layout initialization pass. It is declared public so that subclasses can override the behavior if necessary. """ if self._share_layout: if isinstance(self.parent(), QtContainer): return True return False
[docs] def compute_min_size(self): """ Calculates the minimum size of the container which would allow all constraints to be satisfied. If the container's resist properties have a strength less than 'medium', the returned size will be zero. If the container does not own its layout, the returned size will be invalid. Returns ------- result : QSize A (potentially invalid) QSize which is the minimum size required to satisfy all constraints. """ resist_width, resist_height = self._resist shrink = ('ignore', 'weak') if resist_width in shrink and resist_height in shrink: return QSize(0, 0) if self._owns_layout and self._layout_manager is not None: primitive = self.layout_box.primitive width = primitive('width') height = primitive('height') w, h = self._layout_manager.get_min_size(width, height) if resist_width in shrink: w = 0 if resist_height in shrink: h = 0 return QSize(w, h) return QSize()
[docs] def compute_best_size(self): """ Calculates the best size of the container. The best size of the container is obtained by computing the min size of the layout using a strength which is much weaker than a normal resize. This takes into account the size of any widgets which have their resist clip property set to 'weak' while still allowing the window to be resized smaller by the user. If the container does not own its layout, the returned size will be invalid. Returns ------- result : QSize A (potentially invalid) QSize which is the best size that will satisfy all constraints. """ if self._owns_layout and self._layout_manager is not None: primitive = self.layout_box.primitive width = primitive('width') height = primitive('height') w, h = self._layout_manager.get_min_size(width, height, weak) return QSize(w, h) return QSize()
[docs] def compute_max_size(self): """ Calculates the maximum size of the container which would allow all constraints to be satisfied. If the container's hug properties have a strength less than 'medium', or if the container does not own its layout, the returned size will be the Qt maximum. Returns ------- result : QSize A (potentially invalid) QSize which is the maximum size allowable while still satisfying all constraints. """ hug_width, hug_height = self._hug expanding = ('ignore', 'weak') if hug_width in expanding and hug_height in expanding: return QSize(16777215, 16777215) if self._owns_layout and self._layout_manager is not None: primitive = self.layout_box.primitive width = primitive('width') height = primitive('height') w, h = self._layout_manager.get_max_size(width, height) if w < 0 or hug_width in expanding: w = 16777215 if h < 0 or hug_height in expanding: h = 16777215 return QSize(w, h) return QSize(16777215, 16777215)