Source code for pylablib.core.gui.widgets.layout_manager
from .. import utils
from ...utils import py3, funcargparse
from .. import QtCore, QtWidgets
import contextlib
[docs]class IQLayoutManagedWidget:
"""
GUI widget which can manage layouts.
Typically, first it is set up using :meth:`setup` method to specify the master layout kind;
afterwards, widgets and sublayout can be added using :meth:`add_to_layout`.
In addition, it can directly add named sublayouts using :meth:`add_sublayout` method.
Abstract mix-in class, which needs to be added to a class inheriting from ``QWidget``.
Alternatively, one can directly use :class:`QLayoutManagedWidget`, which already inherits from ``QWidget``.
"""
def __init__(self, *args, **kwargs):
if not isinstance(self,QtWidgets.QWidget):
raise RuntimeError("IQLayoutManagedWidget should be mixed with a QWidget class or subclass")
super().__init__(*args,**kwargs)
self.main_layout=None
self._default_layout="main"
def _make_new_layout(self, kind, *args, **kwargs):
"""Make a layout of the given kind"""
if kind=="grid":
return QtWidgets.QGridLayout(*args,**kwargs)
if kind=="vbox":
return QtWidgets.QVBoxLayout(*args,**kwargs)
if kind=="hbox":
return QtWidgets.QHBoxLayout(*args,**kwargs)
raise ValueError("unrecognized layout kind: {}".format(kind))
def _set_main_layout(self):
self.main_layout=self._make_new_layout(self.main_layout_kind,self)
name=getattr(self,"name",None)
self.main_layout.setObjectName(name+"_main_layout" if name else "main_layout")
if self.no_margins:
self.main_layout.setContentsMargins(0,0,0,0)
[docs] def setup(self, layout="grid", no_margins=False):
"""
Setup the layout.
Args:
layout: layout kind; can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box).
no_margins: if ``True``, set all layout margins to zero (useful when the widget is in the middle of layout hierarchy)
"""
self.main_layout_kind=layout
self.no_margins=no_margins
self._set_main_layout()
self._sublayouts={"main":(self.main_layout,self.main_layout_kind)}
self._spacers=[]
[docs] @contextlib.contextmanager
def using_layout(self, name):
"""Use a different sublayout as default inside the ``with`` block"""
current_layout,self._default_layout=self._default_layout,name
try:
yield
finally:
self._default_layout=current_layout
def _normalize_location(self, location, default_location=None, default_layout=None):
if location=="skip":
return None,"skip"
if not isinstance(location,(list,tuple)):
location=() if location is None else (location,)
if location and isinstance(location[0],py3.textstring) and location[0]!="next":
lname,location=location[0],location[1:]
else:
lname=default_layout or self._default_layout
layout,lkind=self._sublayouts[lname]
if default_location is None:
default_location=("next",0,1,1)
location+=(None,)*(4-len(location))
location=[d if l is None else l for (l,d) in zip(location,default_location)]
row,col,rowspan,colspan=location
if lkind=="grid":
row_cnt,col_cnt=layout.rowCount(),layout.columnCount()
elif lkind=="vbox":
col,colspan=0,1
row_cnt,col_cnt=layout.count(),1
else:
if col==0:
col=row
if colspan==1:
colspan=rowspan
row,rowspan=0,1
row_cnt,col_cnt=1,layout.count()
if lkind in {"grid","vbox"}:
if row=="next":
row=row_cnt if layout.count() else 0
else:
row=(row%max(row_cnt,1) if row<0 else row)
if rowspan=="end":
rowspan=max(row_cnt-row,1)
elif rowspan<0:
rowspan=max(row_cnt+rowspan-row,1)
if lkind in {"grid","hbox"}:
if col=="next":
col=col_cnt if layout.count() else 0
else:
col=(col%max(col_cnt,1) if col<0 else col)
if colspan=="end":
colspan=max(col_cnt-col,1)
elif colspan<0:
colspan=max(col_cnt+colspan-col,1)
return lname,(row,col,rowspan,colspan)
def _insert_layout_element(self, lname, element, location, kind="widget"):
layout,lkind=self._sublayouts[lname]
if lkind=="grid":
if kind=="widget":
layout.addWidget(element,*location)
elif kind=="item":
layout.addItem(element,*location)
elif kind=="layout":
layout.addLayout(element,*location)
else:
raise ValueError("unrecognized element kind: {}".format(kind))
else:
idx=location[0] if lkind=="vbox" else location[1]
if lkind=="vbox" and (location[1]!=0 or location[3]!=1):
raise ValueError("vbox layout only has one column")
if lkind=="hbox" and (location[0]!=0 or location[2]!=1):
raise ValueError("hbox layout only has one row")
if kind=="widget":
layout.insertWidget(idx,element)
elif kind=="item":
layout.insertItem(idx,element)
elif kind=="layout":
layout.insertLayout(idx,element)
else:
raise ValueError("unrecognized element kind: {}".format(kind))
[docs] def add_to_layout(self, element, location=None, kind="widget"):
"""
Add an existing `element` to the layout at the given `location`.
`kind` can be ``"widget"`` for widgets, ``"layout"`` for other layouts, or ``"item"`` for layout items (spacers).
"""
lname,location=self._normalize_location(location)
if location!="skip":
self._insert_layout_element(lname,element,location,kind=kind)
return element
[docs] def remove_layout_element(self, element):
"""Remove a previously added layout element"""
for layout,_ in self._sublayouts.values():
idx=utils.find_layout_element(layout,element)
if idx is not None:
utils.delete_layout_item(layout,idx)
return True
return False
[docs] def get_element_position(self, element):
"""
Get the sublayout and the position of the given widget.
Return tuple ``(sublayout, location)``, where ``sublayout`` is the sublayout name (``"name"`` for the main layout),
and ``location`` is a tuple ``(row, column, rowspan, colspan)``.
If the given widget is not in this layout, return ``None``.
"""
for name,(layout,kind) in self._sublayouts.items():
idx=utils.find_layout_element(layout,element)
if idx is not None:
if kind=="grid":
location=layout.getItemPosition(idx)
elif kind=="hbox":
location=(0,idx,1,1)
elif kind=="vbox":
location=(idx,0,1,1)
return name,location
return None
[docs] def add_sublayout(self, name, kind="grid", location=None):
"""
Add a sublayout to the given location.
`name` specifies the sublayout name, which can be used to refer to it in specifying locations later.
`kind` can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box).
"""
if name in self._sublayouts:
raise ValueError("sublayout {} already exists".format(name))
layout=self._make_new_layout(kind)
layout.setContentsMargins(0,0,0,0)
layout.setObjectName(name)
self.add_to_layout(layout,location,kind="layout")
self._sublayouts[name]=(layout,kind)
return layout
[docs] @contextlib.contextmanager
def using_new_sublayout(self, name, kind="grid", location=None):
"""
Create a different sublayout and use it as default inside the ``with`` block.
`kind` can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box).
"""
self.add_sublayout(name,kind=kind,location=location)
with self.using_layout(name):
yield
[docs] def get_sublayout(self, name=None):
"""Get the previously added sublayout"""
return self._sublayouts[name or self._default_layout][0]
def _iter_layout_items(self, layout, include, nested=False):
for idx in range(layout.count()):
item=layout.itemAt(idx)
if item.widget() is not None and "widget" in include:
yield "widget",item.widget()
if item.layout() is not None:
if "layout" in include:
yield "layout",item.layout()
if nested:
for itm in self._iter_layout_items(item.layout(),include,nested=True):
yield itm
[docs] def iter_sublayout_items(self, name=None, include=("widget",), nested=False):
"""
Iterate over items contained in a given sublayout.
`include` is a tuple which contains items to iterate over; can include ``"widget"`` or ``"layout"``.
If ``nested==True``, iterate over items in contained layouts as well.
"""
if include=="all":
include=("widget","layout")
elif not isinstance(include,(tuple,list)):
include=(include,)
include=set(include)
if include-{"widget","layout"}:
raise ValueError("unrecognized include kinds: {}".format(include-{"widgets","layout"}))
for itm in self._iter_layout_items(self.get_sublayout(name=name),include,nested=nested):
yield itm
[docs] def get_sublayout_kind(self, name=None):
"""Get the kind of the previously added sublayout"""
return self._sublayouts[name or self._default_layout][1]
[docs] def get_layout_shape(self, name=None):
"""Get shape ``(rows, cols)`` of the current layout"""
layout,kind=self._sublayouts[name or self._default_layout]
if kind=="grid":
return layout.rowCount(),layout.columnCount()
elif kind=="hbox":
return 1,layout.count()
else:
return layout.count(),1
[docs] def add_spacer(self, height=0, width=0, stretch_height=False, stretch_width=False, stretch=0, location="next"):
"""
Add a spacer with the given width and height to the given location.
If ``stretch_height==True`` or ``stretch_width==True``, the widget will stretch in these directions; otherwise, the widget size is fixed.
If `stretch` is not ``None``, it specifies stretch of the spacer the corresponding direction (applied to the upper row and leftmost column for multi-cell spacer);
if `kind=="both"``, it can also be a tuple with two stretches along vertical and horizontal directions.
"""
spacer=QtWidgets.QSpacerItem(width,height,
QtWidgets.QSizePolicy.MinimumExpanding if stretch_width else QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.MinimumExpanding if stretch_height else QtWidgets.QSizePolicy.Minimum)
lname,lpos=self._normalize_location(location)
self.add_to_layout(spacer,location,kind="item")
self._spacers.append(spacer) # otherwise the reference is lost, and the object might be deleted
if lname is not None:
r,c=lpos[:2]
layout,lkind=self._sublayouts[lname]
if not isinstance(stretch,tuple):
stretch=(stretch,stretch)
if lkind=="grid":
if stretch_height:
layout.setRowStretch(r,stretch[0])
if stretch_width:
layout.setColumnStretch(c,stretch[1])
elif lkind=="vbox" and stretch_height:
layout.setStretch(r,stretch[0])
elif lkind=="hbox" and stretch_width:
layout.setStretch(c,stretch[1])
return spacer
[docs] def add_padding(self, kind="auto", location="next", stretch=0):
"""
Add a padding (expandable spacer) of the given kind to the given location.
`kind` can be ``"vertical"``, ``"horizontal"``, ``"auto"`` (vertical for ``grid`` and ``vbox`` layouts, horizontal for ``hbox``),
or ``"both"`` (stretches in both directions).
If `stretch` is not ``None``, it specifies stretch of the spacer the corresponding direction (applied to the upper row and leftmost column for multi-cell spacer);
can also be a tuple with two stretches along vertical and horizontal directions.
"""
funcargparse.check_parameter_range(kind,"kind",{"auto","horizontal","vertical","both"})
if kind=="auto":
lname,_=self._normalize_location(location)
if lname is None:
kind="vertical"
else:
_,lkind=self._sublayouts[lname]
kind="horizontal" if lkind=="hbox" else "vertical"
stretch_height=kind in {"vertical","both"}
stretch_width=kind in {"horizontal","both"}
return self.add_spacer(stretch_height=stretch_height,stretch_width=stretch_width,location=location,stretch=stretch)
def _normalize_stretch(self, args):
if len(args)==1:
return list(enumerate(args[0]))
if len(args)==2:
return [(args[0],args[1])]
raise TypeError("method takes one or two positional arguments, {} supplied".format(len(args)))
[docs] def set_row_stretch(self, *args, layout=None):
"""
Set row stretch for a given layout.
Takes either two arguments ``index`` and ``stretch``, or a single list of stretches for all rows.
"""
layout,lkind=self._sublayouts[layout or self._default_layout]
for i,s in self._normalize_stretch(args):
if lkind=="grid":
layout.setRowStretch(i,s)
elif lkind=="vbox":
layout.setStretch(i,s)
else:
raise ValueError("only gird and vbox layout support column stretch")
[docs] def set_column_stretch(self, *args, layout=None):
"""
Set column stretch for a given layout.
Takes either two arguments ``index`` and ``stretch``, or a single list of stretches for all columns.
"""
layout,lkind=self._sublayouts[layout or self._default_layout]
for i,s in self._normalize_stretch(args):
if lkind=="grid":
layout.setColumnStretch(i,s)
elif lkind=="hbox":
layout.setStretch(i,s)
else:
raise ValueError("only gird and hbox layout support column stretch")
[docs] def add_decoration_label(self, text, location="next"):
"""Add a decoration text label with the given text"""
label=QtWidgets.QLabel(self)
label.setText(str(text))
label.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.add_to_layout(label,location)
return label
[docs] def insert_row(self, row, sublayout=None, stretch=0):
"""Insert a new row at the given location in the grid layout"""
layout,kind=self._sublayouts[sublayout or self._default_layout]
if kind!="grid":
raise ValueError("can add rows only to grid layouts (vbox layouts work automatically)")
utils.insert_layout_row(layout,row%(layout.rowCount() or 1),stretch=stretch)
[docs] def insert_column(self, col, sublayout=None, stretch=0):
"""Insert a new column at the given location in the grid layout"""
layout,kind=self._sublayouts[sublayout or self._default_layout]
if kind!="grid":
raise ValueError("can add columns only to grid layouts (hbox layouts work automatically)")
utils.insert_layout_column(layout,col%(layout.colCount() or 1),stretch=stretch)
[docs] def clear(self):
"""Clear the layout and remove all the added elements"""
utils.clean_layout(self.main_layout,delete_layout=True)
if self.main_layout is not None:
self._set_main_layout()
self._sublayouts={"main":(self.main_layout,self.main_layout_kind)}
self._spacers=[]
[docs]class QLayoutManagedWidget(IQLayoutManagedWidget, QtWidgets.QWidget):
"""
GUI widget which can manage layouts.
Typically, first it is set up using :meth:`setup` method to specify the master layout kind;
afterwards, widgets and sublayout can be added using :meth:`add_to_layout`.
In addition, it can directly add named sublayouts using :meth:`add_sublayout` method.
Simply a combination of :class:`IQLayoutManagedWidget` and ``QWidget``.
"""