#!/usr/bin/env python
# ###########################################################################
#
# 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/>.
#
# ###########################################################################
"""
paneldescriptionwizard.py:
"""
from taurus.external.qt import Qt
import sys
import weakref
from taurus.qt.qtgui.taurusgui.utils import PanelDescription
from taurus.qt.qtgui.icon import getCachedPixmap
from taurus.qt.qtgui.input import GraphicalChoiceWidget
from taurus.qt.qtgui.base import TaurusBaseComponent, TaurusBaseWidget
from taurus.qt.qtcore.communication import SharedDataManager
from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE
from taurus.qt.qtgui.util import TaurusWidgetFactory
from taurus.core.util.log import Logger
import inspect
import copy
class ExpertWidgetChooserDlg(Qt.QDialog):
CHOOSE_TYPE_TXT = "(choose type)"
memberSelected = Qt.pyqtSignal(dict)
def __init__(self, parent=None):
Qt.QDialog.__init__(self, parent)
self.setWindowTitle("Advanced panel type selection")
layout1 = Qt.QHBoxLayout()
layout2 = Qt.QHBoxLayout()
layout = Qt.QVBoxLayout()
# subwidgets
self.moduleNameLE = Qt.QLineEdit()
self.moduleNameLE.setValidator(
Qt.QRegExpValidator(
Qt.QRegExp(r"[a-zA-Z0-9\.\_]*"), self.moduleNameLE
)
)
self.membersCB = Qt.QComboBox()
self.dlgBox = Qt.QDialogButtonBox(
Qt.QDialogButtonBox.Ok | Qt.QDialogButtonBox.Cancel
)
self.dlgBox.button(Qt.QDialogButtonBox.Ok).setEnabled(False)
# layout
layout.addWidget(
Qt.QLabel("Select the module and widget to use in the panel:")
)
layout1.addWidget(Qt.QLabel("Module"))
layout1.addWidget(self.moduleNameLE)
layout2.addWidget(Qt.QLabel("Class (or widget)"))
layout2.addWidget(self.membersCB)
layout.addLayout(layout1)
layout.addLayout(layout2)
layout.addWidget(self.dlgBox)
self.setLayout(layout)
# connections
self.moduleNameLE.editingFinished.connect(self.onModuleSelected)
self.moduleNameLE.textEdited.connect(self.onModuleEdited)
self.membersCB.activated.connect(self.onMemberSelected)
self.dlgBox.accepted.connect(self.accept)
self.dlgBox.rejected.connect(self.reject)
def onModuleEdited(self):
self.dlgBox.button(Qt.QDialogButtonBox.Ok).setEnabled(False)
self.module = None
self.moduleNameLE.setStyleSheet("")
self.membersCB.clear()
def onModuleSelected(self):
modulename = str(self.moduleNameLE.text())
try:
__import__(modulename)
# We use this because __import__('x.y') returns x instead of y !!
self.module = sys.modules[modulename]
self.moduleNameLE.setStyleSheet("QLineEdit {color: green}")
except Exception as e:
Logger().debug(repr(e))
self.moduleNameLE.setStyleSheet("QLineEdit {color: red}")
return
# inspect the module to find the members we want (classes or widgets
# inheriting from QWidget)
members = inspect.getmembers(self.module)
classnames = sorted(
[
n
for n, m in members
if inspect.isclass(m) and issubclass(m, Qt.QWidget)
]
)
widgetnames = sorted(
[n for n, m in members if isinstance(m, Qt.QWidget)]
)
self.membersCB.clear()
self.membersCB.addItem(self.CHOOSE_TYPE_TXT)
self.membersCB.addItems(classnames)
if classnames and widgetnames:
self.membersCB.InsertSeparator(self.membersCB.count())
self.membersCB.addItems(classnames)
def onMemberSelected(self, text):
if str(text) == self.CHOOSE_TYPE_TXT:
return
self.dlgBox.button(Qt.QDialogButtonBox.Ok).setEnabled(True)
# emit a signal with a dictionary that can be used to initialize
self.memberSelected.emit(self.getMemberDescription())
def getMemberDescription(self):
try:
membername = str(self.membersCB.currentText())
member = getattr(self.module, membername, None)
result = {"modulename": self.module.__name__}
except Exception as e:
Logger().debug("Cannot get member description: %s", repr(e))
return None
if inspect.isclass(member):
result["classname"] = membername
else:
result["widgetname"] = membername
return result
@staticmethod
def getDialog():
dlg = ExpertWidgetChooserDlg()
dlg.exec_()
return dlg.getMemberDescription(), (dlg.result() == dlg.Accepted)
class BlackListValidator(Qt.QValidator):
stateChanged = Qt.pyqtSignal(int, int)
def __init__(self, blackList=None, parent=None):
Qt.QValidator.__init__(self, parent)
if blackList is None:
blackList = []
self.blackList = blackList
self._previousState = None
# check the signature of the validate method
# (it changed from old to new versions). See:
# http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/python_v3.html#qvalidator # noqa
dummyValidator = Qt.QDoubleValidator(None)
self._oldMode = len(dummyValidator.validate("", 0)) < 3
def validate(self, input, pos):
if str(input) in self.blackList:
state = self.Intermediate
else:
state = self.Acceptable
if state != self._previousState:
self.stateChanged.emit(state, self._previousState)
self._previousState = state
if (
self._oldMode
): # for backwards compatibility with older versions of PyQt
return state, pos
else:
return state, input, pos
class WidgetPage(Qt.QWizardPage, TaurusBaseWidget):
OTHER_TXT = "Other..."
# TODO: get defaultCandidates from an entry-point
defaultCandidates = [
"taurus.qt.qtgui.panel:TaurusForm",
"taurus_pyqtgraph:TaurusTrend",
"taurus_pyqtgraph:TaurusPlot",
"taurus.qt.qtgui.extra_guiqwt:TaurusImageDialog",
"taurus.qt.qtgui.extra_guiqwt:TaurusTrend2DDialog",
"taurus.qt.qtgui.extra_nexus:TaurusNeXusBrowser",
"taurus.qt.qtgui.tree:TaurusDbTreeWidget",
"sardana.taurus.qt.qtgui.extra_sardana:SardanaEditor",
"taurus.qt.qtgui.graphic.jdraw:TaurusJDrawSynopticsView",
"taurus.qt.qtgui.panel:TaurusDevicePanel",
]
def __init__(self, parent=None, designMode=False, extraWidgets=None):
Qt.QWizardPage.__init__(self, parent)
TaurusBaseWidget.__init__(self, "WidgetPage")
if extraWidgets:
customWidgets, _ = list(zip(*extraWidgets))
pixmaps = {}
for k, s in extraWidgets:
if s is None:
pixmaps[k] = None
else:
try:
pixmaps[k] = getCachedPixmap(s)
if pixmaps[k].isNull():
raise Exception("Invalid Pixmap")
except Exception:
self.warning("Could not create pixmap from %s" % s)
pixmaps[k] = None
else:
customWidgets = []
pixmaps = {}
self.setFinalPage(True)
self.setTitle("Panel type")
self.setSubTitle("Choose a name and type for the new panel")
self.setButtonText(Qt.QWizard.NextButton, "Advanced settings...")
self.widgetDescription = {
"widgetname": None,
"modulename": None,
"classname": None,
}
# name part
self.nameLE = Qt.QLineEdit()
self.registerField("panelname*", self.nameLE)
self.diagnosticLabel = Qt.QLabel("")
nameLayout = Qt.QHBoxLayout()
nameLayout.addWidget(Qt.QLabel("Panel Name"))
nameLayout.addWidget(self.nameLE)
nameLayout.addWidget(self.diagnosticLabel)
# contents
choices = []
row = []
for cname in self.defaultCandidates + list(customWidgets):
if ":" not in cname:
# sanitize the cname (to be valid it must be modname:classname)
_original = cname
if "." in cname:
# replace *last* "." in cname by by ":"
_alt = cname = ":".join(cname.rsplit(".", 1))
else:
# use TaurusWidgetFactory (deprecated)
_qt_widgets = TaurusWidgetFactory()._qt_widgets
if cname in _qt_widgets:
_mod_name, _ = _qt_widgets[cname]
_alt = cname = ":".join((_mod_name, cname))
else:
_alt = "<module_name>:cname" # unknown module
self.deprecated(
dep="specifying class as '{}'".format(_original),
alt="'{}'".format(_alt),
rel="5.0.0",
)
if ":" not in cname:
# discard cname if it could not be sanitized
continue
row.append(cname)
if cname not in pixmaps:
# If no pixmap name was provided, check for matching
# taurus-provided snapshots (i.e 'snapshot:classname.png')
_snapshot = "snapshot:{}.png".format(cname.split(":")[-1])
pixmaps[cname] = getCachedPixmap(_snapshot)
if len(row) == 3:
choices.append(row)
row = []
row.append(self.OTHER_TXT)
choices.append(row)
# defaultPixmap=getPixmap('logos:taurus.png')
self.choiceWidget = GraphicalChoiceWidget(
choices=choices, pixmaps=pixmaps
)
self.widgetTypeLB = Qt.QLabel("<b>Widget Type:</b>")
self.choiceWidget.choiceMade.connect(self.onChoiceMade)
layout = Qt.QVBoxLayout()
layout.addLayout(nameLayout)
layout.addWidget(self.choiceWidget)
layout.addWidget(self.widgetTypeLB)
self.setLayout(layout)
def initializePage(self):
gui = self.wizard().getGui()
if hasattr(gui, "getPanelNames"):
pnames = gui.getPanelNames()
v = BlackListValidator(blackList=pnames, parent=self.nameLE)
self.nameLE.setValidator(v)
v.stateChanged.connect(self._onValidatorStateChanged)
def validatePage(self):
paneldesc = self.wizard().getPanelDescription()
if paneldesc is None:
Qt.QMessageBox.information(
self,
"You must choose a panel type",
"Choose a panel type by clicking on one of the proposed types",
)
return False
try:
w = paneldesc.getWidget()
if not isinstance(w, Qt.QWidget):
raise ValueError
# set the name now because it might have changed since the
# PanelDescription was created
paneldesc.name = self.field("panelname")
# allow the wizard to proceed
return True
except Exception as e:
Qt.QMessageBox.warning(
self,
"Invalid panel",
"The requested panel cannot be created. \nReason:\n%s"
% repr(e),
)
return False
def _onValidatorStateChanged(self, state, previous):
if state == Qt.QValidator.Acceptable:
self.diagnosticLabel.setText("")
else:
self.diagnosticLabel.setText("<b>(Name already exists)</b>")
def onChoiceMade(self, choice):
if choice == self.OTHER_TXT:
wdesc, ok = ExpertWidgetChooserDlg.getDialog()
if ok:
self.widgetDescription.update(wdesc)
else:
return
else:
self.widgetDescription["classname"] = choice
# the name will be set in self.validatePage
self.wizard().setPanelDescription(
PanelDescription("", **self.widgetDescription)
)
paneltype = str(
self.widgetDescription["widgetname"]
or self.widgetDescription["classname"]
)
self.widgetTypeLB.setText("<b>Widget Type:</b> %s" % paneltype)
class AdvSettingsPage(Qt.QWizardPage):
def __init__(self, parent=None):
Qt.QWizardPage.__init__(self, parent)
self.setTitle("Advanced settings")
self.setSubTitle(
"Fine-tune the behavior of the panel by assigning a Taurus model "
+ "and/or defining the panel interactions with other parts "
+ "of the GUI"
)
self.setFinalPage(True)
self.models = []
layout = Qt.QVBoxLayout()
# ----model---------------
# subwidgets
self.modelGB = Qt.QGroupBox("Model")
self.modelGB.setToolTip(
"Choose a Taurus model to be assigned to the panel"
)
# todo: add a regexp validator
# (it should return valid on TAURUS_MODEL_LIST_MIME_TYPE)
self.modelLE = Qt.QLineEdit()
self.modelChooserBT = Qt.QToolButton()
self.modelChooserBT.setIcon(Qt.QIcon("designer:devs_tree.png"))
# self.modelChooser = TaurusModelChooser()
# connections
self.modelChooserBT.clicked.connect(self.showModelChooser)
self.modelLE.editingFinished.connect(self.onModelEdited)
# layout
layout1 = Qt.QHBoxLayout()
layout1.addWidget(self.modelLE)
layout1.addWidget(self.modelChooserBT)
self.modelGB.setLayout(layout1)
# ----communications------
# subwidgets
self.commGB = Qt.QGroupBox("Communication")
self.commGB.setToolTip(
"Define how the panel communicates with other panels and the GUI"
)
self.commLV = Qt.QTableView()
self.commModel = CommTableModel()
self.commLV.setModel(self.commModel)
self.commLV.setEditTriggers(self.commLV.AllEditTriggers)
self.selectedComm = self.commLV.selectionModel().currentIndex()
self.addBT = Qt.QToolButton()
self.addBT.setIcon(Qt.QIcon.fromTheme("list-add"))
self.removeBT = Qt.QToolButton()
self.removeBT.setIcon(Qt.QIcon.fromTheme("list-remove"))
self.removeBT.setEnabled(False)
# layout
layout2 = Qt.QVBoxLayout()
layout3 = Qt.QHBoxLayout()
layout2.addWidget(self.commLV)
layout3.addWidget(self.addBT)
layout3.addWidget(self.removeBT)
layout2.addLayout(layout3)
self.commGB.setLayout(layout2)
# connections
self.addBT.clicked.connect(self.commModel.insertRows)
self.removeBT.clicked.connect(self.onRemoveRows)
self.commLV.selectionModel().currentRowChanged.connect(
self.onCommRowSelectionChanged
)
layout.addWidget(self.modelGB)
layout.addWidget(self.commGB)
self.setLayout(layout)
def initializePage(self):
try:
widget = self.wizard().getPanelDescription().getWidget()
except Exception as e:
Logger().debug(repr(e))
widget = None
# prevent the user from changing the model if it was already set
if (
isinstance(widget, TaurusBaseComponent)
and widget.getModelName() != ""
):
self.modelLE.setText("(already set by the chosen widget)")
self.modelGB.setEnabled(False)
# try to get the SDM as if we were in a TaurusGui app
try:
if isinstance(Qt.qApp.SDM, SharedDataManager):
sdm = Qt.qApp.SDM
except Exception as e:
Logger().debug(repr(e))
sdm = None
self.itemDelegate = CommItemDelegate(widget=widget, sdm=sdm)
self.commLV.setItemDelegate(self.itemDelegate)
def showModelChooser(self):
from taurus.qt.qtgui.panel import TaurusModelChooser
models, ok = TaurusModelChooser.modelChooserDlg(
parent=self, asMimeData=True
)
if not ok:
return
self.models = str(models.data(TAURUS_MODEL_LIST_MIME_TYPE))
self.modelLE.setText(models.text())
def onModelEdited(self):
self.models = str(self.modelLE.text())
def onRemoveRows(self):
if self.selectedComm.isValid():
self.commModel.removeRows(self.selectedComm.row())
def onCommRowSelectionChanged(self, current, previous):
self.selectedComm = current
enable = (
current.isValid()
and 0 <= current.row() < self.commModel.rowCount()
)
self.removeBT.setEnabled(enable)
def validatePage(self):
desc = self.wizard().getPanelDescription()
# model
desc.model = self.models
# communications
for uid, slotname, signalname in self.commModel.dumpData():
if slotname:
desc.sharedDataRead[uid] = slotname
if signalname:
desc.sharedDataWrite[uid] = signalname
self.wizard().setPanelDescription(desc)
return True
class CommTableModel(Qt.QAbstractTableModel):
NUMCOLS = 3
UID, R, W = list(range(NUMCOLS))
dataChanged = Qt.pyqtSignal(int, int)
def __init__(self, parent=None):
Qt.QAbstractTableModel.__init__(self, parent)
self.__table = []
def dumpData(self):
return copy.deepcopy(self.__table)
def rowCount(self, index=Qt.QModelIndex()):
return len(self.__table)
def columnCount(self, index=Qt.QModelIndex()):
return self.NUMCOLS
def headerData(self, section, orientation, role=Qt.Qt.DisplayRole):
if role == Qt.Qt.TextAlignmentRole:
if orientation == Qt.Qt.Horizontal:
return int(Qt.Qt.AlignLeft | Qt.Qt.AlignVCenter)
return int(Qt.Qt.AlignRight | Qt.Qt.AlignVCenter)
if role != Qt.Qt.DisplayRole:
return None
# So this is DisplayRole...
if orientation == Qt.Qt.Horizontal:
if section == self.UID:
return "Data UID"
elif section == self.R:
return "Reader (slot)"
elif section == self.W:
return "Writer (signal)"
return None
else:
return str("%i" % (section + 1))
def data(self, index, role=Qt.Qt.DisplayRole):
if not index.isValid() or not (0 <= index.row() < self.rowCount()):
return None
row = index.row()
column = index.column()
# Display Role
if role == Qt.Qt.DisplayRole:
text = self.__table[row][column]
if text == "":
if column == self.UID:
text = "(enter UID)"
else:
text = "(not registered)"
return str(text)
return None
def flags(self, index):
return (
Qt.Qt.ItemIsEnabled
| Qt.Qt.ItemIsEditable
| Qt.Qt.ItemIsDragEnabled
| Qt.Qt.ItemIsDropEnabled
| Qt.Qt.ItemIsSelectable
)
def setData(self, index, value=None, role=Qt.Qt.EditRole):
if index.isValid() and (0 <= index.row() < self.rowCount()):
row = index.row()
column = index.column()
self.__table[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def insertRows(self, position=None, rows=1, parentindex=None):
if position is None:
position = self.rowCount()
if parentindex is None:
parentindex = Qt.QModelIndex()
self.beginInsertRows(parentindex, position, position + rows - 1)
slice = [self.rowModel() for i in range(rows)]
self.__table = (
self.__table[:position] + slice + self.__table[position:]
)
self.endInsertRows()
return True
def removeRows(self, position, rows=1, parentindex=None):
if parentindex is None:
parentindex = Qt.QModelIndex()
self.beginResetModel()
self.beginRemoveRows(parentindex, position, position + rows - 1)
self.__table = (
self.__table[:position] + self.__table[position + rows :]
)
self.endRemoveRows()
self.endResetModel()
return True
@staticmethod
def rowModel(uid="", slot="", signal=""):
return [uid, slot, signal]
class CommItemDelegate(Qt.QStyledItemDelegate):
NUMCOLS = 3
UID, R, W = list(range(NUMCOLS))
def __init__(self, parent=None, widget=None, sdm=None):
super(CommItemDelegate, self).__init__(parent)
if widget is not None:
widget = weakref.proxy(widget)
self._widget = widget
if sdm is not None:
sdm = weakref.proxy(sdm)
self._sdm = sdm
def createEditor(self, parent, option, index):
column = index.column()
combobox = Qt.QComboBox(parent)
combobox.setEditable(True)
if column == self.UID and self._sdm is not None:
combobox.addItems(self._sdm.activeDataUIDs())
elif column == self.R and self._widget is not None:
slotnames = [
n
for n, o in inspect.getmembers(self._widget, inspect.ismethod)
if not n.startswith("_")
]
combobox.addItems(slotnames)
# # @todo: inspect the methods in search of (new style) signals
# elif column==self.W:
# if self._widget is not None:
# combobox.addItems(['(Not registered)'])
return combobox
def setEditorData(self, editor, index):
editor.setEditText("")
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText())
[docs]
class PanelDescriptionWizard(Qt.QWizard, TaurusBaseWidget):
"""A wizard-style dialog for configuring a new TaurusGui panel.
Use :meth:`getDialog` for launching it
"""
def __init__(
self, parent=None, designMode=False, gui=None, extraWidgets=None
):
Qt.QWizard.__init__(self, parent)
name = "PanelDescriptionWizard"
TaurusBaseWidget.__init__(self, name)
self._panelDescription = None
if gui is None:
gui = parent
if gui is not None:
self._gui = weakref.proxy(gui)
self.widgetPG = WidgetPage(extraWidgets=extraWidgets)
self.advSettingsPG = AdvSettingsPage()
# self.addPage(self.namePG)
self.addPage(self.widgetPG)
self.addPage(self.advSettingsPG)
[docs]
def getGui(self):
"""returns a reference to the GUI to which the dialog is associated"""
return self._gui
[docs]
def getPanelDescription(self):
"""Returns the panel description with the choices made so far
:return: the panel description
:rtype: PanelDescription
"""
return self._panelDescription
[docs]
def setPanelDescription(self, desc):
"""Sets the Panel description
:param desc:
:type desc: PanelDescription
"""
self._panelDescription = desc
[docs]
@staticmethod
def getDialog(parent, extraWidgets=None):
"""Static method for launching a new Dialog.
:param parent: parent widget for the new dialog
:return: tuple of a description object and a state flag. The state is
True if the dialog was accepted and False otherwise
:rtype: tuple<PanelDescription,bool>
"""
dlg = PanelDescriptionWizard(parent, extraWidgets=extraWidgets)
dlg.exec_()
return dlg.getPanelDescription(), (dlg.result() == dlg.Accepted)
def _test():
from taurus.qt.qtgui.application import TaurusApplication
app = TaurusApplication(sys.argv, cmd_line_parser=None)
form = PanelDescriptionWizard()
def kk(d):
print(d)
Qt.qApp.SDM = SharedDataManager(form)
Qt.qApp.SDM.connectReader("111111", kk)
Qt.qApp.SDM.connectWriter("222222", form, "thisisasignalname")
form.show()
sys.exit(app.exec_())
def _test2():
from taurus.qt.qtgui.application import TaurusApplication
_ = TaurusApplication(sys.argv, cmd_line_parser=None)
print(ExpertWidgetChooserDlg.getDialog())
sys.exit()
def main():
from taurus.qt.qtgui.application import TaurusApplication
app = TaurusApplication(sys.argv, cmd_line_parser=None)
from taurus.qt.qtgui.container import TaurusMainWindow
form = TaurusMainWindow()
def kk(d):
print(d)
Qt.qApp.SDM = SharedDataManager(form)
Qt.qApp.SDM.connectReader("someUID", kk)
Qt.qApp.SDM.connectWriter("anotherUID", form, "perspectiveChanged")
form.show()
paneldesc, ok = PanelDescriptionWizard.getDialog(
form,
extraWidgets=[
("PyQt5.Qt:QLineEdit", "logos:taurus.png"),
("PyQt5.Qt:QTextEdit", None),
("PyQt5.Qt.QLabel", None), # deprecated (for checking sanitation)
("TaurusLabel", None), # deprecated (for checking sanitation)
],
)
if ok:
w = paneldesc.getWidget(sdm=Qt.qApp.SDM)
form.setCentralWidget(w)
form.setWindowTitle(paneldesc.name)
print(Qt.qApp.SDM.info())
sys.exit(app.exec_())
if __name__ == "__main__":
# test2()
main()