Source code for qcobj.cfggui

# -*- coding: utf-8 -*-
from __future__ import print_function, division

#import sip
#sip.setapi('QString', 2)
#sip.setapi('QVariant', 2)
from . qtCompat import Qt, Signal, QtCore, QtGui

import sys
import textwrap
import signal
from pint import DimensionalityError
from pint.unit import UndefinedUnitError
from datetime import datetime
import pylab
from io import BytesIO
import numpy as np
import itertools

# Local imports
from qcobj.qconfigobj import (QConfigObj, QValidator, ValidateError, Q_,
        eng_string, extract, isStringLike, splitPolygons)

ROOTNAME = 'General'
BOOL_COLOR = QtGui.QColor('green')
QUANTITY_COLOR = QtGui.QColor('red')
STR_COLOR = QtGui.QColor('blue')
qvalidator = QValidator()
EXPAND_ALL = 'Expand All'
COLLAPSE_ALL = 'Collapse All'
BACKGND_COLOR = QtGui.QColor('white')


#------------------------------------------------------------------------------
[docs]def split_list(L, n, stringify=True): """ Return a generator with the list `L` splitted in groups of `n` elements. If stringify evaluates as true, the groups of `n` elements are joined and terminated by \n """ assert type(L) is list, "%s is not a list!" % L for i in range(0, len(L), n): if stringify: yield " ".join(L[i: i + n]) + "\n" else: yield L[i: i + n]
#------------------------------------------------------------------------------
[docs]def noBlanks(withblanks, wordsPerLine=2): """ Remove blanks and format with `wordsPerLine` words per line """ return "".join(list(split_list(withblanks, wordsPerLine)))
#------------------------------------------------------------------------------
[docs]def deBlank(section, key, wordsPerLine=2): """ Remove blanks and format with `wordsPerLine` words per line every value with the key == 'polygon' """ if key == 'polygon': section[key] = noBlanks(section[key].split(), wordsPerLine)
#------------------------------------------------------------------------------
[docs]def createPolygons(pols, k): data =[] for pol in splitPolygons(pols): if pol: data.append(k * np.loadtxt(BytesIO(pol.encode()))) return data
#------------------------------------------------------------------------------
[docs]def colorize(s, color): """ Return an HTML colorized string for `s` """ color = color.lower() return "<font color=%s>%s</font>" % (color, s)
#------------------------------------------------------------------------------
[docs]def getPath(index): """ Return section path at `index` """ # Build section path path = [] parentIndex = index.parent() while parentIndex.isValid(): secname = parentIndex.internalPointer().name() if secname != ROOTNAME: path.insert(0, secname) parentIndex = parentIndex.parent() return path
#------------------------------------------------------------------------------
[docs]def valueAtPath(cobj, path, name): """ Return cobj value at `path` or raise RuntimeError """ if cobj is not None: section = cobj while path: try: section = section[path.pop(0)] except KeyError: return "__???__" if name == ROOTNAME: return section else: try: return section[name] except KeyError: return "__???__"
#==============================================================================
[docs]class TreeItem(QtCore.QObject):
[docs] def __init__(self, name='', parent=None, data=None): self.parentItem = parent self.childItems = [] self._name = name self._data = data
[docs] def name(self): return self._name
[docs] def appendChild(self, item): self.childItems.append(item)
[docs] def child(self, row): return self.childItems[row]
[docs] def childCount(self): return len(self.childItems)
[docs] def columnCount(self): return 3
#return len(self.itemData)
[docs] def data(self): return self._data
[docs] def parent(self): return self.parentItem
[docs] def row(self): if self.parentItem: return self.parentItem.childItems.index(self) return 0
[docs] def setData(self, value, validRange, column): """ Set node data with value converted to appropriate units as stated in validRange and return it or return None """ qtype = validRange.partition('(')[0] if column == 1: if qtype.endswith('list'): try: tmp = eval(value) except (NameError, TypeError) as e: msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Validation Error!", str(e), QtGui.QMessageBox.Ok) msgbox.exec_() return None elif qtype == 'quantity': # Quantity: get units if isinstance(self._data, (tuple, list)): units = self._data[0].units else: units = self._data.units # Make value a list values = [v.strip() for v in value.split(',')] tmp = ["%s %s" % (v, units) for v in values] # Validate all elements in list try: valid = qvalidator.check(validRange, tmp) except (ValidateError, ValueError) as e: msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Validation Error!", str(e), QtGui.QMessageBox.Ok) msgbox.exec_() return None except UndefinedUnitError as e: msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Undefned Unit Error!", str(e), QtGui.QMessageBox.Ok) msgbox.exec_() return None self._data = valid return self._data else: # String, boolean, ... tmp = value try: valid = qvalidator.check(validRange, tmp) except (ValidateError, ValueError) as e: msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Validation Error!", str(e), QtGui.QMessageBox.Ok) msgbox.exec_() return None else: self._data = valid return self._data elif column == 2: # Units # Only quantities have values in column 2 if value: if isinstance(self._data, (tuple, list)): data = self._data else: data = (self._data, ) newdata = () for d in data: try: newq = d.to(value) except DimensionalityError as e: msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Dimensions disagree!", str(e), QtGui.QMessageBox.Ok) msgbox.exec_() return None except UndefinedUnitError as e: msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Undefned Unit Error!", str(e), QtGui.QMessageBox.Ok) msgbox.exec_() return None else: newdata += (newq, ) if len(newdata) == 1: self._data = newdata[0] else: self._data = newdata return self._data return None else: return None
#==============================================================================
[docs]class TreeModel(QtCore.QAbstractItemModel):
[docs] def __init__(self, parent, qcobj=None): super(TreeModel, self).__init__() self._parent = parent self._header = ["Item", "Value", "Units"] self.rootItem = TreeItem(name='root', parent=None) self._loaded = False self.setupModelData(qcobj) self._compareQcobj = None
[docs] def columnCount(self, parent): if parent.isValid(): return parent.internalPointer().columnCount() else: return self.rootItem.columnCount()
[docs] def data(self, index, role): if not index.isValid(): return None item = index.internalPointer() name = item.name() val = item.data() refval = valueAtPath(self._compareQcobj, getPath(index), name) if refval is None: # Only one Qcobj! refval = val # Units try: if isinstance(val, tuple): units = str(val[0].units) else: units = str(val.units) except AttributeError: units = None # Reference Units try: if isinstance(val, tuple): refunits = str(refval[0].units) else: refunits = str(refval.units) except AttributeError: refunits = None ######### # Tooltip ######### if role == Qt.ToolTipRole: return self.qcobj.comment(getPath(index), name) ################## # Foreground Color ################## elif role == Qt.ForegroundRole: try: if isinstance(val, tuple): val[0].magnitude else: val.magnitude if index.column() == 1: if val == refval: return QUANTITY_COLOR else: return BACKGND_COLOR elif index.column() == 2: if units == refunits: return QUANTITY_COLOR else: return BACKGND_COLOR except AttributeError: if isStringLike(val) and index.column() == 1: if val == refval: return STR_COLOR else: return BACKGND_COLOR elif isStringLike(val) and index.column() == 2: if units == refunits: return STR_COLOR else: return BACKGND_COLOR if (str(val) in "True False".split() and index.column() in (1, 2)): if val == refval: return BOOL_COLOR else: return BACKGND_COLOR return None ################## # Background Color ################## elif role == Qt.BackgroundRole: try: if isinstance(val, tuple): val[0].magnitude else: val.magnitude if index.column() == 1: if val == refval: return BACKGND_COLOR else: return QUANTITY_COLOR elif index.column() == 2: if units == refunits: return BACKGND_COLOR else: return QUANTITY_COLOR except AttributeError: if isStringLike(val) and index.column() == 1: if val == refval: return BACKGND_COLOR else: return STR_COLOR elif isStringLike(val) and index.column() == 2: if units == refunits: return BACKGND_COLOR else: return STR_COLOR if (str(val) in "True False".split() and index.column() == 1): if val == refval: return BACKGND_COLOR else: return BOOL_COLOR elif (str(val) in "True False".split() and index.column() == 2): if units == refunits: return BACKGND_COLOR else: return BOOL_COLOR return None return None ########### # Display ########### elif role in (Qt.DisplayRole, ): if item.childCount() == 0: if index.column() == 0: return name elif index.column() == 1: # Value column try: if isinstance(val, tuple): return ", ".join( [str(eng_string(v.magnitude, doround=6)) for v in val]) else: return str(eng_string(val.magnitude, doround=6)) except AttributeError: return textwrap.dedent(str(val)).strip('\n') elif index.column() == 2: # Units column return units else: if index.column() == 0: return name.title() ######### # Edit ######### elif role in (Qt.EditRole, ): if item.childCount() == 0: if index.column() == 0: return name elif index.column() == 1: # Value column try: if isinstance(val, tuple): return ", ".join( [str(eng_string(v.magnitude, doround=6)) for v in val]) else: return str(eng_string(val.magnitude, doround=6)) except AttributeError: return textwrap.dedent(str(val)).strip('\n') elif index.column() == 2: # Units column return units else: if index.column() == 0: return name.title()
[docs] def flags(self, index): """ Must be implemented """ if not index.isValid(): return Qt.NoItemFlags default = Qt.ItemIsEnabled | Qt.ItemIsSelectable if index.column() == 0: return default else: return default | Qt.ItemIsEditable
[docs] def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self._header[section] return None
[docs] def index(self, row, column, parent): if not self.hasIndex(row, column, parent): return QtCore.QModelIndex() if not parent.isValid(): parentItem = self.rootItem else: parentItem = parent.internalPointer() childItem = parentItem.child(row) if childItem: return self.createIndex(row, column, childItem) else: return QtCore.QModelIndex()
[docs] def parent(self, index): if not index.isValid(): return QtCore.QModelIndex() childItem = index.internalPointer() parentItem = childItem.parent() if parentItem == self.rootItem: return QtCore.QModelIndex() return self.createIndex(parentItem.row(), 0, parentItem)
[docs] def rowCount(self, parent): if parent.column() > 0: return 0 if not parent.isValid(): parentItem = self.rootItem else: parentItem = parent.internalPointer() return parentItem.childCount()
[docs] def setComparison(self, qcobj): """ Set comparison qcobj for highlighting differences """ self._compareQcobj = qcobj
[docs] def setData(self, index, value, role): if role == Qt.EditRole: path = getPath(index) key = index.internalPointer().name() validRange = self.qcobj.validRange(path, key) retval = index.internalPointer().setData(value, validRange, index.column()) if retval is not None: # Save value into qcobj section = self.qcobj for p in path: section = section[p] section[key] = retval self.dataChanged.emit(index, index) self._parent.setFileChanged(self.qcobj.filename) return True else: return False else: print("Treemodel setData", args) return False
[docs] def setupModelData(self, qcobj): """ Populate model with data from QCconfigObj instance """ parent = self.rootItem self.qcobj = qcobj def fill(obj, parent): if parent != self.rootItem: for item in sorted(obj.scalars): section = TreeItem(name=item, parent=parent, data=obj[item]) parent.appendChild(section) for item in sorted(obj.sections): section = TreeItem(name=item, parent=parent, data=obj[item]) parent.appendChild(section) if obj[item].sections is not None: fill(obj[item], section) if self._loaded: # Make a new root item self.rootItem = TreeItem(name='root', parent=None) generalItem = TreeItem(name=ROOTNAME, parent=self.rootItem) self.rootItem.appendChild(generalItem) if qcobj: for item in sorted(qcobj.scalars): section = TreeItem(name=item, parent=generalItem, data=qcobj[item]) generalItem.appendChild(section) fill(qcobj, self.rootItem) self._loaded = True self.layoutChanged.emit()
#==============================================================================
[docs]class TreeView(QtGui.QTreeView):
[docs] def __init__(self, *args): super(TreeView, self).__init__(*args) self.collapsed.connect(self._resized) self.expanded.connect(self._resized)
def _resized(self, *args): self.resizeColumns()
[docs] def resizeColumns(self): for column in range(self.model().columnCount(QtCore.QModelIndex())): self.resizeColumnToContents(column)
#==============================================================================
[docs]class QuantityDialog(QtGui.QDialog):
[docs] def __init__(self, text, parent=None): super(QuantityDialog, self).__init__(parent) layout = QtGui.QVBoxLayout(self) #text = "".join(["<p>%s</p>" % line for line in text]) #text = "<PRE>%s</PRE>" % text self._text = QtGui.QTextEdit("", self) #self._text.setAcceptRichText(True) self._text.setHtml(text) self._text.setFont(QtGui.QFont("Courier New", 11)) self._text.setReadOnly(True) #self._bbox = QtGui.QDialogButtonBox() #self._bbox.setStandardButtons(QtGui.QDialogButtonBox.Ok) #self._bbox.clicked.connect(self._onOk) layout.addWidget(self._text) #layout.addWidget(self._bbox) self.resize(600, 400)
def _onOk(self, btn): self.close()
#==============================================================================
[docs]class CfgGui(QtGui.QMainWindow):
[docs] def __init__(self, opts): super(CfgGui, self).__init__() # Avoid QtCore.QObject::startTimer: QTimer can only be used with # threads... QtGui.QFileSystemModel(self) self._options = opts self._filesThatChanged = [] if opts.configspec is None: # Load configspec self._configSpec = QtGui.QFileDialog.getOpenFileNames(self, "Open configspec File", '.', "configspec (*.cfg)", options=QtGui.QFileDialog.DontUseNativeDialog) else: self._configSpec = opts.configspec #self._toolbar = self.addToolBar("Toolbar") openFile = QtGui.QAction("&Open...", self, shortcut=QtGui.QKeySequence.Open, statusTip="Open configuration file", triggered=self.openFile) saveFile = QtGui.QAction("&Save", self, shortcut=QtGui.QKeySequence.Save, statusTip="Save configuration to disk", triggered=self.saveFile) #plotAction = QtGui.QAction("Plot", self, #shortcut=QtGui.QKeySequence.Save, #statusTip="Plot all polygons", #triggered=self.onPlot) fileMenu = self.menuBar().addMenu('&File') fileMenu.addAction(openFile) fileMenu.addAction(saveFile) #self.menuBar().addAction(plotAction) self.setAttribute(Qt.WA_DeleteOnClose) if self._options.cfg: self._loadQCobjs(self._options.cfg)
[docs] def _loadQCobjs(self, pn): """ Load all QConfigObj instances form file(s) in pn Remove blanks in polygons and create the widgets for every instance. """ if isinstance(pn, list): qcobjs = [QConfigObj(p, configspec=self._configSpec, strict=self._options.strict, noextra=self._options.noextra) for p in pn] else: qcobjs = [QConfigObj(pn, configspec=self._configSpec, strict=self._options.strict, noextra=self._options.noextra), ] # Remove blanks in polygons for q in qcobjs: q.walk(deBlank, call_on_sections=True, wordsPerLine=2) ntrees = len(qcobjs) self._trees = [TreeView(self) for i in range(ntrees)] self.models = [TreeModel(self) for i in range(ntrees)] allCfgLayout = QtGui.QHBoxLayout() self._filesThatChanged = [] #self._allLithologies = [] i = 0 for tree, model, qcobj in zip(self._trees, self.models, qcobjs): tree.setModel(model) model.dataChanged.connect(tree.resizeColumns) model.setupModelData(qcobj) #self._allLithologies.append(qcobj['Lithologies']) if i: model.setComparison(qcobjs[i -1]) else: pass ## Only for first cfg file #definedlithos = self._allLithologies[0].keys() #quantities = self._allLithologies[0][definedlithos[0]].keys() #quantities.insert(0, RHEOLOGY) # The Buttons btnsWidget = QtGui.QWidget(self) hbl = QtGui.QHBoxLayout() #layerBtn = QtGui.QPushButton('Lithologies', btnsWidget) #layerBtn.setEnabled(True) #layerBtn.clicked.connect(self.showLitho) #layerBtn.layer = LayerDialog(i, self) expandBtn = QtGui.QPushButton(EXPAND_ALL, btnsWidget) expandBtn.setEnabled(True) expandBtn.clicked.connect(self.toggleExpand) expandBtn.tree = tree #quantCombo = QtGui.QComboBox(self) #quantCombo.setEnabled(True) #quantCombo.addItems(quantities) #quantCombo.currentIndexChanged.connect(self._quantityChanged) #quantCombo.setCurrentIndex(-1) #hbl.addWidget(layerBtn) hbl.addWidget(expandBtn) #hbl.addWidget(quantCombo) btnsWidget.setLayout(hbl) singleCfgWidget = QtGui.QWidget(self) singleCfgLayout = QtGui.QVBoxLayout() singleCfgLayout.addWidget(btnsWidget) singleCfgLayout.addWidget(tree) singleCfgWidget.setLayout(singleCfgLayout) tree.resizeColumns() allCfgLayout.addWidget(singleCfgWidget) i += 1 # Add a row for widgets shared between allCfgLayout allCfgWidget = QtGui.QWidget() allCfgWidget.setLayout(allCfgLayout) # Set Central widget cWidget = QtGui.QWidget() vlo = QtGui.QVBoxLayout() if len(qcobjs) > 1: self._scrollLock = QtGui.QCheckBox("Lock scrollbars") self._scrollLock.stateChanged.connect(self._onScroll) vlo.addWidget(self._scrollLock) vlo.addWidget(allCfgWidget) cWidget.setLayout(vlo) self.setCentralWidget(cWidget) self.setWindowTitle('%s: %s' % (pn, [model.qcobj['description'] for model in self.models]))
def _onScroll(self, value): if value: # Connect all scrollbars together for t1, t2 in itertools.permutations(self._trees, 2): t1.verticalScrollBar().valueChanged.connect( t2.verticalScrollBar().setValue) else: # Connect all scrollbars together for t1, t2 in itertools.permutations(self._trees, 2): t1.verticalScrollBar().valueChanged.disconnect( t2.verticalScrollBar().setValue) def _quantityChanged(self, index): if index < 0: return sender = self.sender() quant = sender.itemText(index) html = self.makeHtml(quant) q = QuantityDialog(html, self) q.exec_()
[docs] def setFileChanged(self, filename): if filename not in self._filesThatChanged: self._filesThatChanged.append(filename)
[docs] def closeEvent(self, event): # Changes? if self._filesThatChanged: self.saveFile() event.accept() # let the window close
#def makeHtml(self, quant): #""" Return HTML table for quantity `quant` #""" #numLithologies = len(self._allLithologies) ## Create HTML output #html = ('<table border="1" width="100%%">') #if quant != RHEOLOGY: #html += '<tr>' #html += '<th bgcolor="LightGreen">Config file</th>' #for model in self.models: #html += ('<th bgcolor="LightGreen">%s</th>' % #os.path.basename(model.qcobj.filename)) #html += '</tr>' # end row #msglines = [] #values = [] #for i, lithosInCfg in enumerate(self._allLithologies): #if quant == RHEOLOGY: ## Add file name to html #html += ( #'<tr>' #'<td bgcolor="LightGreen"> </td>' #'<td colspan="5" align="center" bgcolor="LightGreen">' #'%s</td>' #'</tr>' % #os.path.basename(self.models[i].qcobj.filename)) #html += ( #'<tr>' #'<th bgcolor="GreenYellow">Given name</th>' #'<th bgcolor="IndianRed">Standard name</th>' #'<th bgcolor="DarkSalmon">AD</th>' #'<th bgcolor="DarkSalmon">Ea</th>' #'<th bgcolor="DarkSalmon">n</th>' #'<th bgcolor="DarkSalmon">Va</th>' #'</tr>') #for j, litho in enumerate(lithosInCfg): #lit = lithosInCfg[litho] #creep = Creep( #lit['AD'], lit['Ea'], lit['n'], lit['Va']) #for knownCreep in KNOWN_CREEPS: #if knownCreep[0] == creep: #html += ( #'<tr>' #'<th bgcolor="White">%s</th>' #'<th bgcolor="White">%s</th>' #'<th bgcolor="White">%s</th>' #'<th bgcolor="White">%s</th>' #'<th bgcolor="White">%s</th>' #'<th bgcolor="White">%s</th>' #'</tr>' % #(litho, knownCreep[1], creep.strAD(), #creep.strEa(), creep.strn(), #creep.strVa()) #) #break #else: #for j, litho in enumerate(lithosInCfg): #value = lithosInCfg[litho][quant] ## Check if value is a quantity #if hasattr(value, 'magnitude'): #value = '{:~P}'.format(value) #if i > 0: ## More than one config file: ## search for line starting with the same litho #found = False #for k, line in enumerate(msglines): #if line.startswith(litho): ## k is our index #found = True #break #if found: #if values[k].endswith(value): ## Same value use soft color #msglines[k] = ("%s:%s" % #(msglines[k], #colorize(value, 'LightBlue'))) #else: ## Different value use strong color #msglines[k] = ("%s:%s" % #(msglines[j], colorize(value, 'Red'))) #values[k] = "%s %s" % (values[k], value) #else: ## Add a line with the new litho #msglines.append("%s::%s" % (litho, value)) #else: #values.append(value) #msglines.append("%s:%s" % (litho, value)) #for line in msglines: ## Split line at ":" and put result in cells #html += ("<tr>" + "".join( #["<th>%s</th>" % f for f in line.split(":")]) + "</tr>") #html += "</table>" #return html
[docs] def openFile(self): pn = QtGui.QFileDialog.getOpenFileNames(self, "Open Configuration File", '.', "Configuration (*.cfg)", options=QtGui.QFileDialog.DontUseNativeDialog) if pn: self._loadQCobjs(pn)
#def onPlot(self): #width = 0 #depth = 0 #for model in self.models: #width = max(width, model.qcobj['Mesh']['X']['width'].magnitude) #depth = max(depth, model.qcobj['Mesh']['Y']['depth'].magnitude) #plotPolygons(self._allLithologies, width, depth)
[docs] def saveFile(self): for pn in self._filesThatChanged: extensions = "CFG (*.cfg)" from IPython import embed; embed() dlg = QtGui.QFileDialog(self, "Save configuration", pn) dlg.setNameFilter(extensions) dlg.setOptions(QtGui.QFileDialog.DontUseNativeDialog) dlg.setAcceptMode(QtGui.QFileDialog.AcceptSave) if dlg.exec_(): newpn = dlg.selectedFiles()[0] if newpn: # Search for the model with pn filename for model in self.models: if model.qcobj.filename == pn: break cfg = model.qcobj.write_to_string() now = datetime.now().strftime("%Y%m%d% at %H:%M:%S") with open(newpn, "w") as theFile: theFile.write("# Created by %s at %s\n\n" % (__file__, now)) theFile.write(cfg.decode('utf-8')) theFile.close()
#def showLitho(self, *args): #senderBtn = self.sender() #senderBtn.layer.setModal(False) #senderBtn.layer.show()
[docs] def toggleExpand(self, *args): senderBtn = self.sender() if senderBtn.text() == EXPAND_ALL: senderBtn.tree.expandAll() senderBtn.setText(COLLAPSE_ALL) else: senderBtn.tree.collapseAll() senderBtn.setText(EXPAND_ALL)