Source code for silx.gui.plot.CurvesROIWidget
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2017 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""Widget to handle regions of interest (ROI) on curves displayed in a PlotWindow.
This widget is meant to work with :class:`PlotWindow`.
ROI are defined by :
- A name (`ROI` column)
- A type. The type is the label of the x axis.
  This can be used to apply or not some ROI to a curve and do some post processing.
- The x coordinate of the left limit (`from` column)
- The x coordinate of the right limit (`to` column)
- Raw counts: integral of the curve between the
  min ROI point and the max ROI point to the y = 0 line
  .. image:: img/rawCounts.png
- Net counts: the integral of the curve between the
  min ROI point and the max ROI point to [ROI min point, ROI max point] segment
  .. image:: img/netCounts.png
"""
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "27/06/2017"
from collections import OrderedDict
import logging
import os
import sys
import numpy
from silx.io import dictdump
from .. import icons, qt
_logger = logging.getLogger(__name__)
[docs]class CurvesROIWidget(qt.QWidget):
    """Widget displaying a table of ROI information.
    :param parent: See :class:`QWidget`
    :param str name: The title of this widget
    """
    sigROIWidgetSignal = qt.Signal(object)
    """Signal of ROIs modifications.
    Modification information if given as a dict with an 'event' key
    providing the type of events.
    Type of events:
    - AddROI, DelROI, LoadROI and ResetROI with keys: 'roilist', 'roidict'
    - selectionChanged with keys: 'row', 'col' 'roi', 'key', 'colheader',
      'rowheader'
    """
    def __init__(self, parent=None, name=None):
        super(CurvesROIWidget, self).__init__(parent)
        if name is not None:
            self.setWindowTitle(name)
        layout = qt.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        ##############
        self.headerLabel = qt.QLabel(self)
        self.headerLabel.setAlignment(qt.Qt.AlignHCenter)
        self.setHeader()
        layout.addWidget(self.headerLabel)
        ##############
        self.roiTable = ROITable(self)
        rheight = self.roiTable.horizontalHeader().sizeHint().height()
        self.roiTable.setMinimumHeight(4 * rheight)
        self.fillFromROIDict = self.roiTable.fillFromROIDict
        self.getROIListAndDict = self.roiTable.getROIListAndDict
        layout.addWidget(self.roiTable)
        self._roiFileDir = qt.QDir.home().absolutePath()
        #################
        hbox = qt.QWidget(self)
        hboxlayout = qt.QHBoxLayout(hbox)
        hboxlayout.setContentsMargins(0, 0, 0, 0)
        hboxlayout.setSpacing(0)
        hboxlayout.addStretch(0)
        self.addButton = qt.QPushButton(hbox)
        self.addButton.setText("Add ROI")
        self.addButton.setToolTip('Create a new ROI')
        self.delButton = qt.QPushButton(hbox)
        self.delButton.setText("Delete ROI")
        self.addButton.setToolTip('Remove the selected ROI')
        self.resetButton = qt.QPushButton(hbox)
        self.resetButton.setText("Reset")
        self.addButton.setToolTip('Clear all created ROIs. We only let the default ROI')
        hboxlayout.addWidget(self.addButton)
        hboxlayout.addWidget(self.delButton)
        hboxlayout.addWidget(self.resetButton)
        hboxlayout.addStretch(0)
        self.loadButton = qt.QPushButton(hbox)
        self.loadButton.setText("Load")
        self.loadButton.setToolTip('Load ROIs from a .ini file')
        self.saveButton = qt.QPushButton(hbox)
        self.saveButton.setText("Save")
        self.loadButton.setToolTip('Save ROIs to a .ini file')
        hboxlayout.addWidget(self.loadButton)
        hboxlayout.addWidget(self.saveButton)
        layout.setStretchFactor(self.headerLabel, 0)
        layout.setStretchFactor(self.roiTable, 1)
        layout.setStretchFactor(hbox, 0)
        layout.addWidget(hbox)
        self.addButton.clicked.connect(self._add)
        self.delButton.clicked.connect(self._del)
        self.resetButton.clicked.connect(self._reset)
        self.loadButton.clicked.connect(self._load)
        self.saveButton.clicked.connect(self._save)
        self.roiTable.sigROITableSignal.connect(self._forward)
    @property
    def roiFileDir(self):
        """The directory from which to load/save ROI from/to files."""
        if not os.path.isdir(self._roiFileDir):
            self._roiFileDir = qt.QDir.home().absolutePath()
        return self._roiFileDir
    @roiFileDir.setter
    def roiFileDir(self, roiFileDir):
        self._roiFileDir = str(roiFileDir)
[docs]    def setRois(self, roidict, order=None):
        """Set the ROIs by providing a dictionary of ROI information.
        The dictionary keys are the ROI names.
        Each value is a sub-dictionary of ROI info with the following fields:
        - ``"from"``: x coordinate of the left limit, as a float
        - ``"to"``: x coordinate of the right limit, as a float
        - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
        :param roidict: Dictionary of ROIs
        :param str order: Field used for ordering the ROIs.
             One of "from", "to", "type".
             None (default) for no ordering, or same order as specified
             in parameter ``roidict`` if provided as an OrderedDict.
        """
        if order is None or order.lower() == "none":
            roilist = list(roidict.keys())
        else:
            assert order in ["from", "to", "type"]
            roilist = sorted(roidict.keys(),
                             key=lambda roi_name: roidict[roi_name].get(order))
        return self.roiTable.fillFromROIDict(roilist, roidict)
[docs]    def getRois(self, order=None):
        """Return the currently defined ROIs, as an ordered dict.
        The dictionary keys are the ROI names.
        Each value is a sub-dictionary of ROI info with the following fields:
        - ``"from"``: x coordinate of the left limit, as a float
        - ``"to"``: x coordinate of the right limit, as a float
        - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
        :param order: Field used for ordering the ROIs.
             One of "from", "to", "type", "netcounts", "rawcounts".
             None (default) to get the same order as displayed in the widget.
        :return: Ordered dictionary of ROI information
        """
        roilist, roidict = self.roiTable.getROIListAndDict()
        if order is None or order.lower() == "none":
            ordered_roilist = roilist
        else:
            assert order in ["from", "to", "type", "netcounts", "rawcounts"]
            ordered_roilist = sorted(roidict.keys(),
                                     key=lambda roi_name: roidict[roi_name].get(order))
        return OrderedDict([(name, roidict[name]) for name in ordered_roilist])
    def _add(self):
        """Add button clicked handler"""
        ddict = {}
        ddict['event'] = "AddROI"
        roilist, roidict = self.roiTable.getROIListAndDict()
        ddict['roilist'] = roilist
        ddict['roidict'] = roidict
        self.sigROIWidgetSignal.emit(ddict)
    def _del(self):
        """Delete button clicked handler"""
        row = self.roiTable.currentRow()
        if row >= 0:
            index = self.roiTable.labels.index('Type')
            text = str(self.roiTable.item(row, index).text())
            if text.upper() != 'DEFAULT':
                index = self.roiTable.labels.index('ROI')
                key = str(self.roiTable.item(row, index).text())
            else:
                # This is to prevent deleting ICR ROI, that is
                # usually initialized as "Default" type.
                return
            roilist, roidict = self.roiTable.getROIListAndDict()
            row = roilist.index(key)
            del roilist[row]
            del roidict[key]
            if len(roilist) > 0:
                currentroi = roilist[0]
            else:
                currentroi = None
            self.roiTable.fillFromROIDict(roilist=roilist,
                                          roidict=roidict,
                                          currentroi=currentroi)
            ddict = {}
            ddict['event'] = "DelROI"
            ddict['roilist'] = roilist
            ddict['roidict'] = roidict
            self.sigROIWidgetSignal.emit(ddict)
    def _forward(self, ddict):
        """Broadcast events from ROITable signal"""
        self.sigROIWidgetSignal.emit(ddict)
    def _reset(self):
        """Reset button clicked handler"""
        ddict = {}
        ddict['event'] = "ResetROI"
        roilist0, roidict0 = self.roiTable.getROIListAndDict()
        index = 0
        for key in roilist0:
            if roidict0[key]['type'].upper() == 'DEFAULT':
                index = roilist0.index(key)
                break
        roilist = []
        roidict = {}
        if len(roilist0):
            roilist.append(roilist0[index])
            roidict[roilist[0]] = {}
            roidict[roilist[0]].update(roidict0[roilist[0]])
            self.roiTable.fillFromROIDict(roilist=roilist, roidict=roidict)
        ddict['roilist'] = roilist
        ddict['roidict'] = roidict
        self.sigROIWidgetSignal.emit(ddict)
    def _load(self):
        """Load button clicked handler"""
        dialog = qt.QFileDialog(self)
        dialog.setNameFilters(
            ['INI File  *.ini', 'JSON File *.json', 'All *.*'])
        dialog.setFileMode(qt.QFileDialog.ExistingFile)
        dialog.setDirectory(self.roiFileDir)
        if not dialog.exec_():
            dialog.close()
            return
        # pyflakes bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=666494
        outputFile = dialog.selectedFiles()[0]
        dialog.close()
        self.roiFileDir = os.path.dirname(outputFile)
        self.load(outputFile)
[docs]    def load(self, filename):
        """Load ROI widget information from a file storing a dict of ROI.
        :param str filename: The file from which to load ROI
        """
        rois = dictdump.load(filename)
        currentROI = None
        if self.roiTable.rowCount():
            item = self.roiTable.item(self.roiTable.currentRow(), 0)
            if item is not None:
                currentROI = str(item.text())
        # Remove rawcounts and netcounts from ROIs
        for roi in rois['ROI']['roidict'].values():
            roi.pop('rawcounts', None)
            roi.pop('netcounts', None)
        self.roiTable.fillFromROIDict(roilist=rois['ROI']['roilist'],
                                      roidict=rois['ROI']['roidict'],
                                      currentroi=currentROI)
        roilist, roidict = self.roiTable.getROIListAndDict()
        event = {'event': 'LoadROI', 'roilist': roilist, 'roidict': roidict}
        self.sigROIWidgetSignal.emit(event)
    def _save(self):
        """Save button clicked handler"""
        dialog = qt.QFileDialog(self)
        dialog.setNameFilters(['INI File  *.ini', 'JSON File *.json'])
        dialog.setFileMode(qt.QFileDialog.AnyFile)
        dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
        dialog.setDirectory(self.roiFileDir)
        if not dialog.exec_():
            dialog.close()
            return
        outputFile = dialog.selectedFiles()[0]
        extension = '.' + dialog.selectedNameFilter().split('.')[-1]
        dialog.close()
        if not outputFile.endswith(extension):
            outputFile += extension
        if os.path.exists(outputFile):
            try:
                os.remove(outputFile)
            except IOError:
                msg = qt.QMessageBox(self)
                msg.setIcon(qt.QMessageBox.Critical)
                msg.setText("Input Output Error: %s" % (sys.exc_info()[1]))
                msg.exec_()
                return
        self.roiFileDir = os.path.dirname(outputFile)
        self.save(outputFile)
[docs]    def save(self, filename):
        """Save current ROIs of the widget as a dict of ROI to a file.
        :param str filename: The file to which to save the ROIs
        """
        roilist, roidict = self.roiTable.getROIListAndDict()
        datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
        dictdump.dump(datadict, filename)
[docs]    def setHeader(self, text='ROIs'):
        """Set the header text of this widget"""
        self.headerLabel.setText("<b>%s<\b>" % text)
[docs]class ROITable(qt.QTableWidget):
    """Table widget displaying ROI information.
    See :class:`QTableWidget` for constructor arguments.
    """
    sigROITableSignal = qt.Signal(object)
    """Signal of ROI table modifications.
    """
    def __init__(self, *args, **kwargs):
        super(ROITable, self).__init__(*args, **kwargs)
        self.setRowCount(1)
        self.labels = 'ROI', 'Type', 'From', 'To', 'Raw Counts', 'Net Counts'
        self.setColumnCount(len(self.labels))
        self.setSortingEnabled(False)
        for index, label in enumerate(self.labels):
            item = self.horizontalHeaderItem(index)
            if item is None:
                item = qt.QTableWidgetItem(label,
                                           qt.QTableWidgetItem.Type)
            item.setText(label)
            self.setHorizontalHeaderItem(index, item)
        self.roidict = {}
        self.roilist = []
        self.building = False
        self.fillFromROIDict(roilist=self.roilist, roidict=self.roidict)
        self.cellClicked[(int, int)].connect(self._cellClickedSlot)
        self.cellChanged[(int, int)].connect(self._cellChangedSlot)
        verticalHeader = self.verticalHeader()
        verticalHeader.sectionClicked[int].connect(self._rowChangedSlot)
        self.__setTooltip()
    def __setTooltip(self):
        assert(self.labels[0] == 'ROI')
        self.horizontalHeaderItem(0).setToolTip('Region of interest identifier')
        assert(self.labels[1] == 'Type')
        self.horizontalHeaderItem(1).setToolTip('Type of the ROI')
        assert(self.labels[2] == 'From')
        self.horizontalHeaderItem(2).setToolTip('X-value of the min point')
        assert(self.labels[3] == 'To')
        self.horizontalHeaderItem(3).setToolTip('X-value of the max point')
        assert(self.labels[4] == 'Raw Counts')
        self.horizontalHeaderItem(4).setToolTip('Estimation of the integral \
            between y=0 and the selected curve')
        assert(self.labels[5] == 'Net Counts')
        self.horizontalHeaderItem(5).setToolTip('Estimation of the integral \
            between the segment [maxPt, minPt] and the selected curve')
[docs]    def fillFromROIDict(self, roilist=(), roidict=None, currentroi=None):
        """Set the ROIs by providing a list of ROI names and a dictionary
        of ROI information for each ROI.
        The ROI names must match an existing dictionary key.
        The name list is used to provide an order for the ROIs.
        The dictionary's values are sub-dictionaries containing 3
        mandatory fields:
           - ``"from"``: x coordinate of the left limit, as a float
           - ``"to"``: x coordinate of the right limit, as a float
           - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
        :param roilist: List of ROI names (keys of roidict)
        :type roilist: List
        :param dict roidict: Dict of ROI information
        :param currentroi: Name of the selected ROI or None (no selection)
        """
        if roidict is None:
            roidict = {}
        self.building = True
        line0 = 0
        self.roilist = []
        self.roidict = {}
        for key in roilist:
            if key in roidict.keys():
                roi = roidict[key]
                self.roilist.append(key)
                self.roidict[key] = {}
                self.roidict[key].update(roi)
                line0 = line0 + 1
                nlines = self.rowCount()
                if (line0 > nlines):
                    self.setRowCount(line0)
                line = line0 - 1
                self.roidict[key]['line'] = line
                ROI = key
                roitype = "%s" % roi['type']
                fromdata = "%6g" % (roi['from'])
                todata = "%6g" % (roi['to'])
                if 'rawcounts' in roi:
                    rawcounts = "%6g" % (roi['rawcounts'])
                else:
                    rawcounts = " ?????? "
                if 'netcounts' in roi:
                    netcounts = "%6g" % (roi['netcounts'])
                else:
                    netcounts = " ?????? "
                fields = [ROI, roitype, fromdata, todata, rawcounts, netcounts]
                col = 0
                for field in fields:
                    key2 = self.item(line, col)
                    if key2 is None:
                        key2 = qt.QTableWidgetItem(field,
                                                   qt.QTableWidgetItem.Type)
                        self.setItem(line, col, key2)
                    else:
                        key2.setText(field)
                    if (ROI.upper() == 'ICR') or (ROI.upper() == 'DEFAULT'):
                            key2.setFlags(qt.Qt.ItemIsSelectable |
                                          qt.Qt.ItemIsEnabled)
                    else:
                        if col in [0, 2, 3]:
                            key2.setFlags(qt.Qt.ItemIsSelectable |
                                          qt.Qt.ItemIsEnabled |
                                          qt.Qt.ItemIsEditable)
                        else:
                            key2.setFlags(qt.Qt.ItemIsSelectable |
                                          qt.Qt.ItemIsEnabled)
                    col = col + 1
        self.setRowCount(line0)
        i = 0
        for _label in self.labels:
            self.resizeColumnToContents(i)
            i = i + 1
        self.sortByColumn(2, qt.Qt.AscendingOrder)
        for i in range(len(self.roilist)):
            key = str(self.item(i, 0).text())
            self.roilist[i] = key
            self.roidict[key]['line'] = i
        if len(self.roilist) == 1:
            self.selectRow(0)
        else:
            if currentroi in self.roidict.keys():
                self.selectRow(self.roidict[currentroi]['line'])
                _logger.debug("Qt4 ensureCellVisible to be implemented")
        self.building = False
[docs]    def getROIListAndDict(self):
        """Return the currently defined ROIs, as a 2-tuple
        ``(roiList, roiDict)``
        ``roiList`` is a list of ROI names.
        ``roiDict`` is a dictionary of ROI info.
        The ROI names must match an existing dictionary key.
        The name list is used to provide an order for the ROIs.
        The dictionary's values are sub-dictionaries containing 3
        fields:
           - ``"from"``: x coordinate of the left limit, as a float
           - ``"to"``: x coordinate of the right limit, as a float
           - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
        :return: ordered dict as a tuple of (list of ROI names, dict of info)
        """
        return self.roilist, self.roidict
    def _cellClickedSlot(self, *var, **kw):
        # selection changed event, get the current selection
        row = self.currentRow()
        col = self.currentColumn()
        if row >= 0 and row < len(self.roilist):
            item = self.item(row, 0)
            text = '' if item is None else str(item.text())
            self.roilist[row] = text
            self._emitSelectionChangedSignal(row, col)
    def _rowChangedSlot(self, row):
        self._emitSelectionChangedSignal(row, 0)
    def _cellChangedSlot(self, row, col):
        _logger.debug("_cellChangedSlot(%d, %d)", row, col)
        if self.building:
            return
        if col == 0:
            self.nameSlot(row, col)
        else:
            self._valueChanged(row, col)
    def _valueChanged(self, row, col):
        if col not in [2, 3]:
            return
        item = self.item(row, col)
        if item is None:
            return
        text = str(item.text())
        try:
            value = float(text)
        except:
            return
        if row >= len(self.roilist):
            _logger.debug("deleting???")
            return
        item = self.item(row, 0)
        if item is None:
            text = ""
        else:
            text = str(item.text())
        if not len(text):
            return
        if col == 2:
            self.roidict[text]['from'] = value
        elif col == 3:
            self.roidict[text]['to'] = value
        self._emitSelectionChangedSignal(row, col)
    def nameSlot(self, row, col):
        if col != 0:
            return
        if row >= len(self.roilist):
            _logger.debug("deleting???")
            return
        item = self.item(row, col)
        if item is None:
            text = ""
        else:
            text = str(item.text())
        if len(text) and (text not in self.roilist):
            old = self.roilist[row]
            self.roilist[row] = text
            self.roidict[text] = {}
            self.roidict[text].update(self.roidict[old])
            del self.roidict[old]
            self._emitSelectionChangedSignal(row, col)
    def _emitSelectionChangedSignal(self, row, col):
        ddict = {}
        ddict['event'] = "selectionChanged"
        ddict['row'] = row
        ddict['col'] = col
        ddict['roi'] = self.roidict[self.roilist[row]]
        ddict['key'] = self.roilist[row]
        ddict['colheader'] = self.labels[col]
        ddict['rowheader'] = "%d" % row
        self.sigROITableSignal.emit(ddict)
[docs]class CurvesROIDockWidget(qt.QDockWidget):
    """QDockWidget with a :class:`CurvesROIWidget` connected to a PlotWindow.
    It makes the link between the :class:`CurvesROIWidget` and the PlotWindow.
    :param parent: See :class:`QDockWidget`
    :param plot: :class:`.PlotWindow` instance on which to operate
    :param name: See :class:`QDockWidget`
    """
    sigROISignal = qt.Signal(object)
    def __init__(self, parent=None, plot=None, name=None):
        super(CurvesROIDockWidget, self).__init__(name, parent)
        assert plot is not None
        self.plot = plot
        self.currentROI = None
        self._middleROIMarkerFlag = False
        self._isConnected = False  # True if connected to plot signals
        self._isInit = False
        self.roiWidget = CurvesROIWidget(self, name)
        """Main widget of type :class:`CurvesROIWidget`"""
        # convenience methods to offer a simpler API allowing to ignore
        # the details of the underlying implementation
        self.calculateROIs = self.calculateRois
        self.setRois = self.roiWidget.setRois
        self.getRois = self.roiWidget.getRois
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.setWidget(self.roiWidget)
        self.visibilityChanged.connect(self._visibilityChangedHandler)
[docs]    def toggleViewAction(self):
        """Returns a checkable action that shows or closes this widget.
        See :class:`QMainWindow`.
        """
        action = super(CurvesROIDockWidget, self).toggleViewAction()
        action.setIcon(icons.getQIcon('plot-roi'))
        return action
    def _visibilityChangedHandler(self, visible):
        """Handle widget's visibilty updates.
        It is connected to plot signals only when visible.
        """
        if visible:
            if not self._isInit:
                # Deferred ROI widget init finalization
                self._isInit = True
                self.roiWidget.sigROIWidgetSignal.connect(self._roiSignal)
                # initialize with the ICR
                self._roiSignal({'event': "AddROI"})
            if not self._isConnected:
                self.plot.sigPlotSignal.connect(self._handleROIMarkerEvent)
                self.plot.sigActiveCurveChanged.connect(
                    self._activeCurveChanged)
                self._isConnected = True
                self.calculateROIs()
        else:
            if self._isConnected:
                self.plot.sigPlotSignal.disconnect(self._handleROIMarkerEvent)
                self.plot.sigActiveCurveChanged.disconnect(
                    self._activeCurveChanged)
                self._isConnected = False
    def _handleROIMarkerEvent(self, ddict):
        """Handle plot signals related to marker events."""
        if ddict['event'] == 'markerMoved':
            label = ddict['label']
            if label not in ['ROI min', 'ROI max', 'ROI middle']:
                return
            roiList, roiDict = self.roiWidget.getROIListAndDict()
            if self.currentROI is None:
                return
            if self.currentROI not in roiDict:
                return
            x = ddict['x']
            if label == 'ROI min':
                roiDict[self.currentROI]['from'] = x
                if self._middleROIMarkerFlag:
                    pos = 0.5 * (roiDict[self.currentROI]['to'] +
                                 roiDict[self.currentROI]['from'])
                    self.plot.addXMarker(pos,
                                         legend='ROI middle',
                                         text='',
                                         color='yellow',
                                         draggable=True)
            elif label == 'ROI max':
                roiDict[self.currentROI]['to'] = x
                if self._middleROIMarkerFlag:
                    pos = 0.5 * (roiDict[self.currentROI]['to'] +
                                 roiDict[self.currentROI]['from'])
                    self.plot.addXMarker(pos,
                                         legend='ROI middle',
                                         text='',
                                         color='yellow',
                                         draggable=True)
            elif label == 'ROI middle':
                delta = x - 0.5 * (roiDict[self.currentROI]['from'] +
                                   roiDict[self.currentROI]['to'])
                roiDict[self.currentROI]['from'] += delta
                roiDict[self.currentROI]['to'] += delta
                self.plot.addXMarker(roiDict[self.currentROI]['from'],
                                     legend='ROI min',
                                     text='ROI min',
                                     color='blue',
                                     draggable=True)
                self.plot.addXMarker(roiDict[self.currentROI]['to'],
                                     legend='ROI max',
                                     text='ROI max',
                                     color='blue',
                                     draggable=True)
            else:
                return
            self.calculateROIs(roiList, roiDict)
            self._emitCurrentROISignal()
    def _roiSignal(self, ddict):
        """Handle ROI widget signal"""
        _logger.debug("PlotWindow._roiSignal %s", str(ddict))
        if ddict['event'] == "AddROI":
            xmin, xmax = self.plot.getXAxis().getLimits()
            fromdata = xmin + 0.25 * (xmax - xmin)
            todata = xmin + 0.75 * (xmax - xmin)
            self.plot.remove('ROI min', kind='marker')
            self.plot.remove('ROI max', kind='marker')
            if self._middleROIMarkerFlag:
                self.remove('ROI middle', kind='marker')
            roiList, roiDict = self.roiWidget.getROIListAndDict()
            nrois = len(roiList)
            if nrois == 0:
                newroi = "ICR"
                fromdata, dummy0, todata, dummy1 = self._getAllLimits()
                draggable = False
                color = 'black'
            else:
                for i in range(nrois):
                    i += 1
                    newroi = "newroi %d" % i
                    if newroi not in roiList:
                        break
                color = 'blue'
                draggable = True
            self.plot.addXMarker(fromdata,
                                 legend='ROI min',
                                 text='ROI min',
                                 color=color,
                                 draggable=draggable)
            self.plot.addXMarker(todata,
                                 legend='ROI max',
                                 text='ROI max',
                                 color=color,
                                 draggable=draggable)
            if draggable and self._middleROIMarkerFlag:
                pos = 0.5 * (fromdata + todata)
                self.plot.addXMarker(pos,
                                     legend='ROI middle',
                                     text="",
                                     color='yellow',
                                     draggable=draggable)
            roiList.append(newroi)
            roiDict[newroi] = {}
            if newroi == "ICR":
                roiDict[newroi]['type'] = "Default"
            else:
                roiDict[newroi]['type'] = self.plot.getXAxis().getLabel()
            roiDict[newroi]['from'] = fromdata
            roiDict[newroi]['to'] = todata
            self.roiWidget.fillFromROIDict(roilist=roiList,
                                           roidict=roiDict,
                                           currentroi=newroi)
            self.currentROI = newroi
            self.calculateROIs()
        elif ddict['event'] in ['DelROI', "ResetROI"]:
            self.plot.remove('ROI min', kind='marker')
            self.plot.remove('ROI max', kind='marker')
            if self._middleROIMarkerFlag:
                self.plot.remove('ROI middle', kind='marker')
            roiList, roiDict = self.roiWidget.getROIListAndDict()
            roiDictKeys = list(roiDict.keys())
            if len(roiDictKeys):
                currentroi = roiDictKeys[0]
            else:
                # create again the ICR
                ddict = {"event": "AddROI"}
                return self._roiSignal(ddict)
            self.roiWidget.fillFromROIDict(roilist=roiList,
                                           roidict=roiDict,
                                           currentroi=currentroi)
            self.currentROI = currentroi
        elif ddict['event'] == 'LoadROI':
            self.calculateROIs()
        elif ddict['event'] == 'selectionChanged':
            _logger.debug("Selection changed")
            self.roilist, self.roidict = self.roiWidget.getROIListAndDict()
            fromdata = ddict['roi']['from']
            todata = ddict['roi']['to']
            self.plot.remove('ROI min', kind='marker')
            self.plot.remove('ROI max', kind='marker')
            if ddict['key'] == 'ICR':
                draggable = False
                color = 'black'
            else:
                draggable = True
                color = 'blue'
            self.plot.addXMarker(fromdata,
                                 legend='ROI min',
                                 text='ROI min',
                                 color=color,
                                 draggable=draggable)
            self.plot.addXMarker(todata,
                                 legend='ROI max',
                                 text='ROI max',
                                 color=color,
                                 draggable=draggable)
            if draggable and self._middleROIMarkerFlag:
                pos = 0.5 * (fromdata + todata)
                self.plot.addXMarker(pos,
                                     legend='ROI middle',
                                     text="",
                                     color='yellow',
                                     draggable=True)
            self.currentROI = ddict['key']
            if ddict['colheader'] in ['From', 'To']:
                dict0 = {}
                dict0['event'] = "SetActiveCurveEvent"
                dict0['legend'] = self.plot.getActiveCurve(just_legend=1)
                self.plot.setActiveCurve(dict0['legend'])
            elif ddict['colheader'] == 'Raw Counts':
                pass
            elif ddict['colheader'] == 'Net Counts':
                pass
            else:
                self._emitCurrentROISignal()
        else:
            _logger.debug("Unknown or ignored event %s", ddict['event'])
    def _activeCurveChanged(self, *args):
        """Recompute ROIs when active curve changed."""
        self.calculateROIs()
[docs]    def calculateRois(self, roiList=None, roiDict=None):
        """Compute ROI information"""
        if roiList is None or roiDict is None:
            roiList, roiDict = self.roiWidget.getROIListAndDict()
        activeCurve = self.plot.getActiveCurve(just_legend=False)
        if activeCurve is None:
            xproc = None
            yproc = None
            self.roiWidget.setHeader()
        else:
            x = activeCurve.getXData(copy=False)
            y = activeCurve.getYData(copy=False)
            legend = activeCurve.getLegend()
            idx = numpy.argsort(x, kind='mergesort')
            xproc = numpy.take(x, idx)
            yproc = numpy.take(y, idx)
            self.roiWidget.setHeader('ROIs of %s' % legend)
        for key in roiList:
            if key == 'ICR':
                if xproc is not None:
                    roiDict[key]['from'] = xproc.min()
                    roiDict[key]['to'] = xproc.max()
                else:
                    roiDict[key]['from'] = 0
                    roiDict[key]['to'] = -1
            fromData = roiDict[key]['from']
            toData = roiDict[key]['to']
            if xproc is not None:
                idx = numpy.nonzero((fromData <= xproc) &
                                    (xproc <= toData))[0]
                if len(idx):
                    xw = xproc[idx]
                    yw = yproc[idx]
                    rawCounts = yw.sum(dtype=numpy.float)
                    deltaX = xw[-1] - xw[0]
                    deltaY = yw[-1] - yw[0]
                    if deltaX > 0.0:
                        slope = (deltaY / deltaX)
                        background = yw[0] + slope * (xw - xw[0])
                        netCounts = (rawCounts -
                                     background.sum(dtype=numpy.float))
                    else:
                        netCounts = 0.0
                else:
                    rawCounts = 0.0
                    netCounts = 0.0
                roiDict[key]['rawcounts'] = rawCounts
                roiDict[key]['netcounts'] = netCounts
            else:
                roiDict[key].pop('rawcounts', None)
                roiDict[key].pop('netcounts', None)
        self.roiWidget.fillFromROIDict(
            roilist=roiList,
            roidict=roiDict,
            currentroi=self.currentROI if self.currentROI in roiList else None)
    def _emitCurrentROISignal(self):
        ddict = {}
        ddict['event'] = "currentROISignal"
        _roiList, roiDict = self.roiWidget.getROIListAndDict()
        if self.currentROI in roiDict:
            ddict['ROI'] = roiDict[self.currentROI]
        else:
            self.currentROI = None
        ddict['current'] = self.currentROI
        self.sigROISignal.emit(ddict)
    def _getAllLimits(self):
        """Retrieve the limits based on the curves."""
        curves = self.plot.getAllCurves()
        if not curves:
            return 1.0, 1.0, 100., 100.
        xmin, ymin = None, None
        xmax, ymax = None, None
        for curve in curves:
            x = curve.getXData(copy=False)
            y = curve.getYData(copy=False)
            if xmin is None:
                xmin = x.min()
            else:
                xmin = min(xmin, x.min())
            if xmax is None:
                xmax = x.max()
            else:
                xmax = max(xmax, x.max())
            if ymin is None:
                ymin = y.min()
            else:
                ymin = min(ymin, y.min())
            if ymax is None:
                ymax = y.max()
            else:
                ymax = max(ymax, y.max())
        return xmin, ymin, xmax, ymax
[docs]    def showEvent(self, event):
        """Make sure this widget is raised when it is shown
        (when it is first created as a tab in PlotWindow or when it is shown
        again after hiding).
        """
        self.raise_()
