#!/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/>.
#
# ###########################################################################
"""
taurusconfigeditor.py:
"""
from taurus.external.qt import Qt, compat
import pickle
import os
import shutil
import tempfile
import click
from taurus.qt.qtcore.configuration import BaseConfigurableClass
from taurus.qt.qtgui.container import TaurusWidget
__docformat__ = "restructuredtext"
class QConfigEditorModel(Qt.QStandardItemModel):
"""A custom Model for QConfigEditor"""
showError = Qt.pyqtSignal("QString", "QString")
def __init__(self, parent=None, designMode=False):
super(QConfigEditorModel, self).__init__(parent)
self._temporaryFile = None
self._settings = None
self._configurationDictionaries = None
self._modifiedPerspectives = []
self.originalFile = None
self.markedItems = []
def setData(self, index, value, role=Qt.Qt.DisplayRole):
"""see :meth:`Qt.QAbstractTableModel.setData`"""
idx_data_str = index.data()
if idx_data_str == value:
return False
# self.itemFromIndex(index).setData(value, role)
try:
self.valueChanged(value, index)
except Exception:
self.showError.emit(
"Wrong value! The value you entered is wrong. "
+ "The old value will be restored.",
)
return Qt.QStandardItemModel.setData(
self, index, index.data(), role
)
return Qt.QStandardItemModel.setData(self, index, value, role)
def loadFile(self, iniFileName):
"""
Loads file with setting, creates temporary settings file, where changes
made to configuration are saved.
:param iniFileName:
:type iniFileName: str
"""
self.originalFile = iniFileName
self._temporaryFile = tempfile.NamedTemporaryFile(delete=False).name
shutil.copyfile(self.originalFile, self._temporaryFile)
self._settings = Qt.QSettings(
self._temporaryFile, Qt.QSettings.IniFormat
)
self.clear()
self.setHorizontalHeaderLabels(["Configuration key", "type", "value"])
self._configurationDictionaries = self.fillTopLevel()
self.markedItems = []
def deleteBranch(self):
"""
Deletes selected branch from the settings tree. Also updates the
temporary configuration file.
"""
tmpindex = self._toDeleteIndex
item = self.itemFromIndex(tmpindex)
path = item.data(Qt.Qt.UserRole)
self._delete = False
self._configurationDictionaries = self.removeBranch(
self._configurationDictionaries, path
)
try:
group = eval(str(path).split(";", 1)[0])
except Exception:
group = str(path).split(";", 1)[0]
itemToMark = self.itemFromIndex(tmpindex.parent())
while itemToMark is not None:
itemToMark.setData(
Qt.QFont("Arial", 10, Qt.QFont.Bold), Qt.Qt.FontRole
)
itemToMark = self.itemFromIndex(itemToMark.index().parent())
self.markedItems.append(self._toDeleteIndex.parent())
self.removeRow(tmpindex.row(), tmpindex.parent())
self.saveSettings(group)
def removeBranch(self, dict, path):
"""
Method called recursively by self.deleteBranch. In each step it takes
next key (from the path) until reaches element to be deleted. After the
element is deleted, returns updated dictionary.
:param dict: a taurus configuration dictionary. See
:class:`BaseConfigurableClass`
:type dict: dict
:param path: a semicolon-separated string containing the path of the
branch in the tree
:type path: str
:return: the modified config dict
:rtype: dict
"""
val = str(path).split(";", 1)
if len(val) == 2:
path = val[1]
try:
key = eval(val[0])
except Exception:
key = val[0]
dict[key] = self.removeBranch(dict[key], path)
if self._delete is True:
if "__orderedConfigNames__" not in dict:
return dict
dict["__orderedConfigNames__"] = self.removeBranch(
dict["__orderedConfigNames__"], path
)
self._delete = False
return dict
else:
if self._delete is True:
if dict.count(val[0]) == 0:
return dict
dict.remove(val[0])
return dict
if "__orderedConfigNames__" not in dict:
self._delete = True
dict.pop(val[0])
return dict
def valueChanged(self, value, index):
"""Modifies value in the temporary settings file and the model
internal dictionary. Invoked by :meth:`Qt.QAbstractTableModel.setData`,
when user make changes to the value in the settings tree.
:param value: the new value (a string that will be python-evaluated)
:type value: str
:param index: index of the model
:type index: QModelIndex
"""
changedItem = self.itemFromIndex(index)
path = changedItem.data(Qt.Qt.UserRole)
self._configurationDictionaries = self.changeTreeValue(
self._configurationDictionaries, path, value
)
try:
group = eval(str(path).split(";", 1)[0])
except Exception:
group = str(path).split(";", 1)[0]
itemToMark = self.itemFromIndex(index.sibling(index.row(), 0))
self.markedItems.append(itemToMark.index())
self.itemFromIndex(index.sibling(index.row(), 1)).setText(
str(type(eval(value)))
)
changedItem.setData(
"Value has been changed. Old value: " + str(changedItem.text()),
Qt.Qt.ToolTipRole,
)
itemToMark.setData(
Qt.QIcon.fromTheme("emblem-important"), Qt.Qt.DecorationRole
)
while itemToMark is not None:
itemToMark.setData(
Qt.QFont("Arial", 10, Qt.QFont.Bold), Qt.Qt.FontRole
)
itemToMark = self.itemFromIndex(itemToMark.index().parent())
self.saveSettings(group)
def changeTreeValue(self, cdict, path, value):
"""
Method called recursively by valueChanged. In each step it takes next
key (from the path) until reaches element to be modified. After the
element is modified, returns updated dictionary.
:param cdict: a configuration dictionary. See
:class:`BaseConfigurableClass`
:param path: a semicolon-separated string containing the path of the
branch in the tree
:type path: str
:param value: the new value (a string that will be python-evaluated)
:type value: str
:return: the modified config dict
:rtype: dict
"""
val = str(path).split(";", 1)
if len(val) == 2:
path = val[1]
try:
key = eval(val[0])
except Exception:
key = val[0]
cdict[key] = self.changeTreeValue(cdict[key], path, value)
return cdict
else:
cdict[val[0]] = eval(value)
return cdict
def fillTopLevel(self):
"""
Creates a dictionary containing Main Window and Perspectives settings.
This metod creates top nodes only (names of the perspectives, main
window) and invokes fillTaurusConfig. Returns complete dictionary.
:return: a configuration dictionary. See
:class:`BaseConfigurableClass`
:rtype: dict
"""
# self.tree.clear()
root = self.invisibleRootItem()
item = Qt.QStandardItem("LAST")
item.setEditable(False)
# item.setSelectable(False)
root.appendRow(item)
mainConfig = {}
configdict = self.getTaurusConfigFromSettings()
if configdict is not None:
mainConfig[None] = configdict
item.setData("None", Qt.Qt.UserRole)
self.fillTaurusConfig(item, configdict)
self._settings.beginGroup("Perspectives")
self.perspectives = self._settings.childGroups()
for name in self.perspectives:
item = Qt.QStandardItem(name)
item.setEditable(False)
# item.setSelectable(False)
path = "Perspectives/" + name
item.setData(path, Qt.Qt.UserRole)
root.appendRow(item)
self._settings.beginGroup(name)
configdict = self.getTaurusConfigFromSettings()
if configdict is not None:
mainConfig["Perspectives/" + str(name)] = configdict
self.fillTaurusConfig(item, configdict)
self._settings.endGroup()
self._settings.endGroup()
return mainConfig
def fillTaurusConfig(self, item, configdict):
"""
Fills the non-top nodes of the dictionary recursively.
:param item: parent item
:type item: Qt.QStandardItem
:param configdict: a configuration dictionary. See
:class:`BaseConfigurableClass`
:type configdict: dict
"""
if not BaseConfigurableClass.isTaurusConfig(configdict):
return
# fill the registered keys
registeredkeys = configdict.get("__orderedConfigNames__", [])
valuesdict = configdict.get("__itemConfigurations__", {})
for k in registeredkeys:
value = valuesdict[k]
child = Qt.QStandardItem(k)
if BaseConfigurableClass.isTaurusConfig(value):
child.setEditable(False)
item.appendRow(child)
txt = item.data(Qt.Qt.UserRole)
path = txt + ";__itemConfigurations__;" + k
child.setData(path, Qt.Qt.UserRole)
# recursive call to fill all nodes
self.fillTaurusConfig(child, value)
else:
typeV = Qt.QStandardItem(repr(type(value)))
valueV = Qt.QStandardItem(repr(value))
typeV.setForeground(Qt.QBrush(Qt.QColor("gray")))
child.setForeground(Qt.QBrush(Qt.QColor("gray")))
item.appendRow([child, typeV, valueV])
txt = item.data(Qt.Qt.UserRole)
path = txt + ";__itemConfigurations__;" + k
child.setEditable(False)
typeV.setEditable(False)
child.setData(path, Qt.Qt.UserRole)
typeV.setData(path, Qt.Qt.UserRole)
valueV.setData(path, Qt.Qt.UserRole)
customkeys = [
k
for k in configdict
if k
not in (
"__orderedConfigNames__",
"__itemConfigurations__",
"__pickable__",
)
]
if len(customkeys) > 0:
custom = Qt.QStandardItem("[custom]")
item.appendRow(custom)
custom.setEditable(False)
# custom.setSelectable(False)
custom.setBackground(Qt.QBrush(Qt.QColor("gray")))
for k in customkeys:
value = configdict[k]
child = Qt.QStandardItem(str(k))
# item.appendRow(child)
if BaseConfigurableClass.isTaurusConfig(value):
child.setEditable(False)
item.appendRow(child)
txt = item.data(Qt.Qt.UserRole)
path = txt + ";" + k
child.setData(path, Qt.Qt.UserRole)
# recursive call to fill all nodes
self.fillTaurusConfig(child, value)
else:
typeV = Qt.QStandardItem(repr(type(value)))
valueV = Qt.QStandardItem(repr(value))
typeV.setForeground(Qt.QBrush(Qt.QColor("gray")))
child.setForeground(Qt.QBrush(Qt.QColor("gray")))
item.appendRow([child, typeV, valueV])
txt = item.data(Qt.Qt.UserRole)
path = txt + ";" + k
child.setData(path, Qt.Qt.UserRole)
child.setEditable(False)
typeV.setEditable(False)
typeV.setData(path, Qt.Qt.UserRole)
valueV.setData(path, Qt.Qt.UserRole)
def getTaurusConfigFromSettings(self, key="TaurusConfig"):
"""
Loads and returns the configuration dictionary from the settings file
using pickle module.
:param key:
:type key: str
:returns (dict)
"""
result = None
qstate = self._settings.value(key)
if qstate is not None:
try:
result = pickle.loads(qstate.data())
except Exception as e:
msg = "problems loading TaurusConfig: \n%s" % repr(e)
Qt.QMessageBox.critical(None, "Error loading settings", msg)
return result
def reloadFile(self):
"""
Reloads the file. Configuration tree is build again.
"""
self.loadFile(self.originalFile)
def saveFile(self):
"""
Replaces original file with temporary file (where changes were being
saved).
"""
if self.markedItems == []:
return
self._settings.sync()
shutil.copyfile(self._temporaryFile, self.originalFile)
self.clearChanges()
# self.reloadFile()
def saveSettings(self, group=None):
"""Saves the current state to the temporary file
:param group: a prefix that will be added to the keys to be saved (no
prefix by default)
:type group: str
"""
if group is not None:
self._settings.beginGroup(group)
# store the config dict
self._settings.setValue(
"TaurusConfig",
Qt.QByteArray(
pickle.dumps(self._configurationDictionaries[group])
),
)
if group is not None:
self._settings.endGroup()
def restoreOriginal(self):
"""
Replaces temporary file with the original file and builds again the
configuration tree.
"""
if self.markedItems == []:
return
self.reloadFile()
def clearChanges(self):
"""
Clears all changes in style of the modified elements in tree view.
"""
for index in self.markedItems:
itemToMark = self.itemFromIndex(index)
while itemToMark is not None:
itemToMark.setData(
Qt.QFont("Arial", 10, Qt.QFont.Normal), Qt.Qt.FontRole
)
itemToMark.setData(None, Qt.Qt.DecorationRole)
itemToMark = self.itemFromIndex(itemToMark.index().parent())
[docs]
class QConfigEditor(TaurusWidget):
"""A widget that shows a tree view of the contents of Taurus
configuration files saved by TaurusMainWindow and lets the user edit
the values of the configuration keys
"""
def __init__(self, parent=None, designMode=False):
TaurusWidget.__init__(self, parent=parent, designMode=designMode)
self.setLayout(Qt.QVBoxLayout())
self.tree = QConfigEditorModel()
self.treeview = Qt.QTreeView()
self.tree.setHorizontalHeaderLabels(
["Configuration key", "type", "value"]
)
self.layout().addWidget(self.treeview)
self._toolbar = Qt.QToolBar("QConfigViewer Main toolBar")
self._toolbar.addAction(
Qt.QIcon.fromTheme("document-open"), "Open File", self.loadFile
)
self._toolbar.addAction(
Qt.QIcon.fromTheme("document-save"), "Save File", self.saveFile
)
self._toolbar.addAction(
Qt.QIcon.fromTheme("edit-undo"),
"Reload from file",
self.restoreOriginal,
)
self.layout().setMenuBar(self._toolbar)
self.setWindowTitle("TaurusConfigEditor")
self.tree.showError.connect(self._showError)
def _showError(self, title, body):
"""
Opens a warning dialog with a given title and body.
:title: (str) title
:body: (str) body
"""
Qt.QMessageBox.warning(self, title, body, Qt.QMessageBox.Ok)
[docs]
def loadFile(self, iniFileName=None):
"""
Loads a configuration stored in a file and creates the tree.
:param iniFileName: Name of the file. If None is given the user is
prompted for a file.
:type iniFileName: str
"""
if iniFileName is None:
if self.tree.originalFile is None:
path = Qt.QDir.homePath()
else:
path = self.tree.originalFile
iniFileName, _ = compat.getOpenFileName(
self, "Select a settings file", path, "Ini Files (*.ini)"
)
if not iniFileName:
return
self.tree.loadFile(iniFileName)
self.treeview.setModel(self.tree)
self.setWindowTitle(
"TaurusConfigEditor - %s"
% os.path.basename(self.tree.originalFile)
)
[docs]
def saveFile(self):
"""Replaces original file with temporary file (where changes were
being saved).
"""
self.tree.saveFile()
[docs]
def restoreOriginal(self):
"""
Replaces temporary file with the original file and builds again the
configuration tree.
"""
self.tree.restoreOriginal()
@click.command("config")
@click.argument("config_file", type=click.Path(exists=True), required=False)
def config_cmd(config_file):
"""Open the taurus configuration editor"""
from taurus.qt.qtgui.application import TaurusApplication
import sys
app = TaurusApplication(cmd_line_parser=None)
w = QConfigEditor()
w.setMinimumSize(500, 500)
w.show()
if config_file:
w.loadFile(config_file)
sys.exit(app.exec_())
if __name__ == "__main__":
config_cmd()