#!/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/>.
#
# ###########################################################################
"""This module provides basic python dictionary/list editor widgets
"""
import sys
from taurus.core.util.containers import SortedDict
from taurus.external.qt import Qt
from taurus.qt.qtgui.container import TaurusBaseContainer
from taurus.qt.qtcore.util.properties import djoin
__docformat__ = "restructuredtext"
# ###########################################################################
# Methods borrowed from fandango modules
def isString(seq):
if isinstance(seq, str):
return True # It matches most python str-like classes
if any(
s in str(type(seq)).lower()
for s in (
"vector",
"array",
"list",
)
):
return False
if "qstring" == str(type(seq)).lower():
return True # It matches QString
return False
def isSequence(seq, INCLUDE_GENERATORS=True):
"""It excludes Strings, dictionaries but includes generators"""
if any(isinstance(seq, t) for t in (list, set, tuple)):
return True
if isString(seq):
return False
if hasattr(seq, "items"):
return False
if INCLUDE_GENERATORS:
if hasattr(seq, "__iter__"):
return True
elif hasattr(seq, "__len__"):
return True
return False
def isDictionary(seq):
"""It includes dicts and also nested lists"""
if isinstance(seq, dict):
return True
if hasattr(seq, "items") or hasattr(seq, "iteritems"):
return True
if seq and isSequence(seq) and isSequence(seq[0]):
if seq[0] and not isSequence(seq[0][0]):
return True # First element of tuple must be hashable
return False
def dict2array(dct):
"""Converts a dictionary in a table of data, lists are unnested columns"""
data, table = {}, []
data["nrows"], data["ncols"] = 0, 2 if isDictionary(dct) else 1
def expand(d, level): # ,nrows=nrows,ncols=ncols):
# self.debug('\texpand(%s(%s),%s)'%(type(d),d,level))
items = (
list(d.items())
if isinstance(d, SortedDict)
else sorted(list(d.items()) if hasattr(d, "items") else d)
)
for k, v in items:
# zero = data["nrows"]
data[(data["nrows"], level)] = k
if isDictionary(v):
data["ncols"] += 1
expand(v, level + 1)
else:
if not isSequence(v):
v = [v]
for t in v:
data[(data["nrows"], level + 1)] = t
data["nrows"] += 1
# for i in range(zero+1,nrows): data[(i,level)] = None
expand(dct, 0)
[table.append([]) for r in range(data.pop("nrows"))]
[
table[r].append(None)
for c in range(data.pop("ncols"))
for r in range(len(table))
]
for coord, value in data.items():
table[coord[0]][coord[1]] = value
return table
def array2dict(table):
"""Converts a table in a dictionary of left-to-right nested date, unnested
columns are lists
"""
nrows, ncols = len(table), len(table[0])
def expand(r, c, end):
print("expand(%s,%s,%s)" % (r, c, end))
i0, t0 = r, table[r][c]
if not t0:
return t0
if c + 1 < ncols and (table[r][c + 1] or not c):
d = {}
keys = []
new_end = r + 1
for i in range(r + 1, end + 1):
t = table[i][c] if i < end else None
if t or i >= end:
# start,name,stop for each key
keys.append((i0, t0, new_end))
t0, i0 = t, i
new_end = i + 1
for i, key, new_end in keys:
nd = expand(i, c + 1, new_end)
d[key] = nd if key not in d else djoin(d.get(key), nd)
print("expand(%s to %s,%s): %s" % (r, end, c, d))
return d
else:
d = [table[i][c] for i in range(r, end)]
print("expand(%s to %s,%s): %s" % (r, end, c, d))
return d
data = expand(0, 0, nrows)
return data
# ###########################################################################
class QBaseDictionaryEditor(Qt.QDialog, TaurusBaseContainer):
def __init__(self, parent=None, designMode=None, title=None):
self.data = {} # An {(x,y):value} array
self.title = title
self.dctmodel = SortedDict()
self.callback = None
self.call__init__wo_kw(Qt.QDialog, parent)
self.call__init__(
TaurusBaseContainer, type(self).__name__, designMode=designMode
) # defineStyle called from here
@classmethod
def main(klass, args=None, title="", modal=False, callback=None):
dialog = klass()
dialog.setModal(modal)
dialog.setCallback(callback)
dialog.setModifiableByUser(True)
dialog.setWindowTitle(title or klass.title or klass.__name__)
if args:
dialog.setModel(args) # [0] if isSequence(args) else args)
dialog.show()
return dialog
def defineStyle(self):
self.info("QBaseDictionaryEditor.defineStyle()")
# self.setWindowTitle('DictionaryEditor')
self.label = Qt.QLabel(
"Dictionary as a nested tree: {key1:[val1,val2],key2:[val3]"
)
# self.value = Qt.QLabel()
self.table = Qt.QTableWidget() # TaurusBaseTable()
self.table.horizontalHeader().setStretchLastSection(True)
# self.table.verticalHeader().setStretchLastSection(True)
self.baccept = Qt.QPushButton("Apply")
self.bcancel = Qt.QPushButton("Cancel")
self.baddColumn = Qt.QPushButton()
self.baddRow = Qt.QPushButton()
[
(
b.setFixedSize(Qt.QSize(20, 20)),
b.setIcon(Qt.QIcon("designer:plus.png")),
)
for b in (self.baddColumn, self.baddRow)
]
self.setLayout(Qt.QGridLayout())
self.layout().addWidget(self.label, 0, 0, 1, 5)
# self.layout().addWidget(self.value,1,0,1,3)
self.layout().addWidget(self.baddColumn, 2, 5, 1, 1)
self.layout().addWidget(self.table, 2, 0, 5, 5)
self.layout().addWidget(self.baddRow, 7, 0, 1, 1)
self.layout().addWidget(self.baccept, 8, 3, 1, 1)
self.layout().addWidget(self.bcancel, 8, 4, 1, 1)
self.baccept.clicked.connect(self.save)
self.bcancel.clicked.connect(self.close)
self.baddRow.clicked.connect(self.addRow)
self.baddColumn.clicked.connect(self.addColumn)
self.reject.connect(self.close)
def addRow(self):
self.table.setRowCount(self.table.rowCount() + 1)
self.table.resizeRowsToContents()
self.table.update()
def addColumn(self):
self.table.horizontalHeader().setStretchLastSection(False)
self.table.setColumnCount(self.table.columnCount() + 1)
self.table.resizeColumnsToContents()
self.table.horizontalHeader().setStretchLastSection(True)
self.table.update()
def setCallback(self, callback):
self.callback = callback
def getCellText(self, row, column):
if row >= self.table.rowCount() or column >= self.table.columnCount():
v = None
else:
i = self.table.item(row, column)
if i is None:
v = i
else:
v = str(i.text()).strip()
self.debug("getCellText(%s,%s): %s" % (row, column, v))
return v
def setCellText(self, row, column, value, bold=False, italic=False):
i = self.table.item(row, column) or Qt.QTableWidgetItem()
i.setText(str(value if value is not None else ""))
if bold or italic:
f = i.font()
if bold:
f.setBold(True)
if italic:
f.setItalic(True)
i.setFont(f)
self.table.setItem(row, column, i)
def setModel(self, model, **kwargs):
raise Exception("setModel(self,model)!")
def updateStyle(self):
raise Exception("updateStyle(self)!")
def getValues(self):
raise Exception("getValues(self)!")
def save(self):
raise Exception("save(self)!")
# ###########################################################################
[docs]
class QDictionaryEditor(QBaseDictionaryEditor):
[docs]
def setModel(self, model, **kwargs):
self.info("DictionaryEditor.setModel(%s(%s))" % (type(model), model))
self.dctmodel = eval(model) if isString(model) else model
# self.updateStyle() called from the property setter
TaurusBaseContainer.setModel(self, model, **kwargs)
[docs]
def getModelClass(self, **kwargs):
return dict
[docs]
def updateStyle(self):
# self.value.setText(str(self.dctmodel))
data = dict2array(self.dctmodel)
self.nrows, self.ncols = len(data), len(data[0])
self.info(data)
self.table.setRowCount(self.nrows)
self.table.setColumnCount(self.ncols)
for r in range(self.nrows):
for c in range(self.ncols):
self.setCellText(
r, c, data[r][c], bold=(not c), italic=(c == 1)
)
self.table.resizeRowsToContents()
self.table.resizeColumnsToContents()
self.update()
[docs]
def getValues(self):
nrows, ncols = self.table.rowCount(), self.table.columnCount()
table = [
[self.getCellText(r, c) for c in range(ncols)]
for r in range(nrows)
]
self.data = array2dict(table) # It returns a SortedDict
self.info("getValues(): %s" % str(self.data))
return self.data
[docs]
def save(self):
self.getValues()
self.info("DictionaryEditor.save(): %s" % self.data)
if self.callback:
self.callback(self.data)
elif self.dctmodel is None:
self.dctmodel = self.data
else: # Overwriting dctmodel
self.dctmodel.clear()
self.dctmodel.update(self.data)
if self.callback:
self.callback(self.data) # A SortedDict is passed here
self.updateStyle()
# ###########################################################################
[docs]
class QListEditor(QBaseDictionaryEditor):
[docs]
def defineStyle(self):
QBaseDictionaryEditor.defineStyle(self)
self.table.setColumnCount(1)
self.baddColumn.hide()
[docs]
def setModel(self, model, **kwargs):
self.info("DictionaryEditor.setModel(%s(%s))" % (type(model), model))
if isString(model):
try:
self.dctmodel = (
list(eval(model))
if any(c in model for c in ("{", "[", "("))
else [model]
)
except Exception:
self.dctmodel = [model]
else:
self.dctmodel = model
# self.updateStyle() called from the property setter
TaurusBaseContainer.setModel(self, model, **kwargs)
[docs]
def getModelClass(self, **kwargs):
return list
[docs]
def updateStyle(self):
# self.value.setText(str(self.dctmodel))
data = list(self.dctmodel)
self.nrows, self.ncols = len(data), 1
self.table.setRowCount(self.nrows)
self.table.setColumnCount(self.ncols)
for r in range(self.nrows):
self.setCellText(r, 0, data[r])
self.table.resizeRowsToContents()
self.table.horizontalHeader().setStretchLastSection(True)
self.update()
[docs]
def getValues(self):
nrows = self.table.rowCount()
self.data = [self.getCellText(r, 0) for r in range(nrows)]
return self.data
[docs]
def save(self):
self.getValues()
self.info(
"DictionaryEditor.save(%s(%s)): %s"
% (type(self.data), self.data, self.callback)
)
if self.callback:
self.callback(self.data)
elif self.dctmodel is None:
self.dctmodel = self.data
else: # Overwriting dctmodel
if isSequence(self.dctmodel):
while len(self.dctmodel):
self.dctmodel.pop(0)
self.dctmodel.extend(self.data)
elif isDictionary(self.dctmodel):
self.dctmodel.clear()
self.dctmodel = djoin(self.dctmodel, self.data)
return self.data
# ###########################################################################
def prepare():
from taurus.qt.qtgui.application import TaurusApplication
app = TaurusApplication(app_name="DictionaryEditor")
args = app.get_command_line_args()
return app, args
if __name__ == "__main__":
app, args = prepare()
dialog = QDictionaryEditor.main(args)
sys.exit(app.exec_())