Source code for gui.rangelist

# -*- coding: utf-8 -*-
# gui/rangelist.py

from __future__ import absolute_import # PEP328
from builtins import zip
import logging

from gui.qt import QtCore, QtGui
from QtCore import Qt, QFileInfo, QMargins
from QtGui import (QWidget, QHBoxLayout, QVBoxLayout, QPushButton,
                   QLabel, QComboBox, QPalette, QDialog, QSpinBox,
                   QCheckBox)
import numpy as np
from gui.bases.datalist import DataList
from gui.bases.mixins import AppSettings
from utils import isList, testfor
from utils.parameter import (ParameterBase, ParameterNumerical, Histogram,
                             isFitParam, isActiveFitParam)
from gui.calc import Calculator

# do not remove, dialog will not work without this
from gui.scientrybox import SciEntryBox

from bases.model import ScatteringModel

# required for svg graphics support
from gui.qt import QtSvg, QtXml, pluginDirs
from gui.liststyle import setBackgroundStyleSheet
from main import makeAbsolutePath

[docs]def getItemIndex(comboBox, text): for i in range(comboBox.count()): if comboBox.itemText(i) == text: return i return -1 # select none, usually
[docs]class RangeDialog(QDialog): """Creates a modal dialog window to ask the user for a range to be added.""" _model = None _labels = None def __init__(self, parent = None, model = None): QDialog.__init__(self, parent) assert isinstance(model, ScatteringModel) self._model = model self.setObjectName("AddRangeDialog") self.setWindowTitle("Add Range") self.setWindowModality(Qt.WindowModal) vlayout = QVBoxLayout(self) vlayout.setObjectName("vlayout") vlayout.addWidget(self._createEntries()) vlayout.addWidget(self._createButtons()) self.setLayout(vlayout) def _createEntries(self): entryWidget = QWidget(self) entryLayout = QHBoxLayout(entryWidget) inputWidgets = (self._createParamBox(), self._createAutoRange(), self._createLower(), self._createUpper(), self._createBins(), self._createXScale(), self._createYWeight()) self._labels = dict() # assumes same ordering of entryWidgets above and Histogram.displayData for col, inputWidget in zip(Histogram.displayData, inputWidgets): fieldWidget = QWidget(self) # combines label + input fieldLayout = QVBoxLayout(fieldWidget) fieldLayout.setContentsMargins(QMargins()) # create label, text is set in _selectParam() self._labels[col] = QLabel(self) self._labels[col].setAlignment(Qt.AlignHCenter) # stack label + input fieldLayout.addWidget(self._labels[col]) fieldLayout.addWidget(inputWidget) fieldWidget.setLayout(fieldLayout) # add field to row of inputs entryLayout.addWidget(fieldWidget) entryWidget.setLayout(entryLayout) self.pbox.setCurrentIndex(0) self.lentry.selectAll() # select the first input by default return entryWidget def _createAutoRange(self): autoRange = QCheckBox(self) autoRange.stateChanged.connect(self._onAutoRangeChange) self.autoRange = autoRange return autoRange def _onAutoRangeChange(self, isChecked): isChecked = (isChecked == Qt.Checked) # no tristate enableLimits = not isChecked self.lentry.setEnabled(enableLimits) self.uentry.setEnabled(enableLimits) self._setLimits() def _createLower(self): # add input for lower limit lentry = SciEntryBox(self) lentry.setPrefix("lower: ") self.lentry = lentry return lentry def _createUpper(self): # add input for upper limit uentry = SciEntryBox(self) uentry.setPrefix("upper: ") self.uentry = uentry return uentry def _createBins(self): # number of histogram bin input box bentry = QSpinBox(self) bentry.setRange(1, 200) bentry.setValue(50) bentry.setSingleStep(10) self.bentry = bentry return bentry def _createParamBox(self): # add a parameter choice list pbox = QComboBox(self) for p in self._model.params(): if not hasattr(p, "isActive"): #not a fit parameter, a regular one continue if not p.isActive(): continue # providing the internal param name as item data pbox.addItem(p.displayName(), p.name()) # pbox.addItem("Test Dummy", "dummy") pbox.setCurrentIndex(-1) pbox.currentIndexChanged[int].connect(self._selectParam) self.pbox = pbox return pbox def _createXScale(self): # histogram scaling choice (X-axis only at this point) sentry = QComboBox(self) for name in Histogram.xscaling(): sentry.addItem(name) # select a default x scaling, for DataObj aware settings, RangeList has # to be connected to DataList in MainWindow by an onDataSelected() slot sentry.setCurrentIndex(getItemIndex(sentry, "lin")) self.sentry = sentry return sentry def _createYWeight(self): # histogram weighting input wentry = QComboBox(self) for name in Histogram.yweighting(): wentry.addItem(name) self.wentry = wentry return wentry def _getParam(self, index): pname = self.pbox.itemData(index) p = getattr(self._model, pname) # perhaps, use testfor() for that: assert p is not None, "Could not find parameter from selection box" return p def _setLimits(self): p = self._getParam(self.pbox.currentIndex()) lval, uval = p.activeRange() try: # account for units conversion lval, uval = p.toDisplay(lval), p.toDisplay(uval) except AttributeError: # raise # will break here, use it for debugging pass self.lentry.setValue(lval) self.uentry.setValue(uval) def _selectParam(self, index): """Gets the index within the selection box.""" p = self._getParam(index) llim, ulim = p.valueRange() try: # account for units conversion llim, ulim = p.toDisplay(llim), p.toDisplay(ulim) except AttributeError: # raise # will break here, use it for debugging pass self.lentry.setRange(llim, ulim) self.uentry.setRange(llim, ulim) self.autoRange.setCheckState(Qt.Checked) # sets limits too # set labels for col, text in zip(Histogram.displayData, Histogram.displayDataDescr): if "lower" in col or "upper" in col: text = u"{t} ({u})".format(t = text, u = p.displayMagnitudeName()) elif any([(l in text) for l in ("axis", "bins", "range")]): # break long descriptions to keep them short text = text.replace(" ", "\n", 1) self._labels[col].setText(text) def _createButtons(self): btnWidget = QWidget(self) btnLayout = QHBoxLayout(btnWidget) okBtn = QPushButton("add", self) okBtn.clicked.connect(self.accept) cancelBtn = QPushButton("cancel", self) cancelBtn.clicked.connect(self.reject) btnLayout.addWidget(okBtn) btnLayout.addWidget(cancelBtn) btnWidget.setLayout(btnLayout) return btnWidget
[docs] def output(self): if not self.exec_() or self.lentry.value() == self.uentry.value(): return None p = None try: pname = self.pbox.itemData(self.pbox.currentIndex()) p = getattr(self._model, pname) except: return None lval, uval = (self.lentry.value(), self.uentry.value()) try: # take units into account, # convert from display units to SI units for internal use lval, uval = (p.toSi(lval), p.toSi(uval)) except AttributeError: pass hist = Histogram(p, lval, uval, self.bentry.value(), self.sentry.currentText(), self.wentry.currentText()) hist.autoFollow = self.autoRange.isChecked() return hist
[docs]class RangeList(DataList, AppSettings): _calculator = None def __init__(self, calculator = None, appSettings = None, **kwargs): DataList.__init__(self, **kwargs) assert isinstance(calculator, Calculator) self._calculator = calculator self.appSettings = appSettings self.sigRemovedData.connect(self.onRemoval) self.listWidget.mouseDoubleClickEvent = self.mouseDoubleClickEvent
[docs] def mouseDoubleClickEvent(self, event): """Shows RangeDialog on double click.""" self.loadData()
[docs] def itemUpdate(self, item, column): # handle autoFollow only, i.e. auto range update changes only if column != Histogram.displayData.index("autoFollow"): return # update histogram based on auto range update on/off hist = item.data() hist.autoFollow = (item.checkState(column) == Qt.Checked) if hist.autoFollow: hist.updateRange() item.update()
[docs] def onRemoval(self, removedHistograms): for hist in removedHistograms: try: hist.param.histograms().remove(hist) except AttributeError: continue
[docs] def append(self, histList): if histList is None: return if not isList(histList): histList = [histList] if not len(histList): return # put a copy of histograms into the ui list, otherwise comparison fails DataList.loadData(self, sourceList = histList, showProgress = False, processSourceFunc = lambda x: x) self.fitColumnsToContents()
[docs] def loadData(self, ranges = None): """Overridden base class method for adding entries to the list.""" # add only one item at a time into the list # set up dialog window for "add range" dialog = RangeDialog(self, self._calculator.model) newHist = dialog.output() if not isinstance(newHist, Histogram): return # do not add duplicates if newHist in self.data(): return # add it to the actual histogram list of the parameter newHist.param.histograms().append(newHist) # hmm, funny here # update the GUI based on that self.updateHistograms()
def _getParams(self): return self._calculator.model.params()
[docs] def updateHistograms(self): """Called after UI update by sigBackendUpdated from an AlgorithmWidget.""" lst = [] for p in self._getParams(): if not isActiveFitParam(p): continue # show the active fit parameters only lst += p.histograms() # finally, the RangeList gets the same hist instances which are in the # parameters, no copy, therefore just rebuild the UI to get in sync # comparison doesn't work: set(self.data()) == set(lst) # the data item has to same instance stored which comes in here self.clear() self.append(lst)
def _beginHistogramGroup(self, model): """Sets the AppSettings group for the current model where histograms can be stored.""" self.setRootGroup() self.appSettings.beginGroup("Model") self.appSettings.beginGroup(model.name()) self.appSettings.beginGroup("histograms")
[docs] def storeSession(self): if self.appSettings is None: return self._beginHistogramGroup(self._calculator.model) # remove all existing histograms from persistent storage for child in self.appSettings.childGroups(): self.appSettings.remove(child) # store current histograms from parameters which may not be visible hists = [] for p in self._getParams(): if not isFitParam(p): continue # get all fitable parameters hists += p.histograms() for i, h in enumerate(hists): self.appSettings.beginGroup(str(i)) for key in h.integralProps(): value = getattr(h, key, None) if isinstance(value, ParameterBase): value = value.name() if isinstance(value, float): # avoid issues on restore with unequal floats value = repr(value) self.appSettings.setValue(key, value) self.appSettings.endGroup() self.setRootGroup()
[docs] def restoreSession(self): """Load last known user settings from persistent app settings.""" if self.appSettings is None: return def parseHistogram(settings): initProps = [] param = None for key in Histogram.integralProps(): value = settings.value(key, None) if key == "param": param = getattr(self._calculator.model, value, None) # get FitParameters which might not be active if param is None or not isFitParam(param): return None value = param initProps.append(value) return Histogram(*initProps) self.clear() self._beginHistogramGroup(self._calculator.model) histCfg = dict() for iKey in self.appSettings.childGroups(): i = -1 try: i = int(iKey) except ValueError: continue if i < 0 or str(i) != iKey: continue self.appSettings.beginGroup(iKey) # append the histogram to the temporary list of a parameter hist = parseHistogram(self.appSettings) self.appSettings.endGroup() if hist is not None and hist.param is not None: if hist.param.name() not in histCfg: histCfg[hist.param.name()] = [] histCfg[hist.param.name()].append(hist) self.setRootGroup() for pname, histLst in histCfg.items(): if not len(histLst): continue param = histLst[0].param # parseHistogram() above return FitParameters only param.histograms().clear() param.histograms().extend(histLst) self.updateHistograms()
[docs] def setupUi(self): setBackgroundStyleSheet(self, "./resources/background_ranges.svg") self.listWidget.setRootIsDecorated(False) self.listWidget.setUniformRowHeights(True) self.listWidget.setItemsExpandable(False) self.listWidget.setAlternatingRowColors(True) self.action("load").setText("add range") # fix default action name self.clearSelection() self.setHeader(Histogram.displayDataDescr) self.setToolTip( "Right-click to add additional ranges." )
# self.addMenuEntry(name = "edit", text = "Edit selected", # menuStates = "hasSelection", # callbacks = self.editEntry) # self.addMenuEntry(name = "recalc", text = "recalc histograms", # menuStates = "hasSelection", # callbacks = self.recalc)
[docs] def editEntry(self): return
# more on the TODO list... Will figure this out later.
[docs] def recalc(self): return # this is not the _calculator instance I was looking for. # self._calculator(recalc = True) # does not work yet! missing arguments for Hist.calc() DataList.updateData(self, selectedOnly = True, showProgress = False, updateFunc = Histogram.calc, stopFunc = None)
# vim: set ts=4 sts=4 sw=4 tw=0: