#!/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:
"""
import copy
import inspect
import sys
import weakref
from taurus.core.util.log import Logger
from taurus.external.qt import Qt
from taurus.qt.qtcore.communication import SharedDataManager
from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE
from taurus.qt.qtgui.base import TaurusBaseComponent, TaurusBaseWidget
from taurus.qt.qtgui.icon import getCachedPixmap
from taurus.qt.qtgui.input import GraphicalChoiceWidget
from taurus.qt.qtgui.taurusgui.utils import PanelDescription
from taurus.qt.qtgui.util import TaurusWidgetFactory
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()