diff options
Diffstat (limited to 'lib/taurus/qt/qtgui/base/taurusbase.py')
-rw-r--r-- | lib/taurus/qt/qtgui/base/taurusbase.py | 1881 |
1 files changed, 1881 insertions, 0 deletions
diff --git a/lib/taurus/qt/qtgui/base/taurusbase.py b/lib/taurus/qt/qtgui/base/taurusbase.py new file mode 100644 index 00000000..3ee23e58 --- /dev/null +++ b/lib/taurus/qt/qtgui/base/taurusbase.py @@ -0,0 +1,1881 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################# +## +## This file is part of Taurus +## +## http://taurus-scada.org +## +## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +## Taurus is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Taurus is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Taurus. If not, see <http://www.gnu.org/licenses/>. +## +############################################################################# + +"""This module provides the set of base classes from which the Qt taurus widgets +should inherit to be considered valid taurus widgets.""" + +__all__ = ["TaurusBaseComponent", "TaurusBaseWidget", "TaurusBaseWritableWidget"] + +__docformat__ = 'restructuredtext' + +import sys +import threading + +import PyTango + +from taurus.external.qt import Qt + +import taurus +from taurus.core.util import eventfilters +from taurus.core.util.timer import Timer +from taurus.core.taurusbasetypes import TaurusElementType, TaurusEventType +from taurus.core.taurusattribute import TaurusAttribute +from taurus.core.taurusdevice import TaurusDevice +from taurus.core.taurusconfiguration import (TaurusConfiguration, + TaurusConfigurationProxy) +from taurus.core.tauruslistener import TaurusListener, TaurusExceptionListener +from taurus.core.taurusoperation import WriteAttrOperation +from taurus.core.util.eventfilters import filterEvent +from taurus.qt.qtcore.configuration import BaseConfigurableClass +from taurus.qt.qtcore.mimetypes import TAURUS_ATTR_MIME_TYPE, TAURUS_DEV_MIME_TYPE, TAURUS_MODEL_MIME_TYPE +from taurus.qt.qtgui.util import ActionFactory + +DefaultNoneValue = "-----" + +TTANGO_TO_TQT = { + str : 'QString', + int : 'int', + long : 'long', + float : 'float', + bool : 'bool', + list : 'QList', + tuple : 'QList', + dict : 'QMap', + PyTango.ArgType.DevDouble : 'float', + PyTango.ArgType.DevFloat : 'float', + PyTango.ArgType.DevLong : 'int', + PyTango.ArgType.DevShort : 'int', + PyTango.ArgType.DevBoolean : 'bool', + PyTango.ArgType.DevUChar : 'int', + PyTango.ArgType.DevState : 'int', + PyTango.ArgType.DevString : 'QString', + PyTango.ArgType.DevULong : 'int', + PyTango.ArgType.DevLong64 : 'long', + PyTango.ArgType.DevULong64 : 'long', + PyTango.ArgType.DevUShort : 'int', + PyTango.ArgType.DevVarBooleanArray : 'QList', + PyTango.ArgType.DevVarCharArray : 'QList', + PyTango.ArgType.DevVarDoubleArray : 'QList', + PyTango.ArgType.DevVarFloatArray : 'QList', + PyTango.ArgType.DevVarLongArray : 'QList', + PyTango.ArgType.DevVarLong64Array : 'QList', + PyTango.ArgType.DevVarShortArray : 'QList', + PyTango.ArgType.DevVarStringArray : 'QList', + PyTango.ArgType.DevVarULongArray : 'QList', + PyTango.ArgType.DevVarUShortArray : 'QList', +} + +class TaurusBaseComponent(TaurusListener, BaseConfigurableClass): + """A generic Taurus component. + + .. note:: + Any class which inherits from TaurusBaseComponent is expected to also + inherit from QObject (or from a QObject derived class) If this is not + fullfilled, at least the class should reimplement the :meth:`getSignaller` + method to return a QObject to be used for emitting and connecting + signals. + """ + _modifiableByUser = False + _showQuality = True + _eventBufferPeriod = 0 + + def __init__(self, name, parent=None, designMode=False): + """Initialization of TaurusBaseComponent""" + self.modelObj = None + self.modelName = '' + self.noneValue = DefaultNoneValue + self._designMode = designMode + self.call__init__(TaurusListener, name, parent) + + BaseConfigurableClass.__init__(self) + + self.taurusMenu = None + self.taurusMenuData = '' + + # attributes storing property values + self._localModelName = '' + self._useParentModel = False + self._showText = True + self._attached = False + self._dangerMessage = "" + self._isDangerous = False + self._forceDangerousOperations = False + self._eventFilters = [] + self._preFilters = [] + self._isPaused = False + self._operations = [] + self._modelInConfig = False + self._autoProtectOperation = True + + self._bufferedEvents = {} + self._bufferedEventsTimer = None + self.setEventBufferPeriod(self._eventBufferPeriod) + + if parent != None and hasattr(parent, "_exception_listener"): + self._exception_listener = parent._exception_listener + else: + self._exception_listener = set([TaurusExceptionListener()]) + + #register configurable properties + self.registerConfigProperty(self.isModifiableByUser, self.setModifiableByUser, "modifiableByUser") + self.registerConfigProperty(self.getModelInConfig, self.setModelInConfig, "ModelInConfig") + self.resetModelInConfig() + + def getSignaller(self): + ''' + Reimplement this method if your derived class does not inherit from + QObject. The return value should be a permanent object capable of + emitting Qt signals. See :class:`TaurusImageItem` as an example + ''' + return self + + def deleteLater(self): + '''Reimplements the Qt.QObject deleteLater method to ensure that the + this object stops listening its model.''' + self.setUseParentModel(False) + self.resetModel() + Qt.QObject.deleteLater(self) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Utility methods + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + + def getTaurusManager(self): + """Returns the the taurus manager singleton. This is just a helper method. + It is the equivalent of doing:: + + import taurus + manager = taurus.Manager() + + :return: (taurus.core.taurusmanager.TaurusManager) the TaurusManager + """ + return taurus.Manager() + + def getTaurusFactory(self, scheme=None): + """Returns the the taurus factory singleton for the given scheme. + This is just a helper method. It is the equivalent of doing:: + + import taurus + factory = taurus.Factory(scheme) + + :param scheme: (str or None) the scheme. None defaults to 'tango'. + + :return: (taurus.core.taurusfactory.TaurusFactory) the TaurusFactory + """ + return taurus.Factory(scheme) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Popup menu behavior + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def contextMenuEvent(self, event): + """Handle the popup menu event + + :param event: the popup menu event + """ + if self.taurusMenu is not None: + self.taurusMenu.exec_(event.globalPos()) + else: + event.ignore() + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Mandatory methods to be implemented in subclass implementation + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ + + def updateStyle(self): + """Method called when the component detects an event that triggers a + change in the style. + Default implementation doesn't do anything. Overwrite when necessary + """ + pass + + def getParentTaurusComponent(self): + """ Returns a parent Taurus component or None if no parent + :class:`taurus.qt.qtgui.base.TaurusBaseComponent` is found. + + :raises: RuntimeError + """ + raise RuntimeError("Not allowed to call TaurusBaseComponent::getParentTaurusComponent()") + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Event handling chain + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def setEventBufferPeriod(self, period): + '''Set the period at wich :meth:`fireBufferedEvents` will be called. + If period is 0, the event buffering is disabled (i.e., events are fired + as soon as they are received) + + :param period: (float) period in seconds for the automatic event firing. + period=0 will disable the event buffering. + ''' + self._eventBufferPeriod = period + if period == 0: + if self._bufferedEventsTimer is not None: + self._bufferedEventsTimer.stop() + self._bufferedEventsTimer = None + self.fireBufferedEvents() #flush the buffer + else: + self._eventsBufferLock = threading.RLock() + self._bufferedEventsTimer = Timer(period, self.fireBufferedEvents, + self) + self._bufferedEventsTimer.start() + + def getEventBufferPeriod(self): + '''Returns the event buffer period + + :return: (float) period (in s). 0 means event buffering is disabled. + ''' + return self._eventBufferPeriod + + def eventReceived(self, evt_src, evt_type, evt_value): + """The basic implementation of the event handling chain is as + follows: + + - eventReceived just calls :meth:`fireEvent` which emits a "taurusEvent" + PyQt signal that is connected (by :meth:`preAttach`) to the + :meth:`filterEvent` method. + - After filtering, :meth:`handleEvent` is invoked with the resulting + filtered event + + .. note:: + in the earlier steps of the chain (i.e., in :meth:`eventReceived`/ + :meth:`fireEvent`), the code is executed in a Python thread, while + from eventFilter ahead, the code is executed in a Qt thread. + When writing widgets, one should normally work on the Qt thread + (i.e. reimplementing :meth:`handleEvent`) + + :param evt_src: (object) object that triggered the event + :param evt_type: (taurus.core.taurusbasetypes.TaurusEventType) type of event + :param evt_value: (object) event value + """ + evt = filterEvent(evt_src, evt_type, evt_value, + filters=self._preFilters) + if evt is not None: + self.fireEvent(*evt) + + def fireEvent(self, evt_src=None, evt_type=None, evt_value=None): + """Emits a "taurusEvent" signal. + It is unlikely that you need to reimplement this method in subclasses. + Consider reimplementing :meth:`eventReceived` or :meth:`handleEvent` + instead depending on whether you need to execute code in the python + or Qt threads, respectively + + :param evt_src: (object or None) object that triggered the event + :param evt_type: (taurus.core.taurusbasetypes.TaurusEventType or None) + type of event + :param evt_value: (object or None) event value + """ + if self._eventBufferPeriod: + # If we have an active event buffer delay, store the event... + with self._eventsBufferLock: + self._bufferedEvents[(evt_src, evt_type)] = (evt_src, evt_type, + evt_value) + else: + # if we are not buffering, directly emit the signal + try: + self.getSignaller().emit(Qt.SIGNAL('taurusEvent'), + evt_src, evt_type, evt_value) + except: + pass #self.error('%s.fireEvent(...) failed!'%type(self)) + + def fireBufferedEvents(self): + '''Fire all events currently buffered (and flush the buffer) + + Note: this method is normally called from an event buffer timer thread + but it can also be called any time the buffer needs to be flushed + ''' + signaller = self.getSignaller() + with self._eventsBufferLock: + for evt in self._bufferedEvents.values(): + signaller.emit(Qt.SIGNAL('taurusEvent'), *evt) + self._bufferedEvents = {} + + + def filterEvent(self, evt_src=-1, evt_type=-1, evt_value=-1): + """The event is processed by each and all filters in strict order + unless one of them returns None (in which case the event is discarded) + + :param evt_src: (object) object that triggered the event + :param evt_type: (taurus.core.taurusbasetypes.TaurusEventType) type of event + :param evt_value: (object) event value + """ + evt = evt_src, evt_type, evt_value + + if evt == (-1,-1,-1): + # @todo In an ideal world the signature of this method should be + # (evt_src, evt_type, evt_value). However there's a bug in PyQt: + # If a signal is disconnected between the moment it is emitted and + # the moment the slot is called, then the slot is called without + # parameters (!?). We added this default values to detect if + # this is the case without printing an error message each time. + # If this gets fixed, we should remove this line. + return + + evt = filterEvent(*evt, filters=self._eventFilters) + if evt is not None: + self.handleEvent(*evt) + + def handleEvent(self, evt_src, evt_type, evt_value): + """Event handling. Default implementation does nothing. + Reimplement as necessary + + :param evt_src: (object or None) object that triggered the event + :param evt_type: (taurus.core.taurusbasetypes.TaurusEventType or None) type of event + :param evt_value: (object or None) event value + """ + pass + + def setEventFilters(self, filters=None, preqt=False): + """sets the taurus event filters list. + The filters are run in order, using each output to feed the next filter. + A filter must be a function that accepts 3 arguments ``(evt_src, evt_type, evt_value)`` + If the event is to be ignored, the filter must return None. + If the event is not to be ignored, filter must return a + ``(evt_src, evt_type, evt_value)`` tuple which may (or not) differ from the input. + + For a library of common filters, see taurus/core/util/eventfilters.py + + :param filters: (sequence) a sequence of filters + :param preqt: (bool) If true, set the pre-filters (that are applied in + eventReceived, at the python thread), + otherwise, set the filters to be applied at the main + Qt thread (default) + + *Note*: If you are setting a filter that applies a transformation on + the parameters, you may want to generate a fake event to force the last + value to be filtered as well. This can be done as in this example:: + + TaurusBaseComponent.fireEvent( TaurusBaseComponent.getModelObj(), + taurus.core.taurusbasetypes.TaurusEventType.Periodic, + TaurusBaseComponent.getModelObj().getValueObj()) + + See also: insertEventFilter + """ + if filters is None: + filters = [] + if preqt: + self._preFilters = list(filters) + else: + self._eventFilters = list(filters) + + def getEventFilters(self, preqt=False): + """Returns the list of event filters for this widget + + :param preqt: (bool) If true, return the pre-filters (that are applied + in eventReceived, at the python thread), + otherwise, return the filters to be applied at the main + Qt thread (default) + + :return: (sequence<callable>) the event filters + """ + return (self._preFilters if preqt else self._eventFilters) + + def insertEventFilter(self, filter, index=-1, preqt=False): + """insert a filter in a given position + + :param filter: (callable(evt_src, evt_type, evt_value)) a filter + :param index: (int) index to place the filter (default = -1 meaning place at the end) + :param preqt: (bool) If true, set the pre-filters (that are applied in + eventReceived, at the python thread), + otherwise, set the filters to be applied at the main + Qt thread (default) + + + See also: setEventFilters + """ + if preqt: + self._preFilters.insert(index, filter) + else: + self._eventFilters.insert(index, filter) + + def setPaused(self, paused = True): + """Toggles the pause mode. + + :param paused: (bool) whether or not to pause (default = True) + """ + if paused == self._isPaused: return #nothing to do + if paused: #pausing + self.insertEventFilter(eventfilters.IGNORE_ALL, 0) + self.debug('paused') + else: #unpausing + try: + self._eventFilters.remove(eventfilters.IGNORE_ALL) + self.debug('Unpaused') + except ValueError: + self.warning('Unpause failed') + self._isPaused = paused + + def isPaused(self): + """Return the current pause state + + :return: (bool) wheater or not the widget is paused + """ + return self._isPaused + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Model class methods + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def getModelClass(self): + """Return the class object for the widget. + Default behavior is to do a 'best effort' to determine which model + type corresponds to the current model name. + Overwrite as necessary. + + :return: (class TaurusModel or None) The class object corresponding to the type + of Taurus model this widget handles or None if no valid class is found. + """ + return self.findModelClass() + + def findModelClass(self): + """Do a "best effort" to determine which model type corresponds to the + given model name. + + :return: (class TaurusModel or None) The class object corresponding to the type + of Taurus model this widget handles or None if no valid class is found. + """ + if self.getUseParentModel(): + return self._findRelativeModelClass(self.getModel()) + else: + return self._findAbsoluteModelClass(self.getModel()) + + def _findAbsoluteModelClass(self, absolute_name): + return taurus.Manager().findObjectClass(absolute_name) + + def _findRelativeModelClass(self, relative_name): + parent_widget = self.getParentTaurusComponent() + if parent_widget is None: + return None + + parent_obj = parent_widget.getModelObj() + if parent_obj is None: + return None + + if relative_name is None or len(relative_name) == 0: + return parent_widget.getModelClass() + + obj = parent_obj.getChildObj(relative_name) + if obj is None: + return None + if isinstance(obj, TaurusConfigurationProxy): + return obj.getRealConfigClass() + else: + return obj.__class__ + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Model related methods + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def setModelName(self, modelName, parent=None): + """This method will detach from the previous taurus model (if any), it + will set the new model to the given modelName and it will attach + this component to the new taurus model. + + :param modelName: (str) the new taurus model name (according to the taurus convention) + :param parent: (TaurusBaseComponent) the parent or None (default) if this + component does not have a parent Taurus component + """ + modelName = str(modelName) + if parent: + modelClass = self.getModelClass() + if not modelClass is None: + parent_model = self.getParentModelObj() + modelName = modelClass.buildModelName(parent_model, modelName) + self._detach() + self.modelName = modelName + self._attach() + + def getModelName(self): + """Returns the current model name. + + :return: (str) the model name + """ + return self.modelName + + def getParentModelName(self): + """Returns the parent model name or an empty string if the component + has no parent + + :return: (str) the parent model name + """ + try: + p = self.getParentTaurusComponent() + if p is None: + return '' + except: + return '' + return p.getModelName() + + def getParentModelObj(self): + """Returns the parent model object or None if the component has no + parent or if the parent's model is None + + :return: (taurus.core.taurusmodel.TaurusModel or None) the parent taurus model object + """ + try: + p = self.getParentTaurusComponent() + if p is None: + return None + except: + return None + return p.getModelObj() + + def getModelObj(self): + """Returns the taurus model obj associated with this component or None if + no taurus model is associated. + + :return: (taurus.core.taurusmodel.TaurusModel or None) the taurus model object + """ + return self.modelObj + + def getModelType(self): + """Returns the taurus model type associated with this component or + taurus.core.taurusbasetypes.TaurusElementType.Unknown if no taurus model is associated. + + :return: (taurus.core.taurusbasetypes.TaurusElementType) the taurus model type + """ + model_obj = self.getModelObj() + if model_obj is None: + return TaurusElementType.Unknown + return model_obj.getTaurusElementType() + + def getModelValueObj(self,cache=True): + """Returns the tango obj value associated with this component or None + if no taurus model is associated. + + :param cache: (bool) if set to True (default) use the cache value. If set to + False will force a connection to the server. + + :return: (PyTango.DeviceAttribute) the tango value object. + """ + if self.modelObj is None: + return None + return self.modelObj.getValueObj(cache=cache) + + def getFormatedToolTip(self,cache=True): + """Returns a string with contents to be displayed in a tooltip. + + :param cache: (bool) if set to True (default) use the cache value. If set to + False will force a connection to the server. + + :return: (str) a tooltip + """ + if self.modelObj is None: + return self.getNoneValue() + obj = self.modelObj.getDisplayDescrObj() + return self.toolTipObjToStr(obj) + + def toolTipObjToStr(self, toolTipObj): + """Converts a python dict to a tooltip string. + + :param toolTipObj: (dict) a python object + + :return: (str) a tooltip + """ + if toolTipObj is None: + return self.getNoneValue() + ret = '<TABLE width="500" border="0" cellpadding="1" cellspacing="0">' + + for id,value in toolTipObj: + ret += '<TR><TD WIDTH="80" ALIGN="RIGHT" VALIGN="MIDDLE"><B>%s:</B></TD><TD>%s</TD></TR>' % (id.capitalize(), value) + ret += '</TABLE>' + return ret + + def displayValue(self, v): + """Returns a string representation of the given value + + :param v: (object) the value to be translated to string + + :return: (str) a string representing the given value + """ + if self.modelObj is None: + return str(v) + ret = self.modelObj.displayValue(v) + if ret is None: ret = self.getNoneValue() + return ret + + def getDisplayValue(self, cache=True): + """Returns a string representation of the model value associated with + this component. + + :param cache: (bool) if set to True (default) use the cache value. If set to + False will force a connection to the server. + + :return: (str) a string representation of the model value. + """ + if self.modelObj is None: + return self.getNoneValue() + + ret = self.modelObj.getDisplayValue(cache) + if ret is None: + return self.getNoneValue() + return ret + + def setNoneValue(self, v): + """Sets the new string representation when no model or no model value exists. + + :param v: (str) the string representation for an invalid value + """ + self.noneValue = v + + def getNoneValue(self): + """Returns the current string representation when no valid model or model value exists. + + :return: (str) a string representation for an invalid value + """ + return self.noneValue + + def isChangeable(self): + """Tells if this component value can be changed by the user. Default implementation + will return True if and only if: + + - this component is attached to a valid taurus model and + - the taurus model is writable and + - this component is not read-only + + :return: (bool) True if this component value can be changed by the user or False otherwise + """ + res = False + if not self.modelObj is None: + res = self.modelObj.isWritable() + res = res and not self.isReadOnly() + return res + + def isReadOnly(self): + """Determines if this component is read-only or not in the sense that the + user can interact with it. Default implementation returns True. + + Override when necessary. + + :return: (bool) whether or not this component is read-only + """ + return True + + def isAttached(self): + """Determines if this component is attached to the taurus model. + + :return: (bool) True if the component is attached or False otherwise. + """ + return self._attached + + def preAttach(self): + """Called inside self.attach() before actual attach is performed. + Default implementation just emits a signal. + + Override when necessary. + """ + try: Qt.QObject.connect(self.getSignaller(), Qt.SIGNAL('taurusEvent'), self.filterEvent) + except: pass #self.error("In %s.preAttach() ... failed!" % str(type(self))) + + def postAttach(self): + """Called inside self.attach() after actual attach is performed. + Default implementation does not do anything. + + Override when necessary. + """ + pass + + def preDetach(self): + """Called inside self.detach() before actual deattach is performed. + Default implementation just disconnects a signal. + + Override when necessary. + """ + try: Qt.QObject.disconnect(self.getSignaller(), Qt.SIGNAL('taurusEvent'), self.filterEvent) + except: pass #self.error("In %s.preDetach() ... failed!" % str(type(self))) + + + def postDetach(self): + """Called inside self.detach() after actual deattach is performed. + Default implementation does not do anything. + + Override when necessary. + """ + pass + + def _attach(self): + """Attaches the component to the taurus model. + In general it should not be necessary to overwrite this method in a subclass. + + :return: (bool) True if success in attachment or False otherwise. + """ + if self.isAttached(): + return self._attached + + self.preAttach() + + cls = self.getModelClass() + + if cls is None: + self._attached = False + #self.trace("Failed to attach: Model class not found") + elif self.modelName == '': + self._attached = False + self.modelObj = None + else: + try: + self.modelObj = taurus.Manager().getObject(cls, self.modelName) + if not self.modelObj is None: + self.modelObj.addListener(self) + self._attached = True + self.changeLogName(self.log_name + "." + self.modelName) + except Exception: + self.modelObj = None + self._attached = False + self.debug("Exception occured while trying to attach '%s'" % self.modelName) + self.traceback() + + self.postAttach() + return self._attached + + def _detach(self): + """Detaches the component from the taurus model""" + self.preDetach() + + if self.isAttached(): + m = self.getModelObj() + if not m is None: + m.removeListener(self) + + pos = self.log_name.find('.') + if pos >= 0: + new_log_name = self.log_name[:self.log_name.rfind('.')] + self.changeLogName(new_log_name) + self.modelObj = None + self._attached = False + self.fireEvent(m, TaurusEventType.Change, None) + + self.postDetach() + + def setModelInConfig(self, yesno): + ''' + Sets whether the model-related properties should be stored for this + widget when creating the config dict with :meth:`createConfig` (and + restored when calling :meth:`applyConfig`). + By default this is not enabled. + The following properties are affected by this: + - "model" + + :param yesno: (bool) If True, the model-related properties will be + registered as config properties. If False, they will be + unregistered. + + .. seealso:: :meth:`registerConfigProperty`, :meth:`createConfig`, + :meth:`applyConfig` + + ''' + if yesno == self._modelInConfig: + return + if yesno: + self.registerConfigProperty(self.getModel, self.setModel, "model") + else: + self.unregisterConfigurableItem("model",raiseOnError=False) + self._modelInConfig = yesno + + def getModelInConfig(self): + return self._modelInConfig + + def resetModelInConfig(self): + return self.setModelInConfig(False) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Pending operations related methods + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def applyPendingOperations(self, ops=None): + """Applies operations without caring about danger messages. + Use :meth:`TaurusBaseWidget.safeApplyOperation` if you want to warn the + user before applying + + :param ops: (sequence<taurus.core.taurusoperation.TaurusOperation> or None) list of operations to apply. + If None is given (default) the component fetches the pending operations + """ + self.debug("Apply changes") + if ops is None: + ops = self.getPendingOperations() + + if self.isAutoProtectOperation(): + import taurus.qt.qtgui.dialog + @taurus.qt.qtgui.dialog.protectTaurusMessageBox + def go(): + self.getTaurusManager().applyPendingOperations(ops) + go() + else: + self.getTaurusManager().applyPendingOperations(ops) + + def hasPendingOperations(self): + """Returns if the component has pending operations + + :return: (bool) True if there are pending operations or False otherwise + """ + return len(self.getPendingOperations()) > 0 + + def getPendingOperations(self): + """Returns the sequence of pending operations + + :return: (sequence<taurus.core.taurusoperation.TaurusOperation>) a list of pending operations + """ + return self._operations + + def resetPendingOperations(self): + """Clears the list of pending operations""" + self._operations = [] + + def setDangerMessage(self, dangerMessage=""): + """Sets the danger message when applying an operation. If dangerMessage is None, + the apply operation is considered safe + + :param dangerMessage: (str or None) the danger message. If None is given (default) + the apply operation is considered safe + """ + self._dangerMessage = dangerMessage + self._isDangerous = len(dangerMessage) > 0 + + def getDangerMessage(self): + """Returns the current apply danger message or None if the apply operation is safe + + :return: (str or None) the apply danger message + """ + return self._dangerMessage + + def resetDangerMessage(self): + """Clears the danger message. After this method is executed the apply operation + for this component will be considered safe.""" + self.setDangerMessage(None) + + def isDangerous(self): + """Returns if the apply operation for this component is dangerous + + :return: (bool) wheter or not the apply operation for this component is dangerous + """ + return self._isDangerous + + def setForceDangerousOperations(self, yesno): + """Forces/clears the dangerous operations + + :param yesno: (bool) force or not the dangerous operations""" + self._forceDangerousOperations = yesno + + def getForceDangerousOperations(self): + """Returns if apply dangerous operations is forced + + :return: (bool) wheter or not apply dangerous operations is forced + """ + return self._forceDangerousOperations + + def resetForceDangerousOperations(self): + """Clears forcing apply dangerous operations""" + self.setForceDangerousOperations(False) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Standard Qt properties + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def getModel(self): + """Returns the model name for this component. + + :return: (str) the model name. + """ + return self._localModelName + + def setModel(self,model): + """Sets/unsets the model name for this component + + :param model: (str) the new model name""" + self.setModelCheck(model) + self.updateStyle() + + def setModelCheck(self,model,check=True): + """Sets the component taurus model. Setting the check argument to True + (default) will check if the current model is equal to the given argument. + If so then nothing is done. This should be the standard way to call this + method since it will avoid recursion. + + :param model: (str) the new model name + :param check: (bool) whether or not to check against the actual model name""" + if model is None: model = '' + model = str(model) + if check == True and model == self._localModelName: + return + + self._localModelName = model + +# # if in offline mode don't bother trying to register +# opMode = taurus.core.taurusmanager.TaurusManager().getOperationMode() +# if opMode == taurus.core.taurusbasetypes.OperationMode.OFFLINE: +# return + + parent_widget = None + try: + # if this widget has a buddy, check to see if it is a valid TaurusWidget + buddy_func = getattr(self,'buddy') + buddy_widget = buddy_func() + if buddy_widget and isinstance(buddy_widget, TaurusBaseComponent): + parent_widget = buddy_widget + elif self.getUseParentModel(): + parent_widget = self.getParentTaurusComponent() + except: + if self.getUseParentModel(): + parent_widget = self.getParentTaurusComponent() + self.setModelName(model, parent_widget) + #self.fireEvent(self.getModelObj(), taurus.core.taurusbasetypes.TaurusEventType.Change, self.getModelValueObj()) + + def resetModel(self): + """Sets the model name to the empty string""" + self.setModel('') + + def getUseParentModel(self): + """Returns whether this component is using the parent model + + :return: (bool) True if using parent model or False otherwise + """ + return getattr(self, '_useParentModel', False) + + @Qt.pyqtSignature("setUseParentModel(bool)") + def setUseParentModel(self, yesno): + """Sets/unsets using the parent model + + :param yesno: (bool) whether or not to use parent model + """ + if yesno == self._useParentModel: + return + self._useParentModel = yesno + # force a recalculation of the model + self.setModelCheck(self.getModel(),False) + + def resetUseParentModel(self): + """Resets the usage of parent model to False""" + self.setUseParentModel(False) + self.updateStyle() + + @Qt.pyqtSignature("setShowQuality(bool)") + def setShowQuality(self,showQuality): + """Sets/unsets the show quality property + + :param showQuality: (bool) whether or not to show the quality + """ + if showQuality == self._showQuality: + return + self._showQuality = showQuality + self.updateStyle() + + def getShowQuality(self): + """Returns if showing the quality as a background color + + :return: (bool) True if showing the quality or False otherwise + """ + return self._showQuality + + def resetShowQuality(self): + """Resets the show quality to self.__class__._showQuality""" + self.setShowQuality(self.__class__._showQuality) + + @Qt.pyqtSignature("setShowText(bool)") + def setShowText(self, showText): + """Sets/unsets showing the display value of the model + + :param showText: (bool) whether or not to show the display value + """ + if showText == self._showText: + return + self._showText = showText + self.fireEvent(self.getModelObj(), TaurusEventType.Change, self.getModelValueObj()) + self.updateStyle() + + def getShowText(self): + """Returns if showing the display value + + :return: (bool) True if showing the display value or False otherwise + """ + return self._showText + + def resetShowText(self): + """Resets the showing of the display value to True""" + self.setShowText(True) + + def setTaurusPopupMenu(self, menuData): + """Sets/unsets the taurus popup menu + + :param menuData: (str) an xml representing the popup menu""" + self.taurusMenuData = str(menuData) + factory = ActionFactory() + self.taurusMenu = factory.getNewMenu(self, self.taurusMenuData) + + def getTaurusPopupMenu(self): + """Returns an xml string representing the current taurus popup menu + + :return: (str) an xml string representing the current taurus popup menu + """ + return self.taurusMenuData + + def resetTaurusPopupMenu(self): + """Resets the taurus popup menu to empty""" + self.taurusMenuData = '' + + def isModifiableByUser(self): + '''whether the user can change the contents of the widget + + :return: (bool) True if the user is allowed to modify the look&feel''' + return self._modifiableByUser + + def setModifiableByUser(self, modifiable): + ''' + sets whether the user is allowed to modify the look&feel + + :param modifiable: (bool) + ''' + self._modifiableByUser = modifiable + + def resetModifiableByUser(self): + '''Equivalent to setModifiableByUser(self.__class__._modifiableByUser)''' + self.setModifiableByUser(self.__class__._modifiableByUser) + + def resetAutoProtectOperation(self): + """Resets protecting operations""" + self.setAutoProtectOperation(True) + + def isAutoProtectOperation(self): + """Tells if this widget's operations are protected against exceptions + + :return: (bool) True if operations are protected against exceptions or + False otherwise""" + return self._autoProtectOperation + + def setAutoProtectOperation(self, protect): + """Sets/unsets this widget's operations are protected against exceptions + + :param protect: wheater or not to protect widget operations + :type protect: bool""" + self._autoProtectOperation = protect + + +class TaurusBaseWidget(TaurusBaseComponent): + """The base class for all Qt Taurus widgets. + + .. note:: + Any class which inherits from TaurusBaseWidget is expected to also + inherit from QWidget (or from a QWidget derived class)""" + + ModelChangedSignal = 'modelChanged(const QString &)' + _dragEnabled = False + + def __init__(self, name, parent=None, designMode=False): + self._disconnect_on_hide = False + self._supportedMimeTypes = None + self._autoTooltip = True + self.call__init__(TaurusBaseComponent, name, parent=parent, designMode=designMode) + self._setText = self._findSetTextMethod() + + ## It makes the GUI to hang... If this needs implementing, we should + ## reimplement it using the Qt parent class, not QWidget... + #def destroy(self): + # '''Reimplements the Qt.QWidget destroy method to ensure that this object + # stops listening its model.''' + # self.setUseParentModel(False) + # self.resetModel() + # Qt.QWidget.destroy(self) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Helper methods + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def getQtClass(self, bases=None): + """Returns the parent Qt class for this widget + + :param bases: (sequence<class> or None) the list of class objects. If None + is given (default) it uses the object base classes from __bases__ + + :return: (QWidget class) the QWidget class object + """ + bases = bases or self.__class__.__bases__ + for klass in bases: + is_taurusbasewidget = issubclass(klass, TaurusBaseWidget) + if issubclass(klass, Qt.QWidget): + if is_taurusbasewidget: + return self.getQtClass(klass.__bases__) + return klass + elif is_taurusbasewidget: + return self.getQtClass(klass.__bases__) + return None + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Qt properties from TaurusBaseComponent that need to be overwritten + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + _UseParentMsg = False + + @Qt.pyqtSignature("setUseParentModel(bool)") + def setUseParentModel(self, yesno): + """Sets/unsets using the parent model. + + .. note:: in some cases you may need to call :meth:`recheckTaurusParent` + after reparenting of some of this widget's ancestors + + :param yesno: (bool) whether or not to use parent model + + .. seealso:: :meth:`recheckTaurusParent` + """ + is_same = yesno == self._useParentModel + if not is_same: + self._updateUseParentModel(yesno) + if yesno and self._designMode and not TaurusBaseWidget._UseParentMsg: + TaurusBaseWidget._UseParentMsg = True + Qt.QMessageBox.information(self, "UseParentModel usage note", + "Using the UseParentModel feature may require you to call "+\ + "recheckTaurusParent() manually for this widget after calling " +\ + "setupUi in your code."+\ + "See the documentation of TaurusBaseWidget.recheckTaurusParent()") + TaurusBaseComponent.setUseParentModel(self, yesno) + + def _updateUseParentModel(self, yesno): + parent_widget = self.getParentTaurusComponent() + if parent_widget: + if yesno: + self.connect(parent_widget, + Qt.SIGNAL(TaurusBaseWidget.ModelChangedSignal), + self.parentModelChanged) + else: + self.disconnect(parent_widget, + Qt.SIGNAL(TaurusBaseWidget.ModelChangedSignal), + self.parentModelChanged) + + def recheckTaurusParent(self): + ''' + Forces the widget to recheck its Taurus parent. Taurus Widgets will in most + situations keep track of changes in their taurus parenting, but in some + special cases (which unfortunately tend to occur when using Qt + Designer) they may not update it correctly. + + If this happens, you can manually call this method. + + For more information, check the :download:`issue demo example + </devel/examples/parentmodel_issue_demo.py>` + ''' + self._updateUseParentModel(True) + + def setModelCheck(self,model,check=True): + """Sets the component taurus model. Setting the check argument to True + (default) will check if the current model is equal to the given argument. + If so then nothing is done. This should be the standard way to call this + method since it will avoid recursion. + + :param model: (str) the new model name + :param check: (bool) whether or not to check against the actual model name + """ + if model is None: model = '' + model = str(model) + send_signal = (model != self._localModelName) + TaurusBaseComponent.setModelCheck(self, model, check) + + if send_signal: + # emit a signal informing the child widgets that the model has changed + self.emit(Qt.SIGNAL(TaurusBaseWidget.ModelChangedSignal), model) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Default Qt signal handlers. Overwrite them as necessary + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def changeEvent(self, evt): + """overwrites QWidget.changeEvent(self, evt) to handle the ParentChangeEvent + in case this widget is using the parent model. Always calls the QWidget.changeEvent + in order not to lose events + """ + if self.getUseParentModel(): + evt_type = evt.type() + if evt_type == Qt.QEvent.ParentChange: + # disconnect from old parent + if self._parentTaurusComponent: + self.disconnect(self._parentTaurusComponent, + Qt.SIGNAL(TaurusBaseWidget.ModelChangedSignal), + self.parentModelChanged) + self._updateUseParentModel(True) + self.setModelCheck(self.getModel(), False) + self.getQtClass().changeEvent(self, evt) + + def parentModelChanged(self, parentmodel_name): + """Invoked when the Taurus parent model changes + + :param parentmodel_name: (str) the new name of the parent model + """ + self.debug("Parent model changed to '%s'" % parentmodel_name) + parentmodel_name = str(parentmodel_name) + if self.getUseParentModel(): + # force an update of the interpretation of the model property + model = self.getModel() + self.setModelCheck(model,False) + self.emit(Qt.SIGNAL(TaurusBaseWidget.ModelChangedSignal), model) + else: + self.debug("received event from parent although not using parent model") + + def handleEvent(self, evt_src, evt_type, evt_value): + """very basic and generalistic handling of events. + + Override when necessary. + + :param evt_src: (object or None) object that triggered the event + :param evt_type: (taurus.core.taurusbasetypes.TaurusEventType or None) type of event + :param evt_value: (object or None) event value + """ + #Update the text shown by the widget + if self._setText: + text = '' + if self.getShowText(): + if isinstance(evt_src, TaurusAttribute): + if evt_type in (TaurusEventType.Change, TaurusEventType.Periodic): + text = self.displayValue(evt_value.value) + elif evt_type == TaurusEventType.Error: + text = self.getNoneValue() + elif evt_type == TaurusEventType.Config: + text = self.getDisplayValue() + else: + text = self.getDisplayValue() + self._setText(text) + + #update tooltip + if self._autoTooltip: + self.setToolTip(self.getFormatedToolTip()) + + #TODO: update whatsThis + + #update appearance + self.updateStyle() + + def setModelInConfig(self, yesno): + ''' + extends :meth:`TaurusBaseComponent.setModelInConfig` to include also + the "useParentModel" property + + .. seealso:: :meth:`TaurusBaseComponent.setModelInConfig` + ''' + if yesno == self._modelInConfig: + return + if yesno: + self.registerConfigProperty(self.getUseParentModel, self.setUseParentModel, "useParentModel") + else: + self.unregisterConfigurableItem("useParentModel",raiseOnError=False) + + TaurusBaseComponent.setModelInConfig(self, yesno) + + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Mandatory overwrite from TaurusBaseComponent + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def updateStyle(self): + """Updates the widget style. Default implementation just calls QWidget.update() + + Override when necessary. + """ + self.update() + + def getParentTaurusComponent(self): + """Returns the first taurus component in the widget hierarchy or None if no + taurus component is found + + :return: (TaurusBaseComponent or None) the parent taurus base component + """ + p = self.parentWidget() + while p and not isinstance(p, TaurusBaseWidget): + p = p.parentWidget() + if isinstance(p, TaurusBaseWidget): + self._parentTaurusComponent = p + else: + self._parentTaurusComponent = p = None + return p + + def setDisconnectOnHide(self, disconnect): + """Sets/unsets disconnection on hide event + + :param disconnect: (bool) whether or not to disconnect on hide event + """ + if not self.visible() and disconnect == False: + self.info("Ignoring setDisconnectOnHide to False because widget is not visible") + return + self._disconnect_on_hide = disconnect + + def hideEvent(self, event): + """Override of the QWidget.hideEvent() + """ + if self._disconnect_on_hide: + try: + if self.getModelName(): + self._detach() + event.accept() + except Exception: + self.warning("Exception received while trying to hide") + self.traceback() + + def showEvent(self, event): + """Override of the QWidget.showEvent()""" + if self._disconnect_on_hide: + try: + if self.getModelName(): + self._attach() + event.accept() + except Exception: + self.warning("Exception received while trying to show") + self.traceback() + + def closeEvent(self, event): + """Override of the QWidget.closeEvent()""" + try: + self._detach() + event.accept() + except Exception: + self.warning("Exception received while trying to close") + self.traceback() + + def handleException(self, e): + for h in self._exception_listener: + h.exceptionReceived(e) + + def _findSetTextMethod(self): + """Determine if this widget is able to display the text value of the taurus + model. It searches through the possible Qt methods to display text. + + :return: (callable) a python method or None if no suitable method is found. + """ + setMethod = None + try: + setMethod = getattr(self,'setText') + except AttributeError: + try: + setMethod = getattr(self,'setTitle') + except AttributeError: + try: + setMethod = getattr(self,'display') + except AttributeError: + # it seems the widget has no way to update a value + pass + + return setMethod + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Drag&Drop related methods: + # default implementation allows setting the model by dropping it on the + # widget (if the widget allows modifications by the user). + # + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def setModifiableByUser(self, modifiable): + '''Reimplemented to acept/reject drops based on whether the widget is modifiable by the user. + See :meth:`TaurusBaseComponent.setModifiableByUser()`''' + TaurusBaseComponent.setModifiableByUser(self, modifiable) + self.setAcceptDrops(modifiable) + + def getSupportedMimeTypes(self): + ''' + returns a list of supported mimeTypes that this widget support (ordered + by priority). If none is set explicitely via :meth:`setSupportedMimeTypes`, + a best effort will be tried based on the model class + + ..seealso: :meth:`setSupportedMimeTypes` + + This provides only a very basic implementation. Reimplement in derived classes if needed + + :return: (list<str>) list of MIME type names + ''' + if self._supportedMimeTypes is not None: + return self._supportedMimeTypes + #fallback guess based on modelclass + try: + modelclass = self.getModelClass() + except: + return [] + if modelclass == TaurusDevice: + return [TAURUS_DEV_MIME_TYPE, TAURUS_MODEL_MIME_TYPE] + elif modelclass == TaurusAttribute: + return [TAURUS_ATTR_MIME_TYPE, TAURUS_MODEL_MIME_TYPE] + else: + return [TAURUS_MODEL_MIME_TYPE] + + def setSupportedMimeTypes(self, mimetypes): + ''' + sets the mimeTypes that this widget support + + :param mimetypes: (list<str>) list (ordered by priority) of MIME type names + ''' + self._supportedMimeTypes = mimetypes + + def dragEnterEvent(self, event): + '''reimplemented to support drag&drop of models. See :class:`QWidget`''' + if self.isModifiableByUser(): + supported = self.getSupportedMimeTypes() + for f in event.mimeData().formats(): + if f in supported: + event.acceptProposedAction() + return + + def getDropEventCallback(self): + '''returns the method to be called when a dropping event occurs. + The default implementation returns `self.setModel`. Reimplement + it subclasses to call different methods. + + :return: (callable) + ''' + return self.setModel + + def dropEvent(self, event): + '''reimplemented to support drag&drop of models. See :class:`QWidget`''' + mtype = self.handleMimeData(event.mimeData(),self.getDropEventCallback()) + if mtype is None: + self.info('Invalid model') + else: + event.acceptProposedAction() + + def handleMimeData(self, mimeData, method): + '''Selects the most appropriate data from the given mimeData object + (in the order returned by :meth:`getSupportedMimeTypes`) and passes + it to the given method. + + :param mimeData: (QMimeData) the MIME data object from which the model + is to be extracted + :param method: (callable<str>) a method that accepts a string as argument. + This method will be called with the data from the mimeData object + + :return: (str or None) returns the MimeType used if the model was + successfully set, or None if the model could not be set + ''' + supported = self.getSupportedMimeTypes() + formats = mimeData.formats() + for mtype in supported: + if mtype in formats: + d = str(mimeData.data(mtype)) + if d is None: + return None + try: + method(d) + return mtype + except: + self.debug('Invalid data (%s) for MIMETYPE=%s'%(repr(d), repr(mtype))) + self.traceback(taurus.Debug) + return None + + def getModelMimeData(self): + '''Returns a MimeData object containing the model data. The default implementation + fills the `TAURUS_MODEL_MIME_TYPE`. If the widget's Model class is + Attribute or Device, it also fills `TAURUS_ATTR_MIME_TYPE` or + `TAURUS_DEV_MIME_TYPE`, respectively + + :return: (QMimeData) + ''' + mimeData = Qt.QMimeData() + modelname = self.getModelName() + mimeData.setData(TAURUS_MODEL_MIME_TYPE, modelname) + try: + modelclass = self.getModelClass() + except: + modelclass = None + if issubclass(modelclass, TaurusDevice): + mimeData.setData(TAURUS_DEV_MIME_TYPE, modelname) + elif issubclass(modelclass, TaurusAttribute): + mimeData.setData(TAURUS_ATTR_MIME_TYPE, modelname) + return mimeData + + def mousePressEvent(self, event): + '''reimplemented to record the start position for drag events. + See :class:`~PyQt4.QtGui.QWidget`''' + if self._dragEnabled and event.button() == Qt.Qt.LeftButton: + self.dragStartPosition = Qt.QPoint(event.pos()) #I need to copy it explicetely to avoid a bug with PyQt4.4 + self.getQtClass().mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + '''reimplemented to provide drag events. + See :class:`~PyQt4.QtGui.QWidget`''' + if not self._dragEnabled or not event.buttons() & Qt.Qt.LeftButton: + return self.getQtClass().mouseMoveEvent(self, event) + if (event.pos() - self.dragStartPosition).manhattanLength() < Qt.QApplication.startDragDistance(): + return self.getQtClass().mouseMoveEvent(self, event) + ret = self.getQtClass().mouseMoveEvent(self, event) #call the superclass + event.accept() #we make sure we accept after having called the superclass so that it is not propagated (many default implementations of mouseMoveEvent call event.ignore()) + drag = Qt.QDrag(self) + drag.setMimeData(self.getModelMimeData()) + drag.exec_(Qt.Qt.CopyAction, Qt.Qt.CopyAction) + return ret + + def isDragEnabled(self): + '''whether the user can drag data from this widget + + :return: (bool) True if the user can drag data''' + return self._dragEnabled + + def setDragEnabled(self, enabled): + ''' + sets whether the user is allowed to drag data from this widget + + :param modifiable: (bool) + ''' + self._dragEnabled = enabled + + def resetDragEnabled(self): + '''Equivalent to setDragEnabled(self.__class__._dragEnabled)''' + self.setModifiableByUser(self.__class__._dragEnabled) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Pending operations related methods: default implementation + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def updatePendingOpsStyle(self): + """This method should be reimplemented by derived classes that want to + change their appearance depending whether there are pending operations or not""" + pass + + def emitValueChanged(self, *args): + """Connect the specific XXXXChanged signals from derived classes to this + method in order to have a unified signal which can be used by Taurus Widgets""" + self.emit(Qt.SIGNAL('valueChanged')) + self.updatePendingOpsStyle() #by default, update its own style + + def safeApplyOperations(self, ops = None): + """Applies the given operations (or the pending operations if None passed) + + :param ops: (sequence<taurus.core.taurusoperation.TaurusOperation> or None) list of operations to apply. + If None is given (default) the component fetches the pending operations + + :return: (bool) False if the apply was aborted by the user. True otherwise. + """ + + if ops is None: ops = self.getPendingOperations() + + #Check if we need to take care of dangerous operations + if self.getForceDangerousOperations(): dangerMsgs = [] + else: dangerMsgs = [op.getDangerMessage() for op in ops if len(op.getDangerMessage()) > 0] + #warn the user if need be + if len(dangerMsgs)==1: + result = Qt.QMessageBox.warning(self, "Potentially dangerous action", + "%s\nProceed?"%dangerMsgs[0], + Qt.QMessageBox.Ok|Qt.QMessageBox.Cancel, + Qt.QMessageBox.Ok) + if result != Qt.QMessageBox.Ok: + return False + + elif len(dangerMsgs)>1: + warningDlg = Qt.QMessageBox(Qt.QMessageBox.Warning, " %d potentially dangerous actions"%len(dangerMsgs), + "You are about to apply %d actions that may be potentially dangerous. Proceed?"%len(dangerMsgs), + Qt.QMessageBox.Ok|Qt.QMessageBox.Cancel, + self) + details = "\n".join(dangerMsgs) + warningDlg.setDetailedText(details) + result = warningDlg.exec_() + if result != Qt.QMessageBox.Ok: + return False + self.applyPendingOperations(ops) + return True + + def setAutoTooltip(self, yesno): + """Determines if the widget should automatically generate a tooltip + based on the current widget model. + + :param yesno: (bool) True to automatically generate tooltip or False otherwise + """ + self._autoTooltip = yesno + + def getAutoTooltip(self): + """Returns if the widget is automatically generating a tooltip based + on the current widget model. + + :return: (bool) True if automatically generating tooltip or False otherwise + """ + return self._autoTooltip + + @classmethod + def getQtDesignerPluginInfo(cls): + """Returns pertinent information in order to be able to build a valid + QtDesigner widget plugin. + + The dictionary returned by this method should contain *at least* the + following keys and values: + - 'module' : a string representing the full python module name (ex.: 'taurus.qt.qtgui.base') + - 'icon' : a string representing valid resource icon (ex.: ':/designer/combobox.png') + - 'container' : a bool telling if this widget is a container widget or not. + + This default implementation returns the following dictionary:: + + { 'group' : 'Taurus [Unclassified]', + 'icon' : ':/designer/taurus.png', + 'container' : False } + + :return: (dict) a map with pertinent designer information""" + return { + 'group' : 'Taurus [Unclassified]', + 'icon' : ':/designer/taurus.png', + 'container' : False } + + +class TaurusBaseWritableWidget(TaurusBaseWidget): + """The base class for all taurus input widgets + + it emits the applied signal when the value has been applied. + """ + + appliedSignalSignature = 'applied' + + def __init__(self, name, taurus_parent=None, designMode = False): + self.call__init__(TaurusBaseWidget, name, parent=taurus_parent, designMode=designMode) + + self._lastValue = None + + # Overwrite not to show quality by default + self._showQuality = False + + # Don't do auto-apply by default + self._autoApply = False + + # Don't force a writing to attribute when there are not pending operations + self._forcedApply = False + + self.connect(self, Qt.SIGNAL('valueChanged'), self.updatePendingOperations) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # TaurusBaseWidget overwriting + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def getModelClass(self): + '''reimplemented from :class:`TaurusBaseWidget`''' + return TaurusAttribute + + def isReadOnly(self): + '''reimplemented from :class:`TaurusBaseWidget`''' + return False + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # QT properties + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def setAutoApply(self, auto): + ''' + Sets autoApply mode. In autoApply mode, the widget writes the value + automatically whenever it is changed by the user (e.g., when + :meth:`valueChanged` is called). If False, a value changed just + flags a "pending operation" which needs to be applied manually by + the user before the value gets written. + + :param auto: (bool) True for setting autoApply mode. False for disabling + ''' + self._autoApply = auto + + def getAutoApply(self): + '''whether autoApply mode is enabled or not. + + :return: (bool) + ''' + return self._autoApply + + def resetAutoApply(self): + '''resets the autoApply mode (i.e.: sets it to False)''' + self.setAutoApply(False) + + def setForcedApply(self, forced): + '''Sets the forcedApply mode. In forcedApply mode, values are written even + if there are not pending operations (e.g. even if the displayed value is + the same as the currently applied one). + + .. seealso: :meth:`forceApply` and :meth:`writeValue` + + :param forced: (bool) True for setting forcedApply mode. False for disabling + ''' + self._forcedApply = forced + + def getForcedApply(self): + '''whether forcedApply mode is enabled or not. + + :return: (bool) + ''' + return self._forcedApply + + def resetForcedApply(self): + '''resets the forcedApply mode (i.e.: sets it to False)''' + self.setForcedApply(False) + + def valueChanged(self, *args): + '''Subclasses should connect some particular signal to this method for + indicating that something has changed. + e.g., a QLineEdit should connect its "textChanged" signal... + ''' + self.emitValueChanged() + if self._autoApply: + self.writeValue() + + def writeValue(self, forceApply=False): + '''Writes the value to the attribute, either by applying pending + operations or (if the ForcedApply flag is True), it writes directly when + no operations are pending + + It emits the applied signal if apply is not aborted. + + :param forceApply: (bool) If True, it behaves as in forceApply mode + (even if the forceApply mode is disabled by + :meth:`setForceApply`) + ''' + + if self.hasPendingOperations(): + applied = self.safeApplyOperations() + if applied: + self.emit(Qt.SIGNAL(self.appliedSignalSignature)) + return + + #maybe we want to force an apply even if there are no pending ops... + kmods = Qt.QCoreApplication.instance().keyboardModifiers() + controlpressed = bool(kmods&Qt.Qt.ControlModifier) + if self.getForcedApply() or forceApply or controlpressed: + self.forceApply() + + def forceApply(self): + '''It (re)applies the value regardless of pending operations. + WARNING: USE WITH CARE. In most cases what you need is to make sure + that pending operations are properly created, not calling this method + + It emits the applied signal if apply is not aborted. + + .. seealso: :meth:`setForceApply` and :meth:`writeValue` + + ''' + try: + v = self.getValue() + op = WriteAttrOperation(self.getModelObj(), v, + self.getOperationCallbacks()) + op.setDangerMessage(self.getDangerMessage()) + applied = self.safeApplyOperations([op]) + if applied: + self.emit(Qt.SIGNAL(self.appliedSignalSignature)) + self.info('Force-Applied value = %s'%str(v)) + except: + self.error('Unexpected exception in forceApply') + self.traceback() + + def handleEvent(self, src, evt_type, evt_value): + '''reimplemented from :class:`TaurusBaseWidget`''' + if evt_type in (TaurusEventType.Change, TaurusEventType.Periodic): + self.emitValueChanged() + + def postAttach(self): + '''reimplemented from :class:`TaurusBaseWidget`''' + TaurusBaseWidget.postAttach(self) + if self.isAttached(): + try: + v = self.getModelValueObj().w_value + except: + v = None + self.setValue(v) + + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + # Pending operations related methods + #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- + + def resetPendingOperations(self): + '''reimplemented from :class:`TaurusBaseWidget`''' + if self.isAttached(): + try: + v = self.getModelValueObj().w_value + except: + v = None + self.setValue(v) + TaurusBaseWidget.resetPendingOperations(self) + self.updateStyle() + + def updatePendingOperations(self): + '''reimplemented from :class:`TaurusBaseWidget`''' + model = self.getModelObj() + try: + model_value = model.getValueObj().w_value + wigdet_value = self.getValue() + if model.areStrValuesEqual(model_value, wigdet_value): + self._operations = [] + else: + operation = WriteAttrOperation(model, wigdet_value, + self.getOperationCallbacks()) + operation.setDangerMessage(self.getDangerMessage()) + self._operations = [operation] + except: + self._operations = [] + self.updateStyle() + + def getOperationCallbacks(self): + '''returns the operation callbacks (i.e., a sequence of methods that will be called after an operation is executed + (this default implementation it returns an empty list). + + :return: (sequence<callable>) + ''' + return [] + + def getValue(self): + ''' + This method must be implemented in derived classes to return + the value to be written. Note that this may differ + from the displayed value (e.g. for a numeric value being + edited by a QLineEdit-based widget, the displayed value will + be a string while getValue will return a number) + ''' + raise NotImplementedError("Not allowed to call TaurusBaseWritableWidget.getValue()") + + def setValue(self, v): + ''' + This method must be implemented in derived classes to provide + a (widget-specific) way of updating the displayed value based + on a given attribute value + + :param v: The attribute value + ''' + raise NotImplementedError("Not allowed to call TaurusBaseWritableWidget.setValue()") + + def updateStyle(self): + '''reimplemented from :class:`TaurusBaseWidget`''' + if self._autoTooltip: + toolTip = self.getFormatedToolTip() + if self.hasPendingOperations(): + v_str = str(self.getValue()) + model_v_str = getattr(self.getModelValueObj(),'w_value', '-----') + toolTip += '<hr/>Displayed value (%s) differs from applied value (%s)' % (v_str, model_v_str) + self.setToolTip(toolTip) + + def _updateValidator(self, evt_value): + #re-set the validator ranges if applicable + if evt_value is None: return + v = self.validator() + if isinstance(v, Qt.QIntValidator): + bottom = evt_value.min_value + top = evt_value.max_value + bottom = int(bottom) if bottom != TaurusConfiguration.no_min_value else -sys.maxint + top = int(top) if top != TaurusConfiguration.no_max_value else sys.maxint + v.setRange(bottom, top) + self.debug("Validator range set to %i-%i"%(bottom,top)) + elif isinstance(v, Qt.QDoubleValidator): + bottom = evt_value.min_value + top = evt_value.max_value + bottom = float(bottom) if bottom != TaurusConfiguration.no_min_value else -float("inf") + top = float(top) if top != TaurusConfiguration.no_max_value else float("inf") + v.setBottom(bottom) + v.setTop(top) + self.debug("Validator range set to %f-%f"%(bottom,top)) + + @classmethod + def getQtDesignerPluginInfo(cls): + '''reimplemented from :class:`TaurusBaseWidget`''' + ret = TaurusBaseWidget.getQtDesignerPluginInfo() + ret['group'] = 'Taurus Input' + return ret |