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)
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
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))
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)
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