#------------------------------------------------------------------------------
# Copyright (c) 2013-2018, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
#------------------------------------------------------------------------------
from collections import deque
import sys
import warnings
from atom.api import Atom, Int, Bool, Coerced, Enum, List, Str
from enaml.nodevisitor import NodeVisitor
from .geometry import Rect
def _coerce_rect(value):
""" Coerce a value to a Rect object.
This function is a private implementation detail.
"""
if isinstance(value, (list, tuple)):
return Rect(*value)
msg = "cannot coerce '%s' to a 'Rect'"
raise TypeError(msg % type(value).__name__)
[docs]
class LayoutNode(Atom):
""" A base class for defining layout nodes.
This class provides basic traversal functionality.
"""
[docs]
def children(self):
""" Get the children of the node.
Returns
-------
result : list
The list of LayoutNode children of the node. The default
implementation returns an empty list.
"""
return []
[docs]
def traverse(self, depth_first=False):
""" Yield all of the nodes in the layout, from this node down.
Parameters
----------
depth_first : bool, optional
If True, yield the nodes in depth first order. If False,
yield the nodes in breadth first order. Defaults to False.
Returns
-------
result : generator
A generator which yields 2-tuples of (parent, node) for all
nodes in the layout.
"""
if depth_first:
stack = [(None, self)]
stack_pop = stack.pop
stack_extend = stack.extend
else:
stack = deque([(None, self)])
stack_pop = stack.popleft
stack_extend = stack.extend
while stack:
parent, node = stack_pop()
yield parent, node
stack_extend((node, child) for child in node.children())
[docs]
def find(self, kind):
""" Find the first layout node of the given kind.
Parameters
----------
kind : type or tuple of types
The type of the layout node to find.
Returns
-------
result : LayoutNode or None
The first layout node of the given type in the tree. The
search is performed breadth-first.
"""
for parent, node in self.traverse():
if isinstance(node, kind):
return node
[docs]
def find_all(self, kind):
""" Find the layout nodes of the given kind.
Parameters
----------
kind : type or tuple of types
The type of the layout nodes to find.
Returns
-------
result : list
The list of the layout nodes in the tree which are of the
request type. They are ordered breadth-first.
"""
res = []
for parent, node in self.traverse():
if isinstance(node, kind):
res.append(node)
return res
[docs]
class ItemLayout(LayoutNode):
""" A layout object for defining an item layout.
"""
#: The name of the DockItem to which this layout item applies.
name = Str()
#: Whether or not the item is floating. An ItemLayout defined as
#: a toplevel item in a DockLayout should be marked as floating.
floating = Bool(False)
#: The geometry to apply to the item. This is expressed in desktop
#: coordinates and only applies if the item is floating.
geometry = Coerced(Rect, (-1, -1, -1, -1), coercer=_coerce_rect)
#: Whether or not the item is linked with its floating neighbors.
#: This value will only have an effect if the item is floating.
linked = Bool(False)
#: Whether or not the item is maximized. This value will only have
#: effect if the item is floating or docked in a SplitLayout.
maximized = Bool(False)
[docs]
def __init__(self, name, **kwargs):
super(ItemLayout, self).__init__(name=name, **kwargs)
[docs]
class TabLayout(LayoutNode):
""" A layout object for defining tabbed dock layouts.
"""
#: The position of the tabs in the tab layout.
tab_position = Enum('top', 'bottom', 'left', 'right')
#: The index of the currently selected tab.
index = Int(0)
#: Whether or not the tab layout is maximized.
maximized = Bool(False)
#: The list of item layouts to include in the tab layout.
items = List(Coerced(ItemLayout))
[docs]
def __init__(self, *items, **kwargs):
super(TabLayout, self).__init__(items=list(items), **kwargs)
[docs]
def children(self):
""" Get the list of children of the tab layout.
"""
return self.items[:]
class _SplitLayoutItemMeta(type):
def __instancecheck__(cls, instance):
return isinstance(instance, (ItemLayout, TabLayout, SplitLayout))
def __call__(cls, item):
if isinstance(item, str):
return ItemLayout(item)
msg = "cannot coerce '%s' to a 'SplitLayout' item"
raise TypeError(msg % type(item).__name__)
class _SplitLayoutItem(object, metaclass=_SplitLayoutItemMeta):
""" A private class which performs type checking for split layouts.
"""
[docs]
class SplitLayout(LayoutNode):
""" A layout object for defining split dock layouts.
"""
#: The orientation of the split layout.
orientation = Enum('horizontal', 'vertical')
#: The default sizes to apply to the items in the splitter. If
#: provided, the length must be equal to the number of items.
sizes = List(Int())
#: This list of split layout items to include in the split layout.
items = List(Coerced(_SplitLayoutItem))
[docs]
def __init__(self, *items, **kwargs):
super(SplitLayout, self).__init__(items=list(items), **kwargs)
[docs]
def children(self):
""" Get the list of children of the split layout.
"""
return self.items[:]
[docs]
class HSplitLayout(SplitLayout):
""" A split layout which defaults to 'horizonal' orientation.
"""
[docs]
def __init__(self, *items, **kwargs):
kwargs['orientation'] = 'horizontal'
super(HSplitLayout, self).__init__(*items, **kwargs)
[docs]
class VSplitLayout(SplitLayout):
""" A split layout which defaults to 'vertical' orientation.
"""
[docs]
def __init__(self, *items, **kwargs):
kwargs['orientation'] = 'vertical'
super(VSplitLayout, self).__init__(*items, **kwargs)
[docs]
class DockBarLayout(LayoutNode):
""" A layout object for defining a dock bar layout.
"""
#: The position of the tool bar in its area. Only one tool bar may
#: occupy a given position at any one time.
position = Enum('top', 'right', 'bottom', 'left')
#: The list of item layouts to include in the tab layout.
items = List(Coerced(ItemLayout))
[docs]
def __init__(self, *items, **kwargs):
super(DockBarLayout, self).__init__(items=list(items), **kwargs)
[docs]
def children(self):
""" Get the list of children of the dock bar layout.
"""
return self.items[:]
class _AreaLayoutItemMeta(type):
def __instancecheck__(cls, instance):
allowed = (type(None), ItemLayout, TabLayout, SplitLayout)
return isinstance(instance, allowed)
def __call__(cls, item):
if isinstance(item, str):
return ItemLayout(item)
msg = "cannot coerce '%s' to an 'AreaLayout' item"
raise TypeError(msg % type(item).__name__)
class _AreaLayoutItem(object, metaclass=_AreaLayoutItemMeta):
""" A private class which performs type checking for area layouts.
"""
[docs]
class AreaLayout(LayoutNode):
""" A layout object for defining a dock area layout.
"""
#: The main layout item to include in the area layout.
item = Coerced(_AreaLayoutItem)
#: The dock bar layouts to include in the area layout.
dock_bars = List(DockBarLayout)
#: Whether or not the area is floating. A DockLayout should have
#: at most one non-floating area layout.
floating = Bool(False)
#: The geometry to apply to the area. This is expressed in desktop
#: coordinates and only applies if the area is floating.
geometry = Coerced(Rect, (-1, -1, -1, -1), coercer=_coerce_rect)
#: Whether or not the area is linked with its floating neighbors.
#: This only has an effect if the area is a floating.
linked = Bool(False)
#: Whether or not the area is maximized. This only has an effect if
#: the area is a floating.
maximized = Bool(False)
[docs]
def __init__(self, item=None, **kwargs):
super(AreaLayout, self).__init__(item=item, **kwargs)
[docs]
def children(self):
""" Get the list of children of the area layout.
"""
item = self.item
base = [item] if item is not None else []
return base + self.dock_bars
class _DockLayoutItemMeta(type):
def __instancecheck__(cls, instance):
return isinstance(instance, (ItemLayout, AreaLayout))
def __call__(cls, item):
if isinstance(item, str):
return ItemLayout(item)
if isinstance(item, (SplitLayout, TabLayout)):
return AreaLayout(item)
msg = "cannot coerce '%s' to a 'DockLayout' item"
raise TypeError(msg % type(item).__name__)
class _DockLayoutItem(object, metaclass=_DockLayoutItemMeta):
""" A private class which performs type checking for dock layouts.
"""
[docs]
class DockLayout(LayoutNode):
""" The layout object for defining toplevel dock layouts.
"""
#: The layout items to include in the dock layout.
items = List(Coerced(_DockLayoutItem))
[docs]
def __init__(self, *items, **kwargs):
super(DockLayout, self).__init__(items=list(items), **kwargs)
[docs]
def children(self):
""" Get the list of children of the dock layout.
"""
return self.items[:]
[docs]
class DockLayoutWarning(UserWarning):
""" A custom user warning for use with dock layouts.
"""
pass
[docs]
class DockLayoutValidator(NodeVisitor):
""" A node visitor which validates a layout.
If an irregularity or invalid condition is found in the layout, a
warning is emitted. Such conditions can result in undefined layout
behavior.
"""
[docs]
def __init__(self, available):
""" Initialize a DockLayoutValidator.
Parameters
----------
available : iterable
An iterable of strings which represent the available dock
item names onto which the layout will be applied. These are
used to validate the set of visited ItemLayout instances.
"""
self._available = set(available)
[docs]
def warn(self, message):
""" Emit a dock layout warning with the given message.
"""
f_globals = self._caller.f_globals
f_lineno = self._caller.f_lineno
f_mod = f_globals.get('__name__', '<string>')
f_name = f_globals.get('__file__')
if f_name:
if f_name.lower().endswith((".pyc", ".pyo")):
f_name = f_name[:-1]
else:
if f_mod == "__main__":
f_name = sys.argv[0]
if not f_name:
f_name = f_mod
warnings.warn_explicit(
message, DockLayoutWarning, f_name, f_lineno, f_mod, None,
f_globals
)
[docs]
def setup(self, node):
""" Setup the dock layout validator.
"""
self._caller = sys._getframe(2)
self._seen_items = set()
self._cant_maximize = {}
[docs]
def teardown(self, node):
""" Teardown the dock layout validator.
"""
for name in self._available - self._seen_items:
msg = "item '%s' is not referenced by the layout"
self.warn(msg % name)
for name in self._seen_items - self._available:
msg = "item '%s' is not an available layout item"
self.warn(msg % name)
del self._caller
del self._seen_items
del self._cant_maximize
[docs]
def visit_ItemLayout(self, node):
""" The visitor method for an ItemLayout node.
"""
if node.name in self._seen_items:
self.warn("duplicate use of ItemLayout name '%s'" % node.name)
self._seen_items.add(node.name)
if not node.floating:
if -1 not in node.geometry:
self.warn("non-floating ItemLayout with specific geometry")
if node.linked:
self.warn("non-floating ItemLayout marked as linked")
if node.maximized and node in self._cant_maximize:
msg = "ItemLayout contained in %s marked as maximized"
self.warn(msg % self._cant_maximize[node])
[docs]
def visit_TabLayout(self, node):
""" The visitor method for a TabLayout node.
"""
for item in node.items:
self._cant_maximize[item] = 'TabLayout'
self.visit(item)
[docs]
def visit_SplitLayout(self, node):
""" The visitor method for a SplitLayout node.
"""
if len(node.sizes) > 0:
if len(node.sizes) != len(node.items):
self.warn("SplitLayout sizes length != items length")
for item in node.items:
if isinstance(item, SplitLayout):
if item.orientation == node.orientation:
msg = "child SplitLayout has same orientation as parent"
self.warn(msg)
self.visit(item)
[docs]
def visit_DockBarLayout(self, node):
""" The visitor method for a DockBarLayout node.
"""
for item in node.items:
self._cant_maximize[item] = 'DockBarLayout'
self.visit(item)
[docs]
def visit_AreaLayout(self, node):
""" The visitor method for an AreaLayout node.
"""
if not node.floating:
if -1 not in node.geometry:
self.warn("non-floating AreaLayout with specific geometry")
if node.linked:
self.warn("non-floating AreaLayout marked as linked")
if node.maximized:
self.warn("non-floating AreaLayout marked as maximized")
if node.item is not None:
self.visit(node.item)
seen_positions = set()
for bar in node.dock_bars:
if bar.position in seen_positions:
msg = "multiple DockBarLayout items in '%s' position"
self.warn(msg % bar.position)
seen_positions.add(bar.position)
self.visit(bar)
[docs]
def visit_DockLayout(self, node):
""" The visitor method for a DockLayout node.
"""
has_non_floating_area = False
for item in node.items:
if isinstance(item, ItemLayout):
if not item.floating:
self.warn("non-floating toplevel ItemLayout")
else: # must be an AreaLayout
if not item.floating:
if has_non_floating_area:
self.warn("multiple non-floating AreaLayout items")
has_non_floating_area = True
self.visit(item)
#------------------------------------------------------------------------------
# Dock Layout Operations
#------------------------------------------------------------------------------
[docs]
class DockLayoutOp(Atom):
""" A sentinel base class for defining dock layout operations.
"""
pass
[docs]
class InsertItem(DockLayoutOp):
""" A layout operation which inserts an item into a layout.
This operation will remove an item from the current layout and
insert it next to a target item. If the item does not exist, the
operation is a no-op.
If the target -
- is a normally docked item
The item will be inserted as a new split item.
- is docked in a tab group
The item will be inserted as a neighbor of the tab group.
- is docked in a dock bar
The item will be appended to the dock bar.
- is a floating dock item
A new dock area will be created and the item will be inserted
as a new split item.
- does not exist
The item is inserted into the border of the primary dock area.
"""
#: The name of the dock item to insert into the layout.
item = Str()
#: The name of the dock item to use as the target location.
target = Str()
#: The position relative to the target at which to insert the item.
position = Enum('left', 'top', 'right', 'bottom')
[docs]
class InsertBorderItem(DockLayoutOp):
""" A layout operation which inserts an item into an area border.
This operation will remove an item from the current layout and
insert it into the border of a dock area. If the item does not
exist, the operation is a no-op.
If the target -
- is a normally docked item
The item is inserted into the border of the dock area containing
the target.
- is docked in a tab group
The item is inserted into the border of the dock area containing
the tab group.
- is docked in a dock bar
The item is inserted into the border of the dock area containing
the dock bar.
- is a floating dock item
A new dock area will be created and the item will be inserted
into the border of the new dock area.
- does not exist
The item is inserted into the border of the primary dock area.
"""
#: The name of the dock item to insert into the layout.
item = Str()
#: The name of the dock item to use as the target location.
target = Str()
#: The border position at which to insert the item.
position = Enum('left', 'top', 'right', 'bottom')
[docs]
class InsertDockBarItem(DockLayoutOp):
""" A layout operation which inserts an item into a dock bar.
This operation will remove an item from the current layout and
insert it into a dock bar in a dock area. If the item does not
exist, the operation is a no-op.
If the target -
- is a normally docked item
The item is inserted into the dock bar of the dock area
containing the target.
- is docked in a tab group
The item is inserted into the dock bar of the dock area
containing the tab group.
- is docked in a dock bar
The item is inserted into the dock bar of the dock area
containing the dock bar.
- is a floating dock item
A new dock area will be created and the item will be inserted
into the dock bar of the new dock area.
- does not exist
The item is inserted into the dock bar of the primary dock
area.
"""
#: The name of the dock item to insert into the layout.
item = Str()
#: The name of the dock item to use as the target location.
target = Str()
#: The dock bar position at which to insert the item.
position = Enum('right', 'left', 'bottom', 'top')
#: The index at which to insert the dock bar item.
index = Int(-1)
[docs]
class InsertTab(DockLayoutOp):
""" A layout operation which inserts a tab into a tab group.
This operation will remove an item from the current layout and
insert it into a tab group in a dock area. If the item does not
exist, the operation is a no-op.
If the target -
- is a normally docked item
The target and item will be merged into a new tab group
using the default tab position.
- is docked in a tab group
The item will be inserted into the tab group.
- is docked in a dock bar
The item will be appended to the dock bar.
- is a floating dock item
A new dock area will be created and the target and item will
be merged into a new tab group.
- does not exist
The item is inserted into the left border of the primary dock
area.
"""
#: The name of the dock item to insert into the tab group.
item = Str()
#: The name of an existing dock item in the tab group of interest.
target = Str()
#: The index at which to insert the dock item.
index = Int(-1)
#: The position of the tabs for a newly created tab group.
tab_position = Enum('default', 'top', 'bottom', 'left', 'right')
[docs]
class FloatItem(DockLayoutOp):
""" A layout operation which creates a floating dock item.
This operation will remove an item from the current layout and
insert convert it into a floating item. If the item does not
exist, the operation is a no-op.
"""
#: The item layout to use when configuring the floating item.
item = Coerced(ItemLayout)
[docs]
class FloatArea(DockLayoutOp):
""" A layout operation which creates a new floating dock area.
This layout operation will create a new floating dock area using
the given area layout specification.
"""
#: The area layout to use when building the new dock area.
area = Coerced(AreaLayout)
[docs]
class RemoveItem(DockLayoutOp):
""" A layout operation which will remove an item from the layout.
This layout operation will remove the dock item from the layout
and hide it. It can be added back to layout later with one of the
other layout operations.
"""
#: The name of the dock item to remove from the layout.
item = Str()
[docs]
class ExtendItem(DockLayoutOp):
""" A layout operation which extends an item in a dock bar.
This layout operation will cause the named item to be extended to
from its dock bar. If the item does not exist in a dock bar, this
operation is a no-op.
"""
#: The name of the dock item to extend from its dock bar.
item = Str()
[docs]
class RetractItem(DockLayoutOp):
""" A layout operation which retracts an item into a dock bar.
This layout operation will cause the named item to be retracted
into its dock bar. If the item does not exist in a dock bar, this
operation is a no-op.
"""
#: The name of the dock item to retract into its dock bar.
item = Str()