from ...utils import general, dictionary
from .. import value_handling
from ..value_handling import _hasattr
from .. import QtCore, QtWidgets, Signal
from ...thread import controller
from .layout_manager import IQLayoutManagedWidget
import collections
TTimer=collections.namedtuple("TTimer",["name","period","timer"])
TTimerEvent=collections.namedtuple("TTimerEvent",["start","loop","stop","timer"])
TChild=collections.namedtuple("TChild",["name","widget","gui_values_path"])
[docs]class IQContainer:
"""
Basic controller object which combines and controls several other widget.
Can either corresponds to a widget (e.g., a frame or a group box), or simply be an organizing entity.
Args:
name: entity name (used by default when adding this object to a values table)
Abstract mix-in class, which needs to be added to a class inheriting from ``QObject``.
Alternatively, one can directly use :class:`QContainer`, which already inherits from ``QObject``.
"""
TimerUIDGenerator=general.NamedUIDGenerator(thread_safe=True)
def __init__(self, *args, name=None, **kwargs):
if not isinstance(self,QtCore.QObject):
raise RuntimeError("IQContainer should be mixed with a QObject class or subclass")
super().__init__(*args,**kwargs)
self.name=None
self.setup_name(name)
self._timers={}
self._timer_events={}
self._running=False
self._stopping=False
self._children=dictionary.Dictionary()
self.gui_values=value_handling.GUIValues()
self.ctl=None
self.c=dictionary.ItemAccessor(self.get_child)
self.w=dictionary.ItemAccessor(self.get_widget)
self.v=dictionary.ItemAccessor(self.get_value,self.set_value)
self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator)
_ignore_set_values=[]
_ignore_get_values=[]
contained_value_changed=Signal(object,object)
[docs] def setup_name(self, name):
"""Set the object's name"""
if name is not None:
self.name=name
self.setObjectName(name) # pylint: disable=no-member
[docs] def setup(self, name=None):
"""Setup the container by initializing its GUI values and setting the ``ctl`` attribute"""
if self.name is None:
self.setup_name(name)
self.ctl=controller.get_gui_controller()
[docs] def add_timer(self, name, period, autostart=True):
"""
Add a periodic timer with the given `name` and `period`.
Rarely needs to be called explicitly (one is created automatically if timer event is created).
If ``autostart==True`` and the container has been started (by calling :meth:`start` method), start the timer as well.
"""
if name is None:
while True:
name=self.TimerUIDGenerator("timer")
if name not in self._timers:
break
if name in self._timers:
raise ValueError("timer {} already exists".format(name))
timer=QtCore.QTimer(self)
timer.timeout.connect(controller.exsafe(lambda: self._on_timer(name)))
self._timers[name]=TTimer(name,period,timer)
if self._running and autostart:
self.start_timer(name)
return name
def _get_timer(self, name):
if name not in self._timers:
raise KeyError("timer {} does not exist".format(name))
return self._timers[name]
[docs] def start_timer(self, name):
"""Start the timer with the given name (also called automatically on :meth:`start` method)"""
if not self.is_timer_running(name):
self._call_timer_events(name,"start")
_,period,timer=self._get_timer(name)
timer.start(max(int(period*1000),1))
[docs] def stop_timer(self, name):
"""Stop the timer with the given name (also called automatically on :meth:`stop` method)"""
running=self.is_timer_running(name)
_,_,timer=self._get_timer(name)
if running:
timer.stop()
self._call_timer_events(name,"stop")
[docs] def is_timer_running(self, name):
"""Check if the timer with the given name is running"""
_,_,timer=self._get_timer(name)
return timer.isActive()
def _on_timer(self, name):
self._call_timer_events(name,"loop")
[docs] def add_timer_event(self, name, loop=None, start=None, stop=None, period=None, timer=None, autostart=True):
"""
Add timer event with the given `name`.
Add an event which should be called periodically (e.g., a GUI update). Internally implemented through Qt timers.
`loop`, `start` and `stop` are the functions called, correspondingly, on timer (periodically), when timer is start, and when it's finished.
One can either specify the timer by name (`timer` parameter), or create a new one with the given `period`.
If ``autostart==True`` and the container has been started (by calling :meth:`start` method), start the timer as well.
"""
if timer is None and period is None:
raise ValueError("either a period or a timer name should be provided")
if name in self._timer_events:
raise ValueError("timer event {} already exists".format(name))
if timer is None:
timer=self.add_timer(None,period,autostart=autostart)
if start is not None and self.is_timer_running(timer):
start()
self._timer_events[name]=TTimerEvent(start,loop,stop,timer)
return timer
def _call_timer_events(self, timer, meth="loop"):
t=self._get_timer(timer).timer
for evt in self._timer_events.values():
if evt.timer==timer:
if meth=="start" and evt.start is not None:
evt.start()
elif meth=="stop" and evt.stop is not None:
evt.stop()
elif evt.loop is not None and t.isActive(): # discard all possible after-stop queued events
evt.loop()
[docs] def add_child_values(self, name, widget, path, add_change_event=True):
"""
Add child's values to the container's table.
If `widget` is a container and ``path==""`` or ends in ``"/*"`` (e.g., ``"subpath/*"``),
use its ``setup_gui_values`` to make it share the same GUI values;
otherwise, simply add it to the GUI values under the given path.
if ``add_change_event==True``, changing of the widget's value emits the container's ``contained_value_changed`` event
"""
path=self._normalize_name(path)
if path=="" or path=="*" or path.endswith("/*"):
if path.endswith("*"):
path=path[:-1]
self.gui_values.add_widget((path,"."+name),widget)
else:
self.gui_values.add_widget(path,widget)
if add_change_event:
if _hasattr(widget,"value_changed"):
widget.value_changed.connect(lambda value: self.contained_value_changed.emit(path,value))
elif _hasattr(widget,"contained_value_changed"):
widget.contained_value_changed.connect(lambda name,value: self.contained_value_changed.emit(self._normalize_name((path,name)),value))
def _setup_child_name(self, widget, name):
if name is None:
name=getattr(widget,"name",None)
if name is None:
raise ValueError("widget name must be provided")
elif _hasattr(widget,"setup_name"):
widget.setup_name(name)
return name
[docs] def add_child(self, name, widget, gui_values_path=True, add_change_event=True):
"""
Add a contained child widget.
If `gui_values_path` is ``False`` or ``None``, do not add it to the GUI values table;
if it is ``True``, add it under the same root (``path==""``) if it's a container, and under `name` if it's not;
otherwise, ``gui_values_path`` specifies the path under which the widget values are stored.
if ``add_change_event==True``, changing of the widget's value emits the container's ``contained_value_changed`` event
"""
name=self._setup_child_name(widget,name)
if name in self._children:
raise ValueError("child {} is already present".format(name))
if gui_values_path!=False and gui_values_path is not None:
if gui_values_path==True:
gui_values_path="" if _hasattr(widget,"get_all_values") else name
self.add_child_values(name,widget,gui_values_path,add_change_event=add_change_event)
else:
gui_values_path=None
self._children[name]=TChild(name,widget,gui_values_path)
return widget
[docs] def get_child(self, name):
"""Get the child widget with the given name"""
path,subpath=self._children.get_max_prefix(name,kind="leaf")
if path:
return self._children[path].widget.get_child(subpath) if subpath else self._children[path].widget
raise KeyError("can't find widget {}".format(name))
def _clear_child(self, child):
if _hasattr(child.widget,"clear"):
child.widget.clear()
if child.gui_values_path is not None:
try:
self.gui_values.remove_handler(child.gui_values_path,remove_indicator=True,disconnect=True)
except KeyError:
pass
[docs] def remove_child(self, name):
"""Remove child from the container and clear it"""
path,subpath=self._children.get_max_prefix(name,kind="leaf")
if path:
if subpath:
return self._children[path].widget.remove_child(subpath)
ch=self._children.pop(path)
self._clear_child(ch)
else:
raise KeyError("can't find widget {}".format(name))
[docs] def add_virtual_element(self, name, value=None, multivalued=False, add_indicator=True):
"""
Add a virtual value element.
Doesn't correspond to any actual widget, but behaves very similarly from the application point of view
(its value can be set or read, it has on-change events, it can have indicator).
The element value is simply stored on set and retrieved on get.
If ``multivalued==True``, the internal value is assumed to be complex, so it is forced to be a :class:`.Dictionary` every time it is set.
If ``add_indicator==True``, add default indicator handler as well.
"""
self.gui_values.add_virtual_element(name,value=value,multivalued=multivalued,add_indicator=add_indicator)
[docs] def add_property_element(self, name, getter=None, setter=None, add_indicator=True):
"""
Add a property value element.
Doesn't correspond to any actual widget, but behaves very similarly from the application point of view;
each time the value is set or get, the corresponding setter and getter methods are called.
If ``add_indicator==True``, add default (stored value) indicator handler as well.
"""
self.gui_values.add_property_element(name,getter=getter,setter=setter,add_indicator=add_indicator)
@controller.exsafe
def start(self):
"""
Start the container.
Starts all the internal timers, and calls ``start`` method for all the contained widgets.
"""
if self._running:
return
for ch in self._children.iternodes():
if _hasattr(ch.widget,"start"):
ch.widget.start()
for n in self._timers:
self.start_timer(n)
self._stopping=False
self._running=True
def _notify_stop(self):
if not self._stopping:
self._stopping=True
for ch in self._children.iternodes():
if _hasattr(ch.widget,"_notify_stop"):
ch.widget._notify_stop()
@controller.exsafe
def stop(self):
"""
Stop the container.
Stops all the internal timers, and calls ``stop`` method for all the contained widgets.
"""
if not self._running:
return
self._notify_stop()
self._running=False
for n in self._timers:
self.stop_timer(n)
for ch in self._children.iternodes():
if _hasattr(ch.widget,"stop"):
ch.widget.stop()
[docs] def is_running(self):
"""Check if the container is running (started and not yet stopped)"""
return self._running
[docs] def is_stopping(self):
"""Check if the container is stopping (stopping initialized and not yet done)"""
return self._stopping
[docs] def clear(self):
"""
Clear the container.
Stop all timers and widgets, and call ``clear`` methods of all contained widgets,
remove all widgets from the values table, remove all widgets from the table.
"""
if self._running:
self.stop()
for ch in self._children.paths():
self.remove_child(ch)
self._children=dictionary.Dictionary()
def _filter_values(self, values, exclude):
if exclude and any(k in values for k in exclude):
values=values.copy()
for v in exclude:
if v in values:
del values[v]
return values
def _normalize_name(self, name):
if isinstance(name,tuple):
name=dictionary.normalize_path(name)
if isinstance(name,(list,tuple)):
return "/".join(name)
return name
[docs] def get_handler(self, name):
"""Get value handler of a widget with the given name"""
return self.gui_values.get_handler(name)
[docs] def get_value(self, name=None):
"""Get value of a widget with the given name (``None`` means all values)"""
return self.gui_values.get_value(name)
[docs] def get_all_values(self):
"""Get values of all widget in the container"""
return self._filter_values(self.gui_values.get_all_values(),self._ignore_get_values)
[docs] def set_value(self, name, value):
"""Set value of a widget with the given name (``None`` means all values)"""
return self.gui_values.set_value(name,value)
[docs] def set_all_values(self, value):
"""Set values of all widgets in the container"""
return self.gui_values.set_all_values(self._filter_values(value,self._ignore_set_values))
[docs] def get_value_changed_signal(self, name):
"""Get a value-changed signal for a widget with the given name"""
return self.gui_values.get_value_changed_signal(name)
[docs] def update_value(self, name=None):
"""
Send update signal for a handler with a given name or list of names.
Emit a value changed signal with the current value to notify the subscribed slots.
If `name` is ``None``, emit for all values in the table.
"""
return self.gui_values.update_value(name=name)
[docs] def get_indicator(self, name=None):
"""Get indicator value for a widget with the given name (``None`` means all indicators)"""
return self.gui_values.get_indicator(name)
[docs] def get_all_indicators(self):
"""Get indicator values of all widget in the container"""
return self.gui_values.get_all_indicators()
[docs] def set_indicator(self, name, value, ignore_missing=False):
"""Set indicator value for a widget or a branch with the given name"""
return self.gui_values.set_indicator(name,value,ignore_missing=ignore_missing)
[docs] def set_all_indicators(self, value, ignore_missing=True):
return self.gui_values.set_all_indicators(value,ignore_missing=ignore_missing)
[docs] def update_indicators(self):
"""Update all indicators to represent current values"""
return self.gui_values.update_indicators()
[docs]class QContainer(IQContainer, QtCore.QObject):
"""
Basic controller object which combines and controls several other widget.
Can either corresponds to a widget (e.g., a frame or a group box), or simply be an organizing entity.
Args:
name: entity name (used by default when adding this object to a values table)
Simply a combination of :class:`IQContainer` and ``QObject``.
"""
[docs]class QFrameContainer(IQWidgetContainer, QtWidgets.QFrame):
"""An extension of :class:`IQWidgetContainer` for a ``QFrame`` Qt base class"""
[docs]class QDialogContainer(IQWidgetContainer, QtWidgets.QDialog):
"""An extension of :class:`IQWidgetContainer` for a ``QDialog`` Qt base class"""
[docs]class QGroupBoxContainer(IQWidgetContainer, QtWidgets.QGroupBox):
"""An extension of :class:`IQWidgetContainer` for a ``QGroupBox`` Qt base class"""
[docs] def setup(self, caption=None, layout="vbox", no_margins=False, name=None): # pylint: disable=arguments-differ, arguments-renamed
super().setup(layout=layout,no_margins=no_margins,name=name)
if caption is not None:
self.setTitle(caption)
[docs]class QTabContainer(IQContainer, QtWidgets.QTabWidget):
"""
Container which manages tab widget.
Does not have its own layout, but can add or remove tabs, which are represented as :class:`QFrameContainer` widgets.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)
self._tabs={}
def _insert_tab(self, tab, caption, index):
if index is None:
index=self.count()
elif index<0:
index=index%self.count()
else:
index=min(index,self.count())
self.insertTab(index,tab,caption)
[docs] def add_tab(self, name, caption, index=None, widget=None, layout="vbox", gui_values_path=True, no_margins=True):
"""
Add a new tab container with the given `caption` to the widget.
`index` specifies the new tab's index (``None`` means adding to the end, negative values count from the end).
If `widget` is ``None``, create a new frame widget using the given `layout` (``"vbox"``, ``"hbox"``, or ``"grid"``)
and `no_margins` (specifies whether the frame has inner margins) arguments;
otherwise, use the supplied widget.
The other parameters are the same as in :meth:`add_child` method.
"""
if name in self._tabs:
raise ValueError("tab {} already exists".format(name))
if widget is None:
widget=QFrameContainer(self)
self.add_child(name=name,widget=widget,gui_values_path=gui_values_path)
widget.setup(layout=layout,no_margins=no_margins)
else:
self.add_child(name=name,widget=widget,gui_values_path=gui_values_path)
self._insert_tab(widget,caption,index)
self._tabs[name]=widget
return widget
[docs] def remove_tab(self, name):
"""
Remove a tab with the given name.
Clear it, remove its GUI values, and delete it and all contained widgets.
"""
super().remove_child(name)
frame=self._tabs.pop(name)
idx=self.indexOf(frame)
self.removeTab(idx)
frame.deleteLater()
[docs] def clear(self):
for n in list(self._tabs):
self.remove_tab(n)
super().clear()
[docs] def get_current_name(self):
"""Get current tab name"""
tab=self.currentWidget()
for n,w in self._tabs.items():
if tab is w:
return n
[docs] def set_by_name(self, name):
"""Set tab by name"""
tab=self.c[name]
self.setCurrentIndex(self.indexOf(tab))