Source code for taurus.qt.qtgui.table.qdictionary

#!/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"""

__all__ = ["QDictionaryEditor","QListEditor"]

__docformat__ = 'restructuredtext'

import sys
import taurus
import numpy
from taurus.core.util.containers import SortedDict
from taurus.external.qt import Qt
from taurus.qt.qtgui.container import TaurusBaseContainer,TaurusWidget
from taurus.qt.qtcore.util.properties import join,djoin

###############################################################################
# Methods borrowed from fandango modules

def isString(seq):
    if isinstance(seq,basestring): 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 = d.items() if isinstance(d,SortedDict) else sorted(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])
    r,c = 0,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: 
                    keys.append((i0,t0,new_end)) #start,name,stop for each key
                    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 self.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(taurus.qt.qtgui.resource.getIcon(':/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.connect(self.baccept,Qt.SIGNAL("clicked()"),self.save)
        self.connect(self.bcancel,Qt.SIGNAL("clicked()"),self.close)
        self.connect(self.baddRow,Qt.SIGNAL("clicked()"),self.addRow)
        self.connect(self.baddColumn,Qt.SIGNAL("clicked()"),self.addColumn)
        self.connect(self,Qt.SIGNAL("reject()"),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): 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): self.info('DictionaryEditor.setModel(%s(%s))'%(type(model),model)) self.dctmodel = eval(model) if isString(model) else model TaurusBaseContainer.setModel(self,model) #self.updateStyle() called from the property setter
[docs] def getModelClass(self): 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): 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: self.dctmodel = [model] else: self.dctmodel = model TaurusBaseContainer.setModel(self,model) #self.updateStyle() called from the property setter
[docs] def getModelClass(self): 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_())