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: 
"""

__all__ = ["QConfigEditor"]

__docformat__ = 'restructuredtext'

from taurus.external.qt import Qt
import cPickle as pickle
import os
import tempfile
from taurus.qt.qtcore.configuration import BaseConfigurableClass
from taurus.qt.qtgui.resource import getThemeIcon
from taurus.qt.qtgui.container import TaurusWidget
import shutil


class QConfigEditorModel(Qt.QStandardItemModel):
    '''A custom Model for QConfigEditor'''
    def __init__(self, parent = None, designMode = False):
        super(Qt.QStandardItemModel, self).__init__()
        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 = Qt.from_qvariant(index.data(), str)
        value_str = Qt.from_qvariant(value, str)
        if idx_data_str == value_str:
            return False
        #self.itemFromIndex(index).setData(value, role)
        try:
            self.valueChanged(value_str, index)
        except:
            self.emit(Qt.SIGNAL("showError"),'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: (str)
        '''
        self.originalFile = unicode(iniFileName)
        self._file = tempfile.NamedTemporaryFile()
        self._temporaryFile = unicode(self._file.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 = Qt.from_qvariant(item.data(Qt.Qt.UserRole), str)
        self._delete = False
        self._configurationDictionaries = self.removeBranch(self._configurationDictionaries, path)
        
        try: group = eval(str(path).split(';',1)[0])
        except: group = str(path).split(';',1)[0]
        itemToMark = self.itemFromIndex(tmpindex.parent())
        while(itemToMark != None):    
            itemToMark.setData(Qt.QVariant(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: (dict) a taurus configuration dictionary. See 
                     :class:`BaseConfigurableClass`
        :param path: (str) a semicolon-separated string containing the 
                     path of the branch in the tree
        
        :returns:  (dict) the modified config dict
        '''
        val = str(path).split(';',1)
        if len(val)==2:
            path = val[1]
            try: key = eval(val[0])
            except: key = val[0] 
            dict[key] = self.removeBranch(dict[key], path)
            if self._delete == True:
                if not dict.has_key('__orderedConfigNames__'):
                    return dict
                dict['__orderedConfigNames__'] = self.removeBranch(dict['__orderedConfigNames__'], path)
                self._delete = False
            return dict
        else:
            if self._delete == True:
                if dict.count(val[0]) == 0:
                    return dict
                dict.remove(val[0])
                return dict
            if not dict.has_key('__orderedConfigNames__'):
                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: (str) the new value (a string that will be python-evaluated)
        :param index: (QModelIndex) index of the model
        '''
        changedItem = self.itemFromIndex(index)
        path = Qt.from_qvariant(changedItem.data(Qt.Qt.UserRole), str)
        self._configurationDictionaries = self.changeTreeValue(self._configurationDictionaries, path, value)
        try: group = eval(str(path).split(';',1)[0])
        except: 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(Qt.QVariant('Value has been changed. Old value: '+ str(changedItem.text())), Qt.Qt.ToolTipRole)
        itemToMark.setData(Qt.QVariant(getThemeIcon('emblem-important')), Qt.Qt.DecorationRole)
        while(itemToMark != None):    
            itemToMark.setData(Qt.QVariant(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: (str) a semicolon-separated string containing the 
                     path of the branch in the tree
        :param value: (str) the new value (a string that will be python-evaluated)
        
        :returns:  (dict) the modified config dict
        '''
        val = str(path).split(';',1)
        if len(val) == 2:
            path = val[1]
            try: key = eval(val[0])
            except: 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.
        
        :returns:  (dict) a configuration dictionary.  See :class:`BaseConfigurableClass`
        '''
        #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(Qt.QVariant('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 = Qt.QVariant("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: (Qt.QStandardItem) parent item
        :param configdict: (dict) a configuration dictionary.  See :class:`BaseConfigurableClass`
        '''
        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 = Qt.from_qvariant(item.data(Qt.Qt.UserRole), str)
                path = Qt.QVariant(txt + ";__itemConfigurations__;" + k)
                child.setData(path, Qt.Qt.UserRole)
                self.fillTaurusConfig(child, value) #recursive call to fill all nodes
            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 = Qt.from_qvariant(item.data(Qt.Qt.UserRole), str)
                path = Qt.QVariant(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__', 'ConfigVersion', '__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 = Qt.from_qvariant(item.data(Qt.Qt.UserRole), str)
                    path = Qt.QVariant(txt +";" + k)
                    child.setData(path, Qt.Qt.UserRole)
                    self.fillTaurusConfig(child, value) #recursive call to fill all nodes
                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 = Qt.from_qvariant(item.data(Qt.Qt.UserRole), str)
                    path = Qt.QVariant(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: (str)
        
        :returns (dict)
        '''
        result = None
        qstate = Qt.from_qvariant(self._settings.value(key), 'toByteArray')
        if qstate is not None and not qstate.isNull():
            try: result = pickle.loads(qstate.data())
            except Exception,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
        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: (str) a prefix that will be added to the keys to be
                       saved (no prefix by default)
        '''  
        if group is not None: 
            self._settings.beginGroup(group)
        
        #store the config dict
        self._settings.setValue("TaurusConfig", Qt.QVariant(Qt.QByteArray(pickle.dumps(self._configurationDictionaries[group]))))
        if group is not None: 
            self._settings.endGroup()
        #self.info('MainWindow settings saved in "%s"'%self._settings.fileName())


    def restoreOriginal(self):
        '''
        Replaces temporary file with the original file and builds again the
        configuration tree.
        '''
        if self.markedItems == []:
            return
        shutil.copyfile(self.originalFile, self._temporaryFile)
        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 != None):    
                itemToMark.setData(Qt.QVariant(Qt.QFont("Arial", 10, Qt.QFont.Normal)), Qt.Qt.FontRole)
                itemToMark.setData(Qt.QVariant(), 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(getThemeIcon("document-open"), "Open File", self.loadFile) self._toolbar.addAction(getThemeIcon("document-save"), "Save File", self.saveFile) self._toolbar.addAction(getThemeIcon("edit-undo"), "Reload from file", self.restoreOriginal) self.layout().setMenuBar(self._toolbar) self.setWindowTitle('TaurusConfigEditor') self.connect(self.tree, Qt.SIGNAL("showError"), self._showError)
[docs] def contextMenuEvent(self, event): '''Reimplemented from :meth:`QWidget.contextMenuEvent`''' self.tree._toDeleteIndex = self.treeview.selectedIndexes()[0] text = Qt.from_qvariant(self.tree._toDeleteIndex.data(), str) if self.tree._toDeleteIndex.column() in [1,2] or text in ['LAST', '[custom]'] or text in self.tree.perspectives: return menu = Qt.QMenu() menu.addAction(getThemeIcon('process-stop'), "Delete branch: "+text, self.tree.deleteBranch) menu.addSeparator() menu.addAction(getThemeIcon('help-browser') ,"Help") menu.exec_(event.globalPos()) event.accept()
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. :iniFileName: (str) Name of the file. If None is given the user is prompted for a file. ''' if iniFileName is None: if self.tree.originalFile is None: path = Qt.QDir.homePath() else: path = self.tree.originalFile iniFileName = Qt.QFileDialog.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()
def main(): from taurus.qt.qtgui.application import TaurusApplication from taurus.core.util import argparse from taurus import Release import sys parser = argparse.get_taurus_parser() parser.set_usage("%prog [options] [INIFILENAME]") parser.set_description("taurus configuration editor") app = TaurusApplication(cmd_line_parser=parser, app_name="taurusconfigeditor", app_version= Release.version) args = app.get_command_line_args() w = QConfigEditor() w.setMinimumSize(500,500) w.show() if len(args) == 1: w.loadFile(args[0]) sys.exit(app.exec_()) if __name__ == '__main__': main()