class ConsoleStream(QtCore.QObject):
    _stdout = None
    _stderr = None

    messageWritten = QtCore.pyqtSignal(str, bool)

    def flush(self):
        pass

    def fileno(self):
        return -1

    def write(self, msg, html=True):
        if (not self.signalsBlocked()):
            self.messageWritten.emit(msg, html)

    @staticmethod
    def stdout():
        if (not ConsoleStream._stdout):
            ConsoleStream._stdout = ConsoleStream()
            sys.stdout = ConsoleStream._stdout
        return ConsoleStream._stdout

    @staticmethod
    def stderr():
        if (not ConsoleStream._stderr):
            ConsoleStream._stderr = ConsoleStream()
            sys.stderr = ConsoleStream._stderr
        return ConsoleStream._stderr
Exemplo n.º 2
0
 def raiseContextMenu(self, ev):
     """
     Raise the context menu
     """
     if not self.menuEnabled():
         return
     menu = self.menu  # getMenu()
     pos = ev.screenPos()
     menu.popup(QtCore.QPoint(pos.x(), pos.y()))
 def add_page(self, widget):
     scrollarea = QtGui.QScrollArea(self)
     scrollarea.setWidgetResizable(True)
     scrollarea.setWidget(widget)
     self.pages_widget.addWidget(scrollarea)
     item = QtGui.QListWidgetItem(self.contents_widget)
     try:
         item.setIcon(widget.icon)
     except TypeError:
         pass
     item.setText(widget.title)
     item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
     item.setSizeHint(QtCore.QSize(0, 25))
Exemplo n.º 4
0
    def __init__(self, parent):
        QtCore.QObject.__init__(self)
        self._parent = parent

        # Autosave feature
        self.autosaveTimer = QtCore.QTimer()
        self.autosaveTimer.setInterval(30000)
        self.autosaveTimer.timeout.connect(self.saveProject)

        # Open last_project
        last_project = scp.preferences.last_project
        if last_project:
            last_project = Path(last_project).with_suffix(".pscp")
        if scp.preferences.autoload_project and last_project is not None and last_project.exists():
            scp.debug_(f'Open last project {last_project}')
            self.project = last_project
        else:
            self.openProject(new=True)
Exemplo n.º 5
0
class CustomViewBox(ViewBox):
    """
    Subclass of ViewBox
    """
    sigPlotModeChanged = QtCore.Signal(object)
    sigColorMapChanged = QtCore.Signal(object)
    sigLineWidthChanged = QtCore.Signal(object)

    def __init__(self, parent=None, ndim=1, prefs=None):
        """
        Constructor of the CustomViewBox
        """
        super().__init__(parent)

        self.ndim = ndim
        self.prefs = prefs

        self.setRectMode()  # Set mouse mode to rect for convenient zooming
        self.menu = None  # Override pyqtgraph ViewBoxMenu
        self.menu = self.getMenu()  # Create the menu

    def raiseContextMenu(self, ev):
        """
        Raise the context menu
        """
        if not self.menuEnabled():
            return
        menu = self.menu  # getMenu()
        pos = ev.screenPos()
        menu.popup(QtCore.QPoint(pos.x(), pos.y()))

    def getMenu(self):
        """
        Create the menu
        """
        if self.menu is None:
            self.menu = QtGui.QMenu()

        self.PlotModeMenu = QtGui.QMenu("Plot mode")
        self.plotModeCombo = QtGui.QComboBox()
        self.modeItems = [
            'pen', 'scatter', 'scatter+pen', 'bar'
        ] if self.ndim < 2 else ['stack', 'map', 'image', 'surface']
        self.plotModeCombo.insertItems(1, self.modeItems)
        self.plotModeCombo.activated.connect(self.emitPlotModeChanged)
        self.plotModeAction = QtGui.QWidgetAction(None)
        self.plotModeAction.setDefaultWidget(self.plotModeCombo)
        self.PlotModeMenu.addAction(self.plotModeAction)
        self.menu.addMenu(self.PlotModeMenu)

        if self.ndim == 2:
            self.ColorMapMenu = QtGui.QMenu("Colormap")
            self.colorMapCombo = QtGui.QComboBox()
            self.colorMapItems = plt.colormaps()
            self.colorMapCombo.insertItems(1, self.colorMapItems)
            self.colorMapCombo.setCurrentIndex(
                self.colorMapItems.index(self.prefs.colormap))
            self.colorMapCombo.activated.connect(self.emitColorMapChanged)
            self.colorMapAction = QtGui.QWidgetAction(None)
            self.colorMapAction.setDefaultWidget(self.colorMapCombo)
            self.ColorMapMenu.addAction(self.colorMapAction)
            self.menu.addMenu(self.ColorMapMenu)

        self.LineMenu = QtGui.QMenu("Linewidth")
        self.lineSpinBox = pg.SpinBox(value=1, step=0.1, decimals=3)
        self.lineSpinBox.sigValueChanged.connect(self.emitLineWidthChanged)
        self.lineAction = QtGui.QWidgetAction(None)
        self.lineAction.setDefaultWidget(self.lineSpinBox)
        self.LineMenu.addAction(self.lineAction)
        self.menu.addMenu(self.LineMenu)

        self.viewAll = QtGui.QAction("Zoom reset", self.menu)
        self.viewAll.triggered.connect(self.autoRange)
        self.menu.addAction(self.viewAll)

        self.leftMenu = QtGui.QMenu("Left mouse click mode")
        group = QtGui.QActionGroup(self)
        pan = QtGui.QAction(u'Pan', self.leftMenu)
        zoom = QtGui.QAction(u'Zoom', self.leftMenu)
        self.leftMenu.addAction(pan)
        self.leftMenu.addAction(zoom)
        pan.triggered.connect(self.setPanMode)
        zoom.triggered.connect(self.setRectMode)
        pan.setCheckable(True)
        zoom.setCheckable(True)
        pan.setActionGroup(group)
        zoom.setActionGroup(group)
        self.menu.addMenu(self.leftMenu)

        self.menu.addSeparator()

        return self.menu

    def emitLineWidthChanged(self, val):
        self.sigLineWidthChanged.emit(float(val.value()))

    def emitPlotModeChanged(self, index):
        mode = self.modeItems[index]
        self.sigPlotModeChanged.emit(mode)

    def emitColorMapChanged(self, index):
        color = self.colorMapItems[index]
        self.sigColorMapChanged.emit(color)

    def setRectMode(self):
        """
        Set mouse mode to rect
        """
        self.setMouseMode(self.RectMode)

    def setPanMode(self):
        """
        Set mouse mode to pan
        """
        self.setMouseMode(self.PanMode)
Exemplo n.º 6
0
class PlotWidget(GraphicsLayoutWidget):

    sigZoomReset = QtCore.Signal()

    _dataset = None  # current dataset associated to the plotwidget
    _autorange = True

    # ..................................................................................................................
    def __init__(self, parent):

        # Use the Qt's GraphicsView framework offered by Pyqtgraph
        super().__init__(title='')

        self.parent = parent

        # Prepare additionnal traces
        self.selected = None  # traces selected in 2D spectra
        self.selected_pen = None  # original pen of a selected curve
        self.sigZoomReset.connect(self.zoomReset)

    # ..................................................................................................................
    def _masked(self, data, mask):
        """
        Utility function which returns a masked array.
        """
        if self._dataset is None:
            return None

        if not np.any(mask):
            mask = np.zeros(data.shape).astype(bool)
        data = np.ma.masked_where(mask, data)  # np.ma.masked_array(data, mask)
        return data

    # ..................................................................................................................
    @property
    def dataset(self):
        """
        Returns the current dataset
        """
        return self._dataset

    # ..................................................................................................................
    @dataset.setter
    def dataset(self, val):
        """
        Set the current dataset.
        """
        self._dataset = val

    # ..................................................................................................................
    @property
    def data(self):
        """
        Returns the current dataset masked data.
        """
        # z intensity (by default we plot real component of the data)

        return self._masked(np.real(self._dataset.data), self._dataset.mask)

    # ..................................................................................................................
    @property
    def processeddata(self):
        """
        Returns processeddata with the same mask as the original dataset.
        """
        if self._dataset.processeddata is None:
            return None
        return self._masked(np.real(self._dataset.processeddata),
                            self._dataset.processedmask)

    # ..................................................................................................................
    @property
    def baselinedata(self):
        """
        Returns baselinedata with the same mask as the original dataset.
        """
        if self._dataset.baselinedata is None:
            return None
        return self._masked(np.real(self._dataset.baselinedata),
                            self._dataset.mask)

    # ..................................................................................................................
    @property
    def referencedata(self):
        """
        Returns referencedata with the same mask as the original dataset.
        """
        if self._dataset.referencedata is None:
            return None
        return self._masked(np.real(self._dataset.referencedata),
                            self._dataset.mask)

#..................................................................................................................

    def draw_regions(self, reg=None):

        procs = self.parent.controller.params.param('processing').children()
        for proc in procs:
            if proc.name().startswith('define regions'):
                kind = proc.param('kind').value()
                if kind == 'undefined' or not hasattr(proc, 'regions'):
                    return
                dim = self.dataset.dims[-1]
                if not proc.param('regiongroup').childs[0].value().startswith(
                        dim):
                    continue

                for el, par in proc.regions.regionItems.values():
                    if el._name.startswith(f"{dim}_{kind}_"):
                        if not proc.opts['expanded']:
                            self.p.removeItem(el)
                        else:
                            self.p.addItem(el, ignoreBounds=True)

    # ..................................................................................................................
    def changeColorMap(self, map):
        self.dataset.meta['colormap'] = map
        self.draw(self.dataset)

    def changePlotMode(self, mode):
        self.dataset.meta['plotmode'] = mode
        self.draw(self.dataset)

    def changeLineWidth(self, lw):
        self.dataset.meta['linewidth'] = lw
        self.draw(self.dataset)

    def zoomReset(self):
        self._autorange = True

    def inverted(self, lim):
        if lim[0] > lim[1]:
            return True
        return False

    # ..................................................................................................................
    def draw(self, dataset, zoom_reset=False):
        """
        Draw 1D or 2D dataset corresponding to the current dataset.

        Parameters
        ----------
        zoom_reset: bool, optional
            True if the x and y range must be reset to the full range when redrawing.
        """

        self.dataset = dataset
        if self._autorange:
            zoom_reset = True

        # Create the main plotItem
        if not hasattr(self, 'p'):
            vb = CustomViewBox(ndim=dataset.ndim, prefs=dataset.preferences)
            self.p = self.addPlot(row=0, col=0, viewBox=vb)

        # Draw main data
        scp.debug_('>>>>>>>>>> Draw')
        self._draw(zoom_reset=zoom_reset)

        # Draw processed
        self._draw_processed(zoom_reset=zoom_reset)

        self._autorange = False

    # ..................................................................................................................
    def _draw_processed(self, **kwargs):
        # Draw in a second viewbox the processed data.

        if self.dataset.processeddata is None:
            if not hasattr(self, 'proc'):
                return
            else:
                # Try to remove the processing plotItem if it exists
                self.removeItem(self.proc)
                del self.proc
                self.p.setTitle('')
                return

        if not hasattr(self, 'proc'):
            # we have processed data but not yet the corresponding plotItem: create one.
            vb = CustomViewBox(ndim=self.dataset.ndim,
                               prefs=self.dataset.preferences)
            self.proc = self.addPlot(row=1,
                                     col=0,
                                     title='Processed dataset',
                                     viewBox=vb)
            self.p.setTitle('Original dataset')

        self._draw(plotitem=self.proc, processed=True, **kwargs)

        # self.p.vb.register(self.p.titleLabel.text)
        # self.proc.vb.register(self.proc.titleLabel.text)

    def _draw(self, **kwargs):

        # Prepare the viewbox
        plot = kwargs.get('plotitem', self.p)
        vb = plot.vb
        if self.dataset.ndim > 1:
            vb.sigColorMapChanged.connect(self.changeColorMap)
            vb.sigPlotModeChanged.connect(self.changePlotMode)
            vb.sigLineWidthChanged.connect(self.changeLineWidth)
        plot.clear()

        # Copy the dataset
        new = self.dataset.copy()
        processed = kwargs.get('processed', False)
        if processed:
            zdata = self.processeddata
        else:
            zdata = self.data

        # Get some preferences
        prefs = new.preferences
        lw = new.meta.get('linewidth', prefs.lines_linewidth)

        # Set axis
        # ========
        # Set the abscissa axis (x)
        # -------------------------

        # The actual dimension name is the last in the new.dims list
        dimx = new.dims[-1]

        # reduce data to the ROI
        x = getattr(new, dimx)
        lx, ux = x.roi
        if new.ndim > 1:
            new = new[:, lx:ux]
        else:
            new = new[lx:ux]

        # read again the x coordinate in case of ROI change
        x = getattr(new, dimx)
        if x is not None and x.implements('CoordSet'):
            # if several coords, take the default ones:
            x = x.default
        xsize = new.shape[-1]
        show_x_points = False
        if x is not None and hasattr(x, 'show_datapoints'):
            show_x_points = x.show_datapoints
        if show_x_points:
            # remove data and units for display
            x = scp.LinearCoord.arange(xsize)

        discrete_data = False
        if x is not None and (not x.is_empty or x.is_labeled):
            xdata = x.data
            if not np.any(xdata):
                if x.is_labeled:
                    discrete_data = True
                    # take into account the fact that sometimes axis have just labels
                    xdata = range(1, len(x.labels) + 1)
        else:
            xdata = range(xsize)

        xlim = [xdata[0], xdata[-1]]
        xlim.sort()

        vb.invertX(x.reversed)
        print(plot, x.reversed, xlim, x.limits, plot.getAxis('bottom').range)

        zoom_reset = kwargs.get('zoom_reset', False)
        if not zoom_reset:
            if sorted(plot.getAxis('bottom').range) != [
                    0, 1
            ] and x.title in plot.getAxis('bottom').labelText:
                range = plot.getAxis('bottom').range
                range = sorted(range, reverse=True)
                vb.setXRange(*range, padding=0)
                print('1 - setXrange (range)', range)
            else:
                vb.setXRange(*xlim, padding=0)
                print('2 - setXrange (xlim)', xlim)
        else:
            vb.setXRange(*xlim, padding=0)
            print('3 - setXrange (xlim)', xlim)

        ndim = new._squeeze_ndim
        if ndim > 1:

            # Set the ordinates axis (y)
            # --------------------------

            # The actual dimension name is the second in the new.dims list
            dimy = new.dims[-2]

            # Reduce to ROI
            y = getattr(new, dimy)
            ly, uy = y.roi
            new = new[ly:uy]

            y = getattr(new, dimy)
            if y is not None and y.implements('CoordSet'):
                # if several coords, take the default ones:
                y = y.default
            ysize = new.shape[-2]

            show_y_points = False
            if ysize > 1:

                # 2D (else it will be displayed as 1D)
                # ------------------------------------
                if y is not None and hasattr(y, 'show_datapoints'):
                    show_y_points = y.show_datapoints
                if show_y_points:
                    # remove data and units for display
                    y = scp.LinearCoord.arange(ysize)

                if y is not None and (not y.is_empty or y.is_labeled):
                    ydata = y.data

                    if not np.any(ydata):
                        if y.is_labeled:
                            ydata = range(1, len(y.labels) + 1)
                else:
                    ydata = range(ysize)

                yl = [ydata[0], ydata[-1]]
                yl.sort()

                ylim = list(kwargs.get("ylim", yl))
                ylim.sort()
                ylim[-1] = min(ylim[-1], yl[-1])
                ylim[0] = max(ylim[0], yl[0])

        # Amplitude (z)
        # -------------

        zlim = kwargs.get('zlim', (np.ma.min(zdata), np.ma.max(zdata)))

        method = new.meta.get('mode', prefs.method_2D) if ndim > 1 else 'stack'

        if method in ['stack']:  # For 2D and 1D plot

            # The z axis info
            # ---------------
            amp = 0
            zl = (np.min(np.ma.min(zdata) - amp),
                  np.max(np.ma.max(zdata)) + amp)
            zlim = list(kwargs.get('zlim', zl))
            zlim.sort()
            z_reverse = kwargs.get('z_reverse', False)
            vb.invertY(z_reverse)

            # Set the limits
            # ---------------

            # if yscale == "log" and min(zlim) <= 0:
            #    # set the limits wrt smallest and largest strictly positive values
            #    ax.set_ylim(10 ** (int(np.log10(np.amin(np.abs(zdata)))) - 1),
            #                10 ** (int(np.log10(np.amax(np.abs(zdata)))) + 1))

            vb.setYRange(*zlim, padding=0)

        else:

            #TODO
            pass  # not implemented

            # # the y axis info
            # # ----------------
            # # if data_only:
            # #    ylim = ax.get_ylim()
            #
            # ylim = list(kwargs.get('ylim', ylim))
            # ylim.sort()
            # y_reverse = kwargs.get('y_reverse', y.reversed if y else False)
            # if y_reverse:
            #     ylim.reverse()
            #
            # # set the limits
            # # ----------------
            # ax .set_ylim(ylim)

        # Log scale

        # yscale = kwargs.get("yscale", "linear")
        # ax.set_yscale(yscale)
        # xscale = kwargs.get("xscale", "linear")
        # ax.set_xscale(xscale)  # , nonpositive='mask')

        # Plot the dataset
        # ================

        #  ax.grid(prefs.axes_grid)  # TODO

        cmap = new.meta.get('colormap', prefs.colormap)
        self.cmap = pg.colormap.get(cmap, source='matplotlib', skipCache=True)

        if method in ['stack']:

            # if data.ndim == 1:
            #    data = data.at_least2d()

            ncurves = zdata.shape[0]
            colors = self.cmap.color
            if ncurves > 1:
                icolor = np.linspace(0, (colors.shape[0] - 1),
                                     ncurves).astype(int)
                colors = colors[icolor]
            else:
                colors = [colors[0]]  # [prefs('color')]
            self.colors = colors

            # self.curves = []
            if hasattr(zdata, 'mask'):
                mask = zdata.mask
                zdata[mask] = np.nan

            # Downsampling
            step = 1
            if ncurves > 250:
                step = int(ncurves / 250)

            for i in np.arange(0, ncurves, step):
                zdat = zdata[i:i + step].max(axis=0) if step > 1 else zdata[i]
                if np.alltrue(mask):
                    continue
                c = pg.PlotCurveItem(x=xdata,
                                     y=zdat,
                                     pen=mkPen(mkColor(colors[i]), width=lw),
                                     clickable=True,
                                     connect='finite')
                plot.addItem(c)
                c.sigClicked.connect(partial(self._curveSelected, plot))

        # Display a title
        title = kwargs.get('title', None)
        if title:
            plot.setTitle(title)
        elif kwargs.get('plottitle', False):
            plot.setTitle(new.name)

        # Labels
        # ======

        def make_label(ss, label):
            if ss.units is not None and str(ss.units) != 'dimensionless':
                units = r"{:~P}".format(ss.units)
            else:
                units = ''
            label = f"{label} / {units}"
            return label

        # --------------------------------------------------------------------------------------------------------------
        # x label
        # --------------------------------------------------------------------------------------------------------------

        xlabel = kwargs.get("xlabel", None)
        if show_x_points:
            xlabel = 'data points'
        if not xlabel:
            xlabel = make_label(x, x.title)

        plot.setLabel('bottom', text=xlabel)

        # uselabelx = kwargs.get('uselabel_x', False)
        # if x and x.is_labeled and (uselabelx or not np.any(x.data)) and len(x.labels) < number_x_labels + 1:
        #     # TODO refine this to use different orders of labels
        #     ax.set_xticks(xdata)
        #     ax.set_xticklabels(x.labels)
        #

        if ndim > 1:
            # y label
            # --------
            ylabel = kwargs.get("ylabel", None)
            if show_y_points:
                ylabel = 'data points'
            if not ylabel:
                if method in ['stack']:
                    ylabel = make_label(new, y.title)
                else:
                    ylabel = make_label(y, new.dims[-2])

            # uselabely = kwargs.get('uselabel_y', False)
            # if y and y.is_labeled and (uselabely or not np.any(y.data)) and len(y.labels) < number_y_labels:
            #             # TODO refine this to use different orders of labels
            #             ax.set_yticks(ydata)
            #             ax.set_yticklabels(y.labels)

        # --------------------------------------------------------------------------------------------------------------
        # z label
        # --------------------------------------------------------------------------------------------------------------

        zlabel = kwargs.get("zlabel", None)
        if not zlabel:
            if method in ['stack']:
                zlabel = make_label(new, new.title)
            elif method in ['surface']:
                zlabel = make_label(new, 'values')
            plot.setLabel('left', text=zlabel)
        else:
            zlabel = make_label(new, 'z')

        if method in ['stack']:
            # do we display the ordinate axis?
            if kwargs.get('show_y', True):
                plot.setLabel('left', text=zlabel)
            else:
                plot.hideAxis('left')

        if not hasattr(self, 'label'):
            self.label = pg.LabelItem(parent=self.p, justify='left')

        # --------------------------------------------------------------------------------------------------------------
        # Regions
        # --------------------------------------------------------------------------------------------------------------

        # Restore regions
        self.draw_regions()

        # --------------------------------------------------------------------------------------------------------------
        # Vertical line
        # --------------------------------------------------------------------------------------------------------------

        vLine = pg.InfiniteLine(angle=90, movable=False)
        # hLine = pg.InfiniteLine(angle=0, movable=False)
        plot.addItem(vLine, ignoreBounds=True)
        # plot.addItem(hLine, ignoreBounds=True)

        scene = plot.scene()

        def mouseMoved(evt):
            pos = evt
            scene.blockSignals(True)
            if plot.sceneBoundingRect().contains(pos):
                mouse_point = vb.mapSceneToView(pos)
                coord = mouse_point.x()
                ll, hl = vb.state['viewRange'][0]
                lld, hld = x.roi
                if max(ll, lld) <= coord <= min(hl, hld):
                    ds = new[:, float(coord)]
                    vLine.setVisible(True)
                    if self.selected:
                        try:
                            # corresponding x index
                            index = x.loc2index(coord)
                            z = self.selected.yData[index] * x.units
                            zstr = f'{z:~0.2fP} '

                        except Exception:
                            vLine.setVisible(False)
                            scene.blockSignals(False)
                            return  # out of limits
                    else:
                        z = ds.value
                        zstr = f'{z:~0.2fP} '
                    if z.size > 1:
                        # mode than one element (2D)
                        z = z.squeeze()
                        zstr = f'{z.min():~0.2fP} -- {z.max():~0.2fP} '

                    coord = coord * x.units
                    coordstr = f'{coord:~0.2fP}'
                    self.label.setText(
                        f"<span style='background-color:#FFF; font-size: 12pt'>"
                        f"<span style='color: blue'>{x.title} = {coordstr}</span>"
                        f"<br/>"
                        f"<span style='color: green'>{ds.title} = {zstr}</span>"
                        f"</span>")
                    vLine.setPos(mouse_point.x())
                    # hLine.setPos(mouse_point.y())
                else:
                    self.label.setText('')
                    vLine.setPos(0)
                    vLine.setVisible(False)
                    # hLine.setPos(0)
            scene.blockSignals(False)

        plot.scene().sigMouseMoved.connect(mouseMoved)

    # ..................................................................................................................
    def _findCurveIndex(self, plot, curve):

        for index, c in enumerate(plot.curves):
            if not isinstance(c, pg.PlotCurveItem):
                continue
            if c is curve:
                break

        return index

    # ..................................................................................................................
    def _curveSelected(self, plot, curve):
        # Action when a curve is selected

        lw = self.dataset.meta.get('linewidth',
                                   self.dataset.preferences.lines_linewidth)
        if self.dataset._squeeze_ndim < 2:
            return

        if self.selected is not None:
            # reset previous
            #index = self._findCurveIndex(plot, self.selected)
            pen = self.selected_pen
            self.selected.setPen(pen)

        if curve != self.selected:
            # set new selected
            self.selected_pen = curve.opts['pen']
            curve.setPen('k', width=lw * 3)
            self.selected = curve

        else:
            # index = self._findCurveIndex(plot, curve)
            curve.setPen(self.selected_pen)
            self.selected = None
 def sizeHint(self):
     return QtCore.QSize(20, 20)
 def setProperties(self):
     self.delay = QtCore.pyqtProperty(int, self.animationDelay, self.setAnimationDelay)
     self.displayedWhenStopped = QtCore.pyqtProperty(bool, self.isDisplayedWhenStopped, self.setDisplayedWhenStopped)
     self.color = QtCore.pyqtProperty(QtGui.QColor, self.getColor, self.setColor)
Exemplo n.º 9
0
class ProjectTreeWidget(QtGui.QTreeWidget):
    """
    Widget for displaying spectrochempy projects
    """
    # current project
    project = None
    # signals
    sigDatasetAdded = QtCore.Signal()
    sigDatasetRemoved = QtCore.Signal(object)
    sigDatasetSelected = QtCore.Signal(object)

    # ..................................................................................................................
    def __init__(self, parent=None, project=None, showHeader=True):
        """
        Parameters
        ----------
        parent : object
        project : SpectroChemPy Project object
        """
        QtGui.QTreeWidget.__init__(self)
        self.parent = parent
        self.setVerticalScrollMode(self.ScrollPerPixel)
        self.setColumnCount(3)
        self.setHeaderLabels(['name', 'type', 'id'])
        self.setHeaderHidden(not showHeader)
        self.setColumnHidden(1, True)
        self.setColumnHidden(2, True)
        self.sortByColumn(0, QtCore.Qt.AscendingOrder)
        self.setSortingEnabled(False)
        self.setProject(project)
        self.clicked.connect(self.emitSelectDataset)

    # ..................................................................................................................
    def setProject(self, project):
        """
        Parameters
        ----------
        project: SpectroChemPy project object
        """
        self.clear()
        self.project = project
        self.buildTree(project, self.invisibleRootItem())
        self.expandToDepth(3)
        self.resizeColumnToContents(0)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.showContextMenuEvent)

    # ..................................................................................................................
    def buildTree(self, obj, parent, name=''):
        if obj is None:
            node = QtGui.QTreeWidgetItem(['No project', ''])
            parent.addChild(node)
            return
        typeStr, id = obj.id.split('_')  # type(obj).__name__
        if typeStr == 'Project':
            name = obj.name
            id = ' '
        node = QtGui.QTreeWidgetItem([name, typeStr, id])
        parent.addChild(node)
        if typeStr == 'Project':
            for k in obj.allnames:
                self.buildTree(obj[k], node, k)
            node.setIcon(0, QtGui.QIcon(str(geticon('folder.png'))))
        elif typeStr == 'NDDataset':
            node.setFlags(
                QtCore.Qt.ItemIsEnabled |
                QtCore.Qt.ItemIsSelectable)  # QtCore.Qt.ItemIsUserCheckable |
            # node.setCheckState(1, QtCore.Qt.Unchecked)
            node.setIcon(0, QtGui.QIcon(str(geticon('file.png'))))
        else:
            return

    # ..................................................................................................................
    def showContextMenuEvent(self, event):
        self.contextMenu = QtGui.QMenu()
        # Infos about the selected node.
        index = self.indexAt(event)
        if not index.isValid():
            return
        item = self.itemAt(event)
        name = item.text(0)
        if item.text(1) == 'Project':
            # self.contextMenu.addAction('Rename').triggered.connect(partial(self.editname, item))
            self.contextMenu.addAction('Add new dataset').triggered.connect(
                self.emitAddDataset)
        if name != self.project.name:
            # can't remove the top element without cloing the project
            self.contextMenu.addAction('Remove').triggered.connect(
                partial(self.emitRemove, item))
        self.contextMenu.popup(self.mapToGlobal(event))

    # ..................................................................................................................
    def editname(self, *args):
        # TODO: editing name
        scp.debug_(args)

    def emitAddDataset(self):
        self.sigDatasetAdded.emit()

    def emitRemove(self, sel=None):
        if sel is None or sel.text(0) == self.project.name:
            scp.warning_('No item selected. Please select one to remove.')
            return
        name = sel.text(0)
        self.sigDatasetRemoved.emit(name)

    # ..................................................................................................................
    def emitSelectDataset(self, *args, **kwargs):
        """
        When an item is clicked in the project window, some actions can be
        performed, e.g., plot the corresponding data.

        """
        sel = self.currentItem()
        if sel:
            # make a plot of the data
            id = sel.text(2)
            name = sel.text(0)
            if sel.text(1) == "Project":
                if name == self.project.name:
                    return
                name = f'{name}/original'
            scp.debug_(f'---------------------- Dataset {name}({id}) selected')
            self.sigDatasetSelected.emit(name)
Exemplo n.º 10
0
    def __init__(self, show=True):

        super().__init__()

        pg.setConfigOption('background', 'w')
        pg.setConfigOption('foreground', 'k')

        siz = QtWidgets.QDesktopWidget().screenGeometry(-1)
        self.ww, self.wh = ww, wh = min(1500,
                                        siz.width() * .80), min(
                                            900,
                                            siz.height() * .80)
        self.move(QtCore.QPoint(10, 10))  # TODO: center it ?

        # --------------------------------------------------------------------------------------------------------------
        # Logging
        # --------------------------------------------------------------------------------------------------------------

        if not __DEV__:
            # production
            scp.app.log_level = logging.WARNING
        else:
            # development
            scp.app.log_level = logging.DEBUG

        # --------------------------------------------------------------------------------------------------------------
        # Main area
        # --------------------------------------------------------------------------------------------------------------

        self.area = area = LockedDockArea()
        self.setCentralWidget(area)
        self.setWindowIcon(QtGui.QIcon(str(geticon('scpy.png'))))
        self.setWindowTitle('SpectroChemPy GUI')

        # --------------------------------------------------------------------------------------------------------------
        # Create status bar
        # --------------------------------------------------------------------------------------------------------------

        self.statusbar = self.statusBar()
        self.statusbar.showMessage('Welcome to SpectroChemPy')

        # --------------------------------------------------------------------------------------------------------------
        # Create doc for plots
        # --------------------------------------------------------------------------------------------------------------

        # we need to create a dock object for this branch
        self.dplot = dplot = LockedDock('Plot', closable=False)
        dplot.hideTitleBar()

        # --------------------------------------------------------------------------------------------------------------
        # Create project window
        # --------------------------------------------------------------------------------------------------------------

        self.dproject = dproject = LockedDock("Project", size=(300, wh * .30))
        self.projectwidget = ProjectTreeWidget(showHeader=False, parent=self)

        self.project = Project(parent=self)

        dproject.addWidget(self.projectwidget)
        dproject.setMinimumWidth(300)
        dproject.setMaximumWidth(300)

        # --------------------------------------------------------------------------------------------------------------
        # Controller window
        # --------------------------------------------------------------------------------------------------------------

        dcontroller = LockedDock("Controller", size=(300, wh * .70))
        self.controller = Controller(parent=self)
        dcontroller.addWidget(self.controller)

        # --------------------------------------------------------------------------------------------------------------
        # set layout
        # --------------------------------------------------------------------------------------------------------------

        self.area.addDock(dproject, 'left')
        self.area.addDock(dplot, 'right')
        self.area.addDock(dcontroller, 'bottom', dproject)
        self.resize(ww, wh)

        # --------------------------------------------------------------------------------------------------------------
        # Create Menubar and actions
        # --------------------------------------------------------------------------------------------------------------

        # MENU FILE
        # -------------------------------------------------------------------

        project_menu = QtGui.QMenu('&Project', parent=self)
        self.menuBar().addMenu(project_menu)

        # Project menu
        # ----------------------------------------------------------------------------------------------------------

        self.new_action = QtGui.QAction('&New project', self)
        self.new_action.setShortcut(QtGui.QKeySequence.New)
        self.new_action.setStatusTip('Create a new main project')
        self.new_action.triggered.connect(
            partial(self.project.openProject, new=True))
        project_menu.addAction(self.new_action)

        self.open_action = QtGui.QAction('&Open project', self)
        self.open_action.setShortcut(QtGui.QKeySequence.Open)
        self.open_action.setStatusTip('Open a new main project')
        self.open_action.triggered.connect(
            partial(self.project.openProject, new=False))
        project_menu.addAction(self.open_action)

        # Dataset menu
        # --------------------------------------------------------------------------------------------------------------

        project_menu.addSeparator()

        self.add_dataset_action = QtGui.QAction('Add dataset', self)
        self.add_dataset_action.triggered.connect(self.project.addDataset)
        self.add_dataset_action.setShortcut(
            QtGui.QKeySequence('Ctrl+A', QtGui.QKeySequence.NativeText))
        self.add_dataset_action.setDisabled(True)
        project_menu.addAction(self.add_dataset_action)

        self.remove_dataset_action = QtGui.QAction('remove selected dataset',
                                                   self)
        self.remove_dataset_action.triggered.connect(
            self.project.removeDataset)
        self.remove_dataset_action.setShortcut(
            QtGui.QKeySequence('Ctrl+D', QtGui.QKeySequence.NativeText))
        self.remove_dataset_action.setDisabled(True)
        project_menu.addAction(self.remove_dataset_action)

        # Save project menu
        # ----------------------------------------------------------------------------------------------------------

        project_menu.addSeparator()

        self.save_action = QtGui.QAction('&Save project', self)
        self.save_action.setStatusTip('Save the entire project into a file')
        self.save_action.setShortcut(QtGui.QKeySequence.Save)
        self.save_action.triggered.connect(
            partial(self.project.saveProject, force=True))
        self.save_action.setDisabled(True)
        project_menu.addAction(self.save_action)

        self.save_as_action = QtGui.QAction('Save project as...', self)
        self.save_as_action.setStatusTip(
            'Save the entire project into a new file')
        self.save_as_action.setShortcut(QtGui.QKeySequence.SaveAs)
        self.save_as_action.triggered.connect(
            partial(self.project.saveProject, force=True, saveas=True))
        self.save_as_action.setDisabled(True)
        project_menu.addAction(self.save_as_action)

        # Close project menu
        # ----------------------------------------------------------------------------------------------------------

        project_menu.addSeparator()

        self.close_action = QtGui.QAction('Close project', self)
        self.close_action.setShortcut(
            QtGui.QKeySequence('Ctrl+Shift+W', QtGui.QKeySequence.NativeText))
        self.close_action.setStatusTip(
            'Close the main project and delete all data and plots out of '
            'memory')
        self.close_action.triggered.connect(self.project.closeProject)
        self.close_action.setDisabled(True)
        project_menu.addAction(self.close_action)

        # Quit
        # --------------------------------------------------------------------------------------------------------------

        if sys.platform != 'darwin':  # mac os makes this anyway
            quit_action = QtGui.QAction('Quit', self)
            quit_action.triggered.connect(
                QtCore.QCoreApplication.instance().quit)
            quit_action.setShortcut(QtGui.QKeySequence.Quit)
            project_menu.addAction(quit_action)

        self.menuBar().addMenu(project_menu)

        # Processing menu
        # ---------------
        proc_menu = QtGui.QMenu('Script', parent=self)
        self.menuBar().addMenu(proc_menu)

        # export script
        export_script_action = QtGui.QAction('Export script', self)
        export_script_action.triggered.connect(self.controller.exportScript)
        proc_menu.addAction(export_script_action)

        # import script
        import_script_action = QtGui.QAction('Import script', self)
        import_script_action.triggered.connect(self.controller.importScript)
        proc_menu.addAction(import_script_action)

        # Help menu
        # --------------------------------------------------------------------------------------------------------------

        help_menu = QtGui.QMenu('Help', parent=self)
        self.menuBar().addMenu(help_menu)

        # About

        about_action = QtGui.QAction('About', self)
        about_action.triggered.connect(self.onAbout)
        help_menu.addAction(about_action)

        # Preferences

        prefs_action = QtGui.QAction('Preferences', self)
        prefs_action.triggered.connect(lambda: self.onEditPreferences(True))
        prefs_action.setShortcut(QtGui.QKeySequence.Preferences)
        help_menu.addAction(prefs_action)

        # Documentation

        doc_action = QtGui.QAction('Documentationt', self)
        doc_action.triggered.connect(self.onDoc)
        help_menu.addAction(doc_action)

        # Console

        # console_action = QtGui.QAction('Console', self)
        # console_action.triggered.connect(self.show_console)
        # help_menu.addAction(console_action)

        if sys.platform == 'darwin':
            self.menuBar().setNativeMenuBar(
                False
            )  # this put the menu in the  #  window itself in OSX, as in windows.  # TODO: set this in
            # preferences

        self.preference_pages.extend([
            GeneralPreferencesWidget,
        ])

        # --------------------------------------------------------------------------------------------------------------
        # Signal connections
        # --------------------------------------------------------------------------------------------------------------

        # user requests
        self.projectwidget.sigDatasetSelected.connect(
            self.project.onSelectDataset)
        self.projectwidget.sigDatasetAdded.connect(self.project.addDataset)
        self.projectwidget.sigDatasetRemoved.connect(
            self.project.removeDataset)

        # model changes
        self.project.sigProjectChanged.connect(
            self.controller.onProjectChanged)
        self.project.sigDatasetChanged.connect(
            self.controller.onDatasetChanged)

        # --------------------------------------------------------------------------------------------------------------
        # Show window
        # --------------------------------------------------------------------------------------------------------------

        if show:
            self.show()
            self.controller.onProjectChanged('opened')
Exemplo n.º 11
0
class Regions(QtCore.QObject):
    """
    Class defining a set of regions on the plot
    """
    kind = 'undefined'
    regionItems = {}

    sigRegionAdded = QtCore.Signal(object, object)
    sigRegionRemoved = QtCore.Signal(object, object)
    sigRegionChanged = QtCore.Signal(object, object)

    # constant
    BRUSH = {'mask': (200, 200, 200, 60), 'baseline': (0, 200, 0, 60), 'integral': (0, 0, 200, 60), }

    # ..................................................................................................................
    def __init__(self, kind='undefined'):

        QtCore.QObject.__init__(self)

        self.kind = kind
        self.brushcolor = self.BRUSH.get(kind.lower(), (254, 0, 0, 60))

    # ..................................................................................................................
    def addRegion(self, param, kind='undefined', span=None, dim='x'):

        # If kind is undfined do nothing
        if kind == 'undefined':
            return

        # kind selected
        self.kind = kind

        # Update param info with provided span or default values
        if span is None and param.value() != 'undefined':
            span, dim = param.value().split('> ')
            span = eval(param.value())
        param.setValue(f'{dim}> {span[0]:.1f}, {span[1]:.1f}')

        # Define
        name = f'{dim}_{self.kind}_{param.name()}'
        region = pg.LinearRegionItem(values=span, brush=self.brushcolor)
        region._name = name
        region.setRegion(span)
        self.regionItems[name] = (region, param)

        # events
        region.sigRegionChangeFinished.connect(partial(self.regionChanged, param))
        param.sigRemoved.connect(self.regionRemoved)
        param.sigStateChanged.connect(self.change)

        # signal
        self.sigRegionAdded.emit(self, region)

    # ..................................................................................................................
    def getLinearRegions(self, kind, dim):

        regions={}
        for k, el in self.regionItems.items():
            if f"{dim}_{kind}_" in k:
                regions[k] = el
        return regions

    # ..................................................................................................................
    def findLinearRegion(self, name, kind, dim):

        name = f'{dim}_{kind}_{name}'
        try:
            return self.getLinearRegions(kind, dim)[name]
        except KeyError:
            return {}

    # ..................................................................................................................
    def regionChanged(self, param, reg):

        low, high = reg.getRegion()
        s = reg._name.split('_')
        dim = s[0]
        param.setValue(f'{dim}> {low:.1f}, {high:.1f}')

        self.sigRegionChanged.emit(self, reg)

    # ..................................................................................................................
    def regionRemoved(self, region):

        el, par = self.findLinearRegion(name=region.name(), kind=self.kind, dim='x')
        del self.regionItems[el._name]
        del el

        self.sigRegionRemoved.emit(self, region)

        # TODO: add a context menu to the LinearRegionItem  with remove entry

    # ..................................................................................................................
    def remove(self):

        for key in list(self.regionItems.keys()):
            region, param =  self.regionItems[key]
            del  self.regionItems[key]
            del region
            self.sigRegionRemoved.emit(self, param)

    # ..................................................................................................................
    def change(self, param, data, info):
        name = param.name()
        if name == 'kind' and data == 'value':
            self.kind = info
            self.brushcolor = self.BRUSH.get(self.kind.lower(), (254, 0, 0, 128))
Exemplo n.º 12
0
class Project(QtCore.QObject):
    """
    Class defining the project model.

    A project contains one or several Dataset objects.

    Parameters
    ----------
    parent: QObject
        A reference to the parent object (in principle it's the Mainwindow of the application).

    """
    sigProjectChanged = QtCore.Signal(object)
    sigSetDirty = QtCore.Signal()
    sigDatasetChanged = QtCore.Signal(object, object)

    _parent = None
    _project = None
    _dataset = None

    _dirty = False

    _directory = scp.preferences.project_directory

    # ..................................................................................................................
    def __init__(self, parent):
        QtCore.QObject.__init__(self)
        self._parent = parent

        # Autosave feature
        self.autosaveTimer = QtCore.QTimer()
        self.autosaveTimer.setInterval(30000)
        self.autosaveTimer.timeout.connect(self.saveProject)

        # Open last_project
        last_project = scp.preferences.last_project
        if last_project:
            last_project = Path(last_project).with_suffix(".pscp")
        if scp.preferences.autoload_project and last_project is not None and last_project.exists():
            scp.debug_(f'Open last project {last_project}')
            self.project = last_project
        else:
            self.openProject(new=True)

    # ..................................................................................................................
    def __call__(self):
        return self.project

    # ------------------------------------------------------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------------------------------------------------------

    @property
    def parent(self):
        return self._parent

    @property
    def project(self):
        return self._project

    @project.setter
    def project(self, fname):
        #self.parent.statusbar.showMessage("Setting a project ... ")
        scp.preferences.last_project = fname
        proj = self._loadProject(fname)
        if proj is not None:
            if not proj.directory:
                proj._directory = self._directory
        self._project = proj
        if scp.preferences.autosave_project:
            self.autosaveTimer.start()
        self.dirty = True
        self.sigProjectChanged.emit('opened')
        #self.parent.statusbar.showMessage("")

    @property
    def dataset(self):
        return self._dataset

    @dataset.setter
    def dataset(self, dataset):
        self._dataset = dataset

    @property
    def dirty(self):
        return self._dirty

    @dirty.setter
    def dirty(self, dirty):
        self._dirty = dirty
        self.sigSetDirty.emit()

    # ------------------------------------------------------------------------------------------------------------------
    # Project
    # ------------------------------------------------------------------------------------------------------------------

    # ..................................................................................................................
    def openProject(self, **kwargs):
        """

        Parameters
        ----------
        new: Bool
            If False, open an existing projetc, otherwise create a new one

        """
        if not kwargs.pop('new', False):
            # Open an existing project
            directory = self._directory
            fname = QtGui.QFileDialog.getOpenFileName(self.parent, 'Project file', str(directory),
                                                      'SpectroChemPy project files (*.pscp);;All files (*)')
            fname = fname[0]
            if not fname:
                return
        else:
            # New project
            fname = None
        self.project = fname

    # ..................................................................................................................
    def closeProject(self):
        # Save current project
        self.saveProject()
        self.project = None
        self.dataset = None
        # Stop autosave
        self.autosaveTimer.stop()
        # Signal
        self.sigProjectChanged.emit('closed')

    # ..................................................................................................................
    def saveProject(self, *args, **kwargs):
        if not kwargs.pop('force', False):
            if not self.dirty:
                return
        scp.debug_('Saving project')
        # we need to save only the original data as they will be recalculaded anyway when reloaded
        proj = self.project.copy()
        for name in proj.projects_names:
            for datasetname in proj[name].datasets_names:
                if 'original' not in datasetname:
                    proj[name].remove_dataset(datasetname)
                else:
                    proj[name][datasetname].processeddata = None
                    proj[name][datasetname].processedmask = False
                    transposed = self.project[name][datasetname].transposed # we take the flag on the self.project
                    # has it is not copied
                    if transposed:
                        proj[name][datasetname].transpose(inplace=True)

        if proj.directory is None:
            proj._directory = self._directory
        if kwargs.get('saveas') or proj.name == 'untitled':
            proj.save_as(self._directory / 'untitled.pscp', Qt_parent=self.parent)
            self.sigProjectChanged.emit('renamed')
        else:
            proj.save()
        scp.preferences.last_project = Path(proj.directory) / proj.name
        self.dirty = False

    # ------------------------------------------------------------------------------------------------------------------
    # Dataset
    # ------------------------------------------------------------------------------------------------------------------

    # ..................................................................................................................
    def addDataset(self, dataset=None):
        scp.debug_('Add a dataset')
        # Read the  dataset
        try:
            if not dataset:
                dataset = scp.read(Qt_parent=self.parent, default_filter='omnic')
            if dataset is None:  # still not determined.
                return
        except Exception as e:
            scp.error_(e)

        # Create a subproject with this dataset
        subproj = scp.Project()
        self.project.add_project(subproj, dataset.name)
        subproj.add_dataset(dataset, f'{dataset.name}/original')

        # Signal
        self.dirty = True
        self.sigProjectChanged.emit('dataset added')

    # ..................................................................................................................
    def removeDataset(self, name=None):
        # Confirm
        if '/original' in name:
            name = name.split('/')[0]
            if not confirm_msg(self.parent, 'Remove', f'Removing the main (original) datset will '
                                                      f'remove all other datasets present in `{name}`.\n'
                                                      f'Is-it what you want?'):
                return
        elif not confirm_msg(self.parent, 'Remove', f'Do you really want to remove the `{name}` dataset?'):
            return
        scp.debug_(f'remove: {name}')
        self.sigDatasetChanged.emit(None, 'deselect')
        if name in self.project.projects_names and self.project[name].implements('Project'):
            # its the whole subproject to be removed
            self.project.remove_project(name)
        else:
            try:
                subproject, name = name.split('/')
                subproject.remove_dataset(name)
            except:
                self.project.remove_dataset(name)

        self.dataset = None
        self.dirty = True
        # Signal
        self.sigProjectChanged.emit('dataset removed')

    # ..................................................................................................................
    def updateDataset(self, dataset):
        if 'untitled' not in dataset.name:
            # the parent subproject should be specified
            subproj = dataset.name.split('/')[0]
            if dataset.name in self.project[subproj].datasets_names:
                # In this case just update : but warning the dataset id must be the same the previous one.
                scp.debug_(f'Update dataset: {dataset.name}')
                id = self.project[subproj]._datasets[dataset.name].id
                dataset._id = id
                self.project[subproj]._datasets[dataset.name] = dataset
                if self.dataset.name == dataset.name:
                    self.sigDatasetChanged.emit(dataset, 'updated')
            else:
                scp.debug_(f'Add dataset {dataset.name} to project')
                self.project[subproj].add_dataset(dataset)
                self.sigProjectChanged.emit('dataset added')
                #self.sigDatasetChanged.emit(dataset, 'added')
            self.dirty = True

    # ..................................................................................................................
    def onSelectDataset(self, name):
        if self.dataset is not None and name == self.dataset.name:
            return
        self.dataset = self.project[name]
        # Signal
        self.sigDatasetChanged.emit(self.dataset, 'select')

    # ------------------------------------------------------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------------------------------------------------------

    # ..................................................................................................................
    def _loadProject(self, *args, **kwargs):
        """
        Load a project.
        """
        if len(args)<1:
            return
        fname = args[0]
        proj = None
        if fname is None or fname in ['', 'untitled']:
            # create a void project
            proj = scp.Project(name='untitled')
        else:
            try:
                proj = scp.Project.load(fname, **kwargs)
                proj.meta['project_file'] = fname

            except Exception as e:
                scp.error_(e)
                self.closeProject()
        return proj