# Source code for taurus.qt.qtgui.panel.taurusconfigeditor

    #!/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())

    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)

        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)
            )

        def saveFile(self):
            """Replaces original file with temporary file (where changes were
            being saved).
            """
            self.tree.saveFile()

        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()