class InspectorWindow(QMainWindow): replotRequest = pyqtSignal(object) closed = pyqtSignal() def __init__(self, parent): self._updating = False self._data = None QMainWindow.__init__(self, parent) loadUi(self, 'inspector.ui') # layout = QVBoxLayout() # layout.setContentsMargins(0, 0, 0, 0) # layout.addWidget(self.canvas) self.sgroup = SettingGroup('inspector') self.tbl.verticalHeader().setDefaultSectionSize( self.tbl.verticalHeader().minimumSectionSize() + 2) with self.sgroup as settings: geometry = settings.value('geometry', QByteArray()) self.restoreGeometry(geometry) windowstate = settings.value('windowstate', QByteArray()) self.restoreState(windowstate) def setDataset(self, data): self.data = data self.dataName.setText('%s - %s' % (data.name, data.title)) self._updating = True self.tbl.setRowCount(len(data.meta)) for i, key in enumerate(sorted(data.meta, key=lambda n: n.lower())): key_item = QTableWidgetItem(key) key_item.setFlags(key_item.flags() & ~Qt.ItemIsEditable) self.tbl.setItem(i, 0, key_item) if key.startswith('col_'): value_item = QTableWidgetItem(str(data.meta[key])) value_item.setFlags(value_item.flags() & ~Qt.ItemIsEditable) else: value_item = QTableWidgetItem(srepr(data.meta[key])) self.tbl.setItem(i, 1, value_item) self._updating = False def closeEvent(self, event): with self.sgroup as settings: settings.setValue('geometry', self.saveGeometry()) settings.setValue('windowstate', self.saveState()) self.closed.emit() return QMainWindow.closeEvent(self, event) def on_tbl_itemChanged(self, item): if self._updating: return try: new_value = eval(str(item.text())) except Exception: QMessageBox.error(self, 'Error', 'The new value is not a valid expression.') return else: key = str(self.tbl.item(item.row(), 0).text()) self.data.meta[key] = new_value self.replotRequest.emit(None) session.set_dirty()
class QIPythonWidget(RichIPythonWidget): """Convenience class for a live IPython console widget.""" closeme = pyqtSignal() redrawme = pyqtSignal() def __init__(self, customBanner=None, *args, **kwargs): if customBanner is not None: self.banner = customBanner super(QIPythonWidget, self).__init__(*args, **kwargs) self.kernel_manager = kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel() kernel_manager.kernel.gui = 'qt4' self.kernel_client = kernel_client = self._kernel_manager.client() kernel_client.start_channels() def stop(): kernel_client.stop_channels() kernel_manager.shutdown_kernel() self.closeme.emit() self.exit_requested.connect(stop) def redraw(): self.redrawme.emit() self.executed.connect(redraw) def pushVariables(self, variableDict): """Given a dictionary containing name / value pairs, push those variables to the IPython console widget. """ self.kernel_manager.kernel.shell.push(variableDict) def clearTerminal(self): """Clears the terminal.""" self._control.clear() def printText(self, text): """Prints some plain text to the console.""" self._append_plain_text(text) def executeCommand(self, command): """Execute a command in the frame of the console widget.""" self._execute(command, True)
class ItemTreeView(QTreeView): newSelection = pyqtSignal() def __init__(self, parent): QTreeView.__init__(self, parent) self.header().hide() # self.setRootIsDecorated(False) # self.setStyleSheet("QTreeView::branch { display: none; }") self.setItemDelegate(ItemListDelegate(self)) self.setSelectionMode(QAbstractItemView.ExtendedSelection) def selectionChanged(self, selected, deselected): self.newSelection.emit() QTreeView.selectionChanged(self, selected, deselected)
class AnnotationWindow(QMainWindow): closed = pyqtSignal() def __init__(self, parent): self._updating = False self._data = None QMainWindow.__init__(self, parent) loadUi(self, 'annotations.ui') self.sgroup = SettingGroup('annotations') session.propsRequested.connect(self.on_session_propsRequested) session.propsUpdated.connect(self.on_session_propsUpdated) with self.sgroup as settings: geometry = settings.value('geometry', QByteArray()) self.restoreGeometry(geometry) windowstate = settings.value('windowstate', QByteArray()) self.restoreState(windowstate) self._editing = True self.on_session_propsUpdated() def on_textBox_textChanged(self): if self._editing: session.set_dirty() def on_session_propsRequested(self): session.props.annotations = self.textBox.toPlainText() def on_session_propsUpdated(self): if 'annotations' in session.props: self._editing = False self.textBox.setText(session.props.annotations) self._editing = True def closeEvent(self, event): self.on_session_propsRequested() with self.sgroup as settings: settings.setValue('geometry', self.saveGeometry()) settings.setValue('windowstate', self.saveState()) self.closed.emit() return QMainWindow.closeEvent(self, event)
class DataOps(QWidget): replotRequest = pyqtSignal(object) pickRequest = pyqtSignal(object) titleChanged = pyqtSignal() def __init__(self, parent): QWidget.__init__(self, parent) self.item = None self.data = None self.picking = None self.picked_points = [] loadUi(self, 'dataops.ui') self.pickedLbl.hide() def initialize(self, item): self.data = item.data self.model = item.model self.item = item if self.data.fitmin is not None: self.limitminEdit.setText('%.5g' % self.data.fitmin) if self.data.fitmax is not None: self.limitmaxEdit.setText('%.5g' % self.data.fitmax) self.monscaleEdit.setText(str(self.data.nscale)) self.titleEdit.setText(self.data.title) self.nameEdit.setText(self.data.name) self.fftNpointsEdit.setText(str(len(self.data.x) * 4)) def on_canvas_pick(self, event): if not hasattr(event, 'artist'): return if self.picking: xdata = event.artist.get_xdata()[event.ind] ydata = event.artist.get_ydata()[event.ind] self.picked_points.append(xdata) self.pickedLbl.setText('%d picked' % len(self.picked_points)) event.canvas.figure.gca().plot([xdata], [ydata], 'ow', ms=8, mec='blue') event.canvas.draw() @pyqtSlot() def on_badResetBtn_clicked(self): self.data.reset_mask() self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_badPointsBtn_clicked(self): if self.picking == 'bad': self.badPointsBtn.setText('Start') self.pickedLbl.hide() self.picking = None self.removeBadPoints(self.picked_points) elif not self.picking: self.badPointsBtn.setText('Click points on plot, then ' 'here to finish') self.pickRequest.emit(self) self.picking = 'bad' self.picked_points = [] self.pickedLbl.setText('0 picked') self.pickedLbl.show() @pyqtSlot() def on_limitsBtn_clicked(self): try: limitmin = float(self.limitminEdit.text()) except ValueError: limitmin = None try: limitmax = float(self.limitmaxEdit.text()) except ValueError: limitmax = None self.data.fitmin, self.data.fitmax = limitmin, limitmax self.replotRequest.emit(None) session.set_dirty() def removeBadPoints(self, points): """'Remove' bad data points (just mask them out).""" for point in points: self.data.mask[self.data.x == point] = False self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_rebinBtn_clicked(self): try: binsize = float(self.precisionEdit.text()) except ValueError: QMessageBox.warning(self, 'Error', 'Enter a valid precision.') return new_array, new_meta = rebin(self.data._data, binsize, self.data.meta) self.data.__init__(new_meta, new_array, self.data.xcol, self.data.ycol, self.data.ncol, self.data.nscale, name=self.data.name, sources=self.data.sources) self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_floatmergeBtn_clicked(self): try: binsize = float(self.precisionEdit.text()) except ValueError: QMessageBox.warning(self, 'Error', 'Enter a valid precision.') return new_array, new_meta = floatmerge(self.data._data, binsize, self.data.meta) self.data.__init__(new_meta, new_array, self.data.xcol, self.data.ycol, self.data.ncol, self.data.nscale, name=self.data.name, sources=self.data.sources) self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_cloneBtn_clicked(self): new_data = self.data.copy() new_model = self.model.copy() from ufit.gui.scanitem import ScanDataItem session.add_item(ScanDataItem(new_data, new_model), self.item.group) @pyqtSlot() def on_arbBtn_clicked(self): dlg = QDialog(self) loadUi(dlg, 'change.ui') with SettingGroup('main') as settings: dlg.xEdit.setText(settings.value('changex', 'x')) dlg.yEdit.setText(settings.value('changey', 'y')) dlg.dyEdit.setText(settings.value('changedy', 'dy')) if dlg.exec_() != QDialog.Accepted: return settings.setValue('changex', dlg.xEdit.text()) settings.setValue('changey', dlg.yEdit.text()) settings.setValue('changedy', dlg.dyEdit.text()) xfml = dlg.xEdit.text() yfml = dlg.yEdit.text() dyfml = dlg.dyEdit.text() new_x = [] new_y = [] new_dy = [] ns = {} for dpoint in zip(self.data.x, self.data.y_raw, self.data.dy_raw): ns.update(x=dpoint[0], y=dpoint[1], dy=dpoint[2]) new_x.append(eval(xfml, ns)) new_y.append(eval(yfml, ns)) new_dy.append(eval(dyfml, ns)) self.data.x[:] = new_x self.data.y_raw[:] = new_y self.data.dy_raw[:] = new_dy self.data.y = self.data.y_raw / self.data.norm self.data.dy = self.data.dy_raw / self.data.norm self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_mulBtn_clicked(self): try: const = float(self.scaleConstEdit.text()) except ValueError: return self.data.y *= const self.data.y_raw *= const self.data.dy *= const self.data.dy_raw *= const self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_addBtn_clicked(self): try: const = float(self.addConstEdit.text()) except ValueError: return self.data.y += const self.data.y_raw += const * self.data.norm self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_scaleXBtn_clicked(self): try: const = float(self.scaleXConstEdit.text()) except ValueError: return self.data.x *= const self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_shiftBtn_clicked(self): try: const = float(self.shiftConstEdit.text()) except ValueError: return self.data.x += const self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_monscaleBtn_clicked(self): try: const = int(self.monscaleEdit.text()) except ValueError: return self.data.rescale(const) self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_titleBtn_clicked(self): self.data.meta.title = str(self.titleEdit.text()) self.titleChanged.emit() session.set_dirty() self.replotRequest.emit(None) @pyqtSlot() def on_nameBtn_clicked(self): self.data.name = str(self.nameEdit.text()) session.set_dirty() self.replotRequest.emit(None) @pyqtSlot() def on_subtractBtn_clicked(self): from ufit.gui.scanitem import ScanDataItem dlg = QDialog(self) loadUi(dlg, 'subtract.ui') data2obj = dlg.setList.populate(ScanDataItem) if dlg.exec_() != QDialog.Accepted: return witems = dlg.setList.selectedItems() if not witems: return try: prec = float(dlg.precisionEdit.text()) except ValueError: QMessageBox.warning(self, 'Error', 'Please enter a valid precision.') return new_data = self.data.subtract(data2obj[witems[0].type()].data, prec, dlg.destructBox.isChecked()) if not dlg.destructBox.isChecked(): new_model = self.model.copy() from ufit.gui.scanitem import ScanDataItem session.add_item(ScanDataItem(new_data, new_model), self.item.group) else: self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_fftBtn_clicked(self): try: npoints = int(self.fftNpointsEdit.text()) except ValueError: QMessageBox.warning(self, 'Error', 'Please enter a valid number of points.') return xmin = self.data.x.min() xmax = self.data.x.max() xinterp = linspace(xmin, xmax, npoints) yinterp = interp1d(self.data.x, self.data.y, kind='linear') yfft = fft(yinterp(xinterp)) p2 = abs(yfft) / npoints p1 = p2[:npoints // 2 + 2] p1[1:-1] *= 2 dx = (xmax - xmin) / (npoints - 1) new_data = ScanData.from_arrays(name='FFT(' + self.data.name + ')', x=(1. / dx) * arange(npoints // 2 + 2) / npoints, y=p1, dy=0.01 * ones(p1.shape), xcol='1/' + self.data.xaxis, ycol='|P1|') new_model = self.model.copy() from ufit.gui.scanitem import ScanDataItem session.add_item(ScanDataItem(new_data, new_model), self.item.group)
class MPLCanvas(FigureCanvas): """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).""" logzChanged = pyqtSignal() replotRequest = pyqtSignal() def __init__(self, parent, width=10, height=6, dpi=72, maincanvas=False): fig = Figure(figsize=(width, height), dpi=dpi) fig.set_facecolor('white') self.print_width = 0 self.main = parent self.logz = False self.axes = fig.add_subplot(111) self.plotter = DataPlotter(self, self.axes) # make tight_layout do the right thing self.axes.set_xlabel('x') self.axes.set_ylabel('y') self.axes.set_title('(data title)\n(info)') FigureCanvas.__init__(self, fig) # create a figure manager so that we can use pylab commands on the # main viewport def make_active(event): Gcf.set_active(self.manager) self.manager = FigureManagerQT(self, 1) self.manager._cidgcf = self.mpl_connect('button_press_event', make_active) Gcf.set_active(self.manager) self.setParent(parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.updateGeometry() # actually get key events self.setFocusPolicy(Qt.StrongFocus) self.mpl_connect('key_press_event', self.key_press) # These will not do anything in standalone mode, but do not hurt. if maincanvas: session.propsRequested.connect(self.on_session_propsRequested) session.propsUpdated.connect(self.on_session_propsUpdated) def on_session_propsRequested(self): session.props.canvas_logz = self.logz def on_session_propsUpdated(self): if 'canvas_logz' in session.props: self.logz = session.props.canvas_logz self.logzChanged.emit() def key_press(self, event): if key_press_handler: key_press_handler(event, self) def resizeEvent(self, event): # reimplemented to add tight_layout() w = event.size().width() h = event.size().height() dpival = float(self.figure.dpi) winch = w / dpival hinch = h / dpival self.figure.set_size_inches(winch, hinch) try: self.figure.tight_layout(pad=2) except Exception: pass self.plotter.save_layout() self.draw() self.update() QWidget.resizeEvent(self, event) def print_(self): sio = BytesIO() self.print_figure(sio, format='svg') svg = QSvgRenderer(QByteArray(sio.getvalue())) sz = svg.defaultSize() aspect = sz.width() / float(sz.height()) printer = QPrinter(QPrinter.HighResolution) printer.setOrientation(QPrinter.Landscape) dlg = QDialog(self) loadUi(dlg, 'printpreview.ui') dlg.width.setValue(self.print_width or 500) ppw = QPrintPreviewWidget(printer, dlg) dlg.layout().insertWidget(1, ppw) def render(printer): height = printer.height() * (dlg.width.value() / 1000.) width = aspect * height painter = QPainter(printer) svg.render(painter, QRectF(0, 0, width, height)) def sliderchanged(newval): ppw.updatePreview() ppw.paintRequested.connect(render) dlg.width.valueChanged.connect(sliderchanged) if dlg.exec_() != QDialog.Accepted: return self.print_width = dlg.width.value() pdlg = QPrintDialog(printer, self) if pdlg.exec_() != QDialog.Accepted: return render(printer) def ufit_replot(self): self.replotRequest.emit()
class MPLToolbar(NavigationToolbar2QT): popoutRequested = pyqtSignal() icon_name_map = { 'home.png': 'magnifier-zoom-fit.png', 'back.png': 'arrow-180.png', 'forward.png': 'arrow.png', 'move.png': 'arrow-move.png', 'zoom_to_rect.png': 'selection-resize.png', 'filesave.png': 'document-pdf.png', 'printer.png': 'printer.png', 'pyconsole.png': 'terminal--arrow.png', 'log-x.png': 'log-x.png', 'log-y.png': 'log-y.png', 'log-z.png': 'log-z.png', 'exwindow.png': 'chart--arrow.png', } toolitems = list(NavigationToolbar2QT.toolitems) del toolitems[7] # subplot adjust toolitems.insert( 0, ('Log x', 'Logarithmic X scale', 'log-x', 'logx_callback')) toolitems.insert( 1, ('Log y', 'Logarithmic Y scale', 'log-y', 'logy_callback')) toolitems.insert( 2, ('Log z', 'Logarithmic Z scale for images', 'log-z', 'logz_callback')) toolitems.insert(3, (None, None, None, None)) toolitems.append( ('Print', 'Print the figure', 'printer', 'print_callback')) toolitems.append(('Pop out', 'Show the figure in a separate window', 'exwindow', 'popout_callback')) toolitems.append( ('Execute', 'Show Python console', 'pyconsole', 'exec_callback')) def _init_toolbar(self): NavigationToolbar2QT._init_toolbar(self) self.locLabel.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self._actions['logx_callback'].setCheckable(True) self._actions['logy_callback'].setCheckable(True) self._actions['logz_callback'].setCheckable(True) self.canvas.logzChanged.connect(self.on_canvas_logzChanged) def _icon(self, name, color=None): if name in self.icon_name_map: return QIcon(':/' + self.icon_name_map[name]) return QIcon() def home(self): # always unzoom completely if hasattr(self, '_views'): self._views.clear() if hasattr(self, '_positions'): self._positions.clear() self.canvas.figure.gca().autoscale() self.canvas.draw() return NavigationToolbar2QT.home(self) def logx_callback(self): ax = self.canvas.figure.gca() if ax.get_xscale() == 'linear': ax.set_xscale('symlog') self._actions['logx_callback'].setChecked(True) else: ax.set_xscale('linear') self._actions['logx_callback'].setChecked(False) self.canvas.draw() def logy_callback(self): ax = self.canvas.figure.gca() if ax.get_yscale() == 'linear': ax.set_yscale('symlog') self._actions['logy_callback'].setChecked(True) else: ax.set_yscale('linear') self._actions['logy_callback'].setChecked(False) self.canvas.draw() def logz_callback(self): ax = self.canvas.figure.gca() self.canvas.logz = not self.canvas.logz session.set_dirty() self._actions['logz_callback'].setChecked(self.canvas.logz) for im in ax.get_images(): if self.canvas.logz: im.set_norm(LogNorm()) else: im.set_norm(None) self.canvas.draw() def on_canvas_logzChanged(self): self._actions['logz_callback'].setChecked(self.canvas.logz) def print_callback(self): self.canvas.print_() def popout_callback(self): self.popoutRequested.emit() def exec_callback(self): try: from ufit.gui.console import ConsoleWindow except ImportError: logger.exception('Qt console window cannot be opened without ' 'IPython; import error was:') QMessageBox.information( self, 'ufit', 'Please install IPython with qtconsole to ' 'activate this function.') return w = ConsoleWindow(self) w.ipython.executeCommand('from ufit.lab import *') w.ipython.pushVariables({ 'fig': self.canvas.figure, 'ax': self.canvas.figure.gca(), 'D': [item for group in session.groups for item in group.items], }) w.show() def save_figure(self, *args): filetypes = self.canvas.get_supported_filetypes_grouped() sorted_filetypes = sorted(filetypes.items()) start = self.canvas.get_default_filename() filters = [] for name, exts in sorted_filetypes: if 'eps' in exts or 'emf' in exts or 'jpg' in exts or \ 'pgf' in exts or 'raw' in exts: continue exts_list = " ".join(['*.%s' % ext for ext in exts]) filter = '%s (%s)' % (name, exts_list) filters.append(filter) filters = ';;'.join(filters) fname, _ = QFileDialog.getSaveFileName(self, 'Choose a filename to save to', start, filters) if fname: try: self.canvas.print_figure(text_type(fname)) except Exception as e: logger.exception('Error saving file') QMessageBox.critical(self, 'Error saving file', str(e))
class ScanDataItem(SessionItem): newModel = pyqtSignal(object, bool) itemtype = 'scan' def __init__(self, data, model=None): self.data = data self.model = model or default_model(data) SessionItem.__init__(self) def change_model(self, model, keep_param_values=True): self.model = model self.newModel.emit(model, keep_param_values) session.set_dirty() def after_load(self): self.data.after_load() # upgrade datastructures def __reduce__(self): return (self.__class__, (self.data, self.model)) def create_panel(self, mainwindow, canvas): return ScanDataPanel(mainwindow, canvas, self) def create_multi_panel(self, mainwindow, canvas): return MultiDataOps(mainwindow, canvas) def update_htmldesc(self): title = self.data.title # XXX self.dataops.titleEdit.setText(title) self.title = title htmldesc = '<big><b>%s</b></big>' % self.index + \ (title and ' - %s' % title or '') + \ (self.data.environment and '<br>%s' % ', '.join(self.data.environment) or '') + \ ('<br><small>%s</small>' % '<br>'.join(self.data.sources[:5])) if len(self.data.sources) > 5: htmldesc += '<br><small>...</small>' self.htmldesc = htmldesc session.itemsUpdated.emit() def export_python(self, filename): with open(filename, 'wb') as fp: fp.write(b'from ufit.lab import *\n') fp.write(b'\n') self.data.export_python(fp, 'data') fp.write('\n') self.model.export_python(fp, 'model') fp.write(b'''\ ## just plot current values data.plot() model.plot_components(data) model.plot(data) ## to fit again use this... #result = model.fit(data) #result.printout() #result.plot() show() ''') def export_ascii(self, filename): with open(filename, 'wb') as fp: self.data.export_ascii(fp) def export_fits(self, filename): xx = linspace(self.data.x.min(), self.data.x.max(), 1000) paramvalues = prepare_params(self.model.params, self.data.meta)[3] yy = self.model.fcn(paramvalues, xx) yys = [] for comp in self.model.get_components(): if comp is self.model: continue yys.append(comp.fcn(paramvalues, xx)) savetxt(filename, array([xx, yy] + yys).T)
class MultiDataOps(QWidget): replotRequest = pyqtSignal(object) def __init__(self, parent, canvas): QWidget.__init__(self, parent) self.canvas = canvas self.replotRequest.connect(self.plot) loadUi(self, 'multiops.ui') def initialize(self, items): self.items = [i for i in items if isinstance(i, ScanDataItem)] self.datas = [i.data for i in self.items] self.monscaleEdit.setText( str(int(mean([d.nscale for d in self.datas])))) self.onemodelBox.clear() self.onemodelBox.addItems(['%d' % i.index for i in self.items]) def plot(self, limits=True, canvas=None): canvas = canvas or self.canvas xlabels = set() ylabels = set() titles = set() canvas.plotter.reset() for i in self.items: c = canvas.plotter.plot_data(i.data, multi=True) canvas.plotter.plot_model(i.model, i.data, labels=False, color=c) xlabels.add(i.data.xaxis) ylabels.add(i.data.yaxis) titles.add(i.data.title) canvas.plotter.plot_finish( ', '.join(xlabels), ', '.join(ylabels), from_encoding(', '.join(titles), 'ascii', 'ignore')) canvas.draw() @pyqtSlot() def on_rebinBtn_clicked(self): try: binsize = float(self.precisionEdit.text()) except ValueError: QMessageBox.warning(self, 'Error', 'Enter a valid precision.') return for data in self.datas: new_array, new_meta = rebin(array(data._data), binsize, data.meta) data.__init__(new_meta, new_array, data.xcol, data.ycol, data.ncol, data.nscale, name=data.name, sources=data.sources) self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_mulBtn_clicked(self): try: const = float(self.scaleConstEdit.text()) except ValueError: return for data in self.datas: data.y *= const data.y_raw *= const data.dy *= const data.dy_raw *= const self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_addBtn_clicked(self): try: const = float(self.addConstEdit.text()) except ValueError: return for data in self.datas: data.y += const data.y_raw += const * data.norm self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_scaleXBtn_clicked(self): try: const = float(self.scaleXConstEdit.text()) except ValueError: return for data in self.datas: data.x *= const self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_shiftBtn_clicked(self): try: const = float(self.shiftConstEdit.text()) except ValueError: return for data in self.datas: data.x += const self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_monscaleBtn_clicked(self): try: const = int(self.monscaleEdit.text()) except ValueError: return for data in self.datas: data.nscale = const data.norm = data.norm_raw / const data.y = data.y_raw / data.norm data.dy = sqrt(data.y_raw) / data.norm data.yaxis = data.ycol + ' / %s %s' % (const, data.ncol) self.replotRequest.emit(None) session.set_dirty() @pyqtSlot() def on_mergeBtn_clicked(self): try: precision = float(self.mergeEdit.text()) except ValueError: QMessageBox.warning(self, 'Error', 'Enter a valid precision.') return new_data = self.datas[0].merge(precision, *self.datas[1:]) session.add_item(ScanDataItem(new_data), self.items[-1].group) @pyqtSlot() def on_floatMergeBtn_clicked(self): try: precision = float(self.mergeEdit.text()) except ValueError: QMessageBox.warning(self, 'Error', 'Enter a valid precision.') return new_data = self.datas[0].merge(precision, floatmerge=True, *self.datas[1:]) session.add_item(ScanDataItem(new_data), self.items[-1].group) @pyqtSlot() def on_onemodelBtn_clicked(self): which = self.onemodelBox.currentIndex() if which < 0: return model = self.items[which].model with_params = self.onemodelWithParamsBox.isChecked() for i, item in enumerate(self.items): if i == which: continue item.change_model(model.copy(), keep_param_values=not with_params) self.replotRequest.emit(None) @pyqtSlot() def on_fitallBtn_clicked(self): for item in self.items: res = item.model.fit(item.data) session.modelFitted.emit(item, res) self.replotRequest.emit(None) @pyqtSlot() def on_paramsetBtn_clicked(self): dlg = ParamSetDialog(self, self.items) if dlg.exec_() != QDialog.Accepted: return session.add_item(ScanDataItem(dlg.new_data), self.items[-1].group) @pyqtSlot() def on_mappingBtn_clicked(self): item = MappingItem([item.data for item in self.items], None) session.add_item(item, self.items[-1].group) @pyqtSlot() def on_globalfitBtn_clicked(self): QMessageBox.warning(self, 'Sorry', 'Not implemented yet.') def export_ascii(self, filename): base, ext = path.splitext(filename) for i, item in enumerate(self.items): item.export_ascii(base + '.%d' % i + ext) def export_fits(self, filename): base, ext = path.splitext(filename) for i, item in enumerate(self.items): item.export_fits(base + '.%d' % i + ext) def export_python(self, filename): base, ext = path.splitext(filename) for i, item in enumerate(self.items): item.export_python(base + '.%d' % i + ext)
class Fitter(QWidget): closeRequest = pyqtSignal() pickRequest = pyqtSignal(object) replotRequest = pyqtSignal(object) def __init__(self, parent, standalone=False, fit_kws={}): QWidget.__init__(self, parent) self.item = getattr(parent, 'item', None) self.logger = logger.getChild('fitter') self.picking = None self.last_result = None self.model = None self.data = None self.param_controls = {} self.fit_kws = fit_kws self.standalone = standalone self.createUI(standalone) def createUI(self, standalone): loadUi(self, 'fitter.ui') if standalone: self.buttonBox.addButton(QDialogButtonBox.Close) self.buttonBox.addButton('Initial guess', QDialogButtonBox.HelpRole) self.buttonBox.addButton('Save params', QDialogButtonBox.ResetRole) self.buttonBox.addButton('Restore saved', QDialogButtonBox.ResetRole) self.buttonBox.addButton('Replot', QDialogButtonBox.ActionRole) fitbtn = self.buttonBox.addButton('Fit', QDialogButtonBox.ApplyRole) fitbtn.setShortcut(QKeySequence('Ctrl+F')) fitbtn.setIcon(QIcon.fromTheme('dialog-ok')) def initialize(self, model, data, fit=True, keep_old=True): self.picking = None self.last_result = None old_model = self.model self.modelLabel.setText(model.get_description()) self.data = data self.model = model self.param_controls = {} self.create_param_controls() # try to transfer values of old parameters to new if keep_old and old_model is not None: oldp_dict = dict((p.name, p) for p in old_model.params) self.restore_from_params(oldp_dict) session.modelFitted.connect(self.on_modelFitted) if self.standalone: if fit: self.do_fit() else: self.replotRequest.emit(None) def create_param_controls(self): self.param_controls = {} self.param_frame = QFrame(self) layout = QGridLayout() for j, text in enumerate(('Param', 'Value', 'Error', 'Fix', 'Expr', 'Min', 'Max', 'Delta')): ctl = QLabel(text, self) ctl.setFont(self.statusLabel.font()) layout.addWidget(ctl, 0, j) i = 1 self.original_params = {} combo_items = [''] + [par.name for par in self.model.params] + \ ['data.' + m for m in sorted(self.data.meta) if isinstance(self.data.meta[m], number_types)] for p in self.model.params: e0 = QLabel(p.name, self) e1 = SmallLineEdit('%.5g' % p.value, self) e2 = QLabel(u'± %.5g' % p.error, self) e3 = QCheckBox(self) e4 = QComboBox(self) e4.setEditable(True) e4.addItems(combo_items) if p.expr and is_float(p.expr): e1.setText(p.expr) e3.setChecked(True) e4.lineEdit().setText('') else: e4.lineEdit().setText(p.expr or '') e5 = SmallLineEdit(p.pmin is not None and '%.5g' % p.pmin or '', self) e6 = SmallLineEdit(p.pmax is not None and '%.5g' % p.pmax or '', self) e7 = SmallLineEdit(p.delta and '%.5g' % p.delta or '', self) ctls = self.param_controls[p] = (e0, e1, e2, e3, e4, e5, e6, e7) for j, ctl in enumerate(ctls): layout.addWidget(ctl, i, j) i += 1 self.original_params[p.name] = p.copy() e1.returnPressed.connect(self.do_plot) e4.lineEdit().returnPressed.connect(self.do_plot) e5.returnPressed.connect(self.do_plot) e6.returnPressed.connect(self.do_plot) e3.clicked.connect(self.update_enables) e4.editTextChanged.connect(self.update_enables) layout.setRowStretch(i+1, 1) self.param_frame.setLayout(layout) self.param_scroll.setWidget(self.param_frame) self.update_enables() def update_enables(self, *ignored): for p, ctls in iteritems(self.param_controls): # if there is an expr... if ctls[4].currentText(): # disable value and minmax, check "fixed" and disable "fixed" ctls[1].setEnabled(False) ctls[3].setCheckState(Qt.PartiallyChecked) # implicitly fixed ctls[3].setEnabled(False) ctls[5].setEnabled(False) ctls[6].setEnabled(False) ctls[7].setEnabled(False) # else, if "fixed" is checked... elif ctls[3].checkState() == Qt.Checked: # enable value, but disable expr and minmax ctls[1].setEnabled(True) ctls[4].setEnabled(False) ctls[5].setEnabled(False) ctls[6].setEnabled(False) ctls[7].setEnabled(False) # else: not fixed, no expr else: # enable everything ctls[1].setEnabled(True) ctls[3].setEnabled(True) ctls[3].setCheckState(Qt.Unchecked) ctls[3].setTristate(False) ctls[4].setEnabled(True) ctls[5].setEnabled(True) ctls[6].setEnabled(True) ctls[7].setEnabled(True) def on_buttonBox_clicked(self, button): role = self.buttonBox.buttonRole(button) if role == QDialogButtonBox.RejectRole: self.closeRequest.emit() elif role == QDialogButtonBox.ApplyRole: self.do_fit() elif role == QDialogButtonBox.ActionRole: self.do_plot() elif role == QDialogButtonBox.HelpRole: self.do_pick() else: if button.text() == 'Save params': self.save_original_params() self.statusLabel.setText('Current parameter values saved.') else: self.restore_from_params(self.original_params) self.statusLabel.setText('Saved parameter values restored.') def update_from_controls(self): for p, ctls in iteritems(self.param_controls): _, val, _, fx, expr, pmin, pmax, delta = ctls p.value = float(val.text()) if val.text() else 0 if fx.checkState() == Qt.Checked: p.expr = str(val.text()) else: p.expr = str(expr.currentText()) p.pmin = float(pmin.text()) if pmin.text() else None p.pmax = float(pmax.text()) if pmax.text() else None p.delta = float(delta.text()) if delta.text() else 0 self.update_enables() session.set_dirty() def restore_from_params(self, other_params): for p in self.model.params: if p.name not in other_params: continue p0 = other_params[p.name] ctls = self.param_controls[p] ctls[1].setText('%.5g' % p0.value) ctls[2].setText(u'± %.5g' % p0.error) ctls[3].setChecked(False) if p0.expr and is_float(p0.expr): ctls[1].setText(p0.expr) ctls[3].setChecked(True) ctls[4].lineEdit().setText('') else: ctls[4].lineEdit().setText(p0.expr or '') ctls[5].setText(p0.pmin is not None and '%.5g' % p0.pmin or '') ctls[6].setText(p0.pmax is not None and '%.5g' % p0.pmax or '') ctls[7].setText(p0.delta and '%.5g' % p0.delta or '') session.set_dirty() self.do_plot() def save_original_params(self): self.original_params = {} for p in self.model.params: self.original_params[p.name] = p.copy() def on_canvas_pick(self, event): if not self.picking: return if not hasattr(event, 'xdata') or event.xdata is None: return self._pick_values.append((event.xdata, event.ydata)) if len(self._pick_values) == len(self._pick_points): self.picking = False self.statusLabel.setText('') self._pick_finished() else: self.statusLabel.setText('%s: click on %s' % (self.picking, self._pick_points[len(self._pick_values)])) def do_pick(self, *args): if self.picking: return self.pickRequest.emit(self) self._pick_points = self.model.get_pick_points() self._pick_values = [] self.picking = 'Guess' self.statusLabel.setText('Guess: click on %s' % self._pick_points[0]) def callback(): self.model.apply_pick(self._pick_values) for p in self.model.params: ctls = self.param_controls[p] if not p.expr: ctls[1].setText('%.5g' % p.value) session.set_dirty() self.do_plot() self._pick_finished = callback def do_plot(self, *ignored): self.update_from_controls() self.replotRequest.emit(None) def do_fit(self): if self.picking: QMessageBox.information(self, 'Fitting', 'Please finish the picking operation first.') return self.update_from_controls() self.statusLabel.setText('Working...') self.statusLabel.repaint() QApplication.processEvents() try: res = self.model.fit(self.data, **self.fit_kws) except Exception as e: self.logger.exception('Error during fit') self.statusLabel.setText('Error during fit: %s' % e) return self.on_modelFitted(self.item, res) self.replotRequest.emit(True) session.set_dirty() def on_modelFitted(self, item, res): if item is not self.item: return self.statusLabel.setText( (res.success and 'Converged. ' or 'Failed. ') + res.message + ' Reduced chi^2 = %.3g.' % res.chisqr) for p in res.params: self.param_controls[p][1].setText('%.5g' % p.value) self.param_controls[p][2].setText(u'± %.5g' % p.error) self.last_result = res
class ModelBuilder(QWidget): closeRequest = pyqtSignal() pickRequest = pyqtSignal(object) newModel = pyqtSignal(object, object, object) def __init__(self, parent): QWidget.__init__(self, parent) self.logger = logger.getChild('model') self.gauss_picking = 0 self.gauss_peak_pos = 0, 0 self.pick_model = None self.data = None self.last_model = None self.createUI() def createUI(self): loadUi(self, 'modelbuilder.ui') self.modeldefStacker.setCurrentIndex(0) self.model_dict = {} for model in concrete_models: QListWidgetItem(model.__name__, self.premodelsList) self.model_dict[model.__name__] = model self.buttonBox.addButton('Check', QDialogButtonBox.NoRole) self.buttonBox.addButton(QDialogButtonBox.Apply) def on_buttonBox_clicked(self, button): role = self.buttonBox.buttonRole(button) if role == QDialogButtonBox.ResetRole: self.modeldefEdit.setText('') elif role == QDialogButtonBox.NoRole: self.eval_model() else: # "apply" self.eval_model(final=True) @pyqtSlot() def on_gaussOnlyBtn_clicked(self): if self.gauss_picking: self._finish_picking() return self.gaussOnlyBtn.setText('Back to full modeling mode') self.pickRequest.emit(self) self.gauss_picking = 1 self.gauss_picked_points = [] self.modeldefStacker.setCurrentIndex(1) self.pick_model = Background(bkgd=self.data.y.min()) self.newModel.emit(self.pick_model, True, False) def on_canvas_pick(self, event): if not self.gauss_picking: return if hasattr(event, 'artist'): return if self.gauss_picking % 2 == 1: # first click, picked position self.gauss_peak_pos = event.xdata, event.ydata else: # second click, picked width pos = self.gauss_peak_pos[0] ampl = self.gauss_peak_pos[1] - self.data.y.min() fwhm = abs(pos - event.xdata) * 2 self.pick_model += GaussInt('p%02d' % (self.gauss_picking / 2), pos=pos, int=fwhm * ampl * 2.5, fwhm=fwhm) self.newModel.emit(self.pick_model, True, False) self.gauss_picking += 1 def _finish_picking(self): if not self.gauss_picking: return self.gauss_picking = None self.gaussOnlyBtn.setText('Gauss peaks only mode') self.modeldefStacker.setCurrentIndex(0) def on_premodelsList_currentItemChanged(self, current, previous): model = self.model_dict[str(current.text())] self.modelinfoLbl.setText(model.__doc__) def on_premodelsList_itemDoubleClicked(self, item): self.on_addmodelBtn_clicked() @pyqtSlot() def on_addmodelBtn_clicked(self): modelitem = self.premodelsList.currentItem() if not modelitem: return modelcls = str(modelitem.text()) modelname = QInputDialog.getText( self, 'ufit', 'Please enter a name ' 'for the model part:')[0] if not modelname: return self.insert_model_code('%s(%r)' % (modelcls, str(modelname))) @pyqtSlot() def on_addCustomBtn_clicked(self): dlg = QDialog(self) loadUi(dlg, 'custommodel.ui') while 1: if dlg.exec_() != QDialog.Accepted: return modelname = str(dlg.nameBox.text()) params = str(dlg.paramBox.text()) value = str(dlg.valueEdit.toPlainText()).strip() if not ident_re.match(modelname): QMessageBox.warning( self, 'Error', 'Please enter a valid model ' 'name (must be a Python identifier using ' 'only alphabetic characters and digits).') continue if not params: QMessageBox.warning(self, 'Error', 'Please enter some parameters.') continue for param in params.split(): if not ident_re.match(param): QMessageBox.warning( self, 'Error', 'Parameter name %s is not valid (must ' 'be a Python identifier using only alphabetic ' 'characters and digits).' % param) params = None break if not params: continue break self.insert_model_code('Custom(%r, %r, %r)' % (modelname, params, value)) def insert_model_code(self, code): currentmodel = str(self.modeldefEdit.toPlainText()) prefix = '' if currentmodel: prefix = ' + ' tc = self.modeldefEdit.textCursor() tc.movePosition(QTextCursor.End) tc.insertText(prefix + code) def initialize(self, data, model): self.model = model self.data = data self.modeldefEdit.setText(model.get_description()) def eval_model(self, final=False): modeldef = str(self.modeldefEdit.toPlainText()).replace('\n', ' ') if not modeldef: QMessageBox.information(self, 'Error', 'No model defined.') return try: model = eval_model(modeldef) except Exception as e: self.logger.exception('Could not evaluate model') QMessageBox.information(self, 'Error', 'Could not evaluate model: %s' % e) return if final: self._finish_picking() self.last_model = model self.newModel.emit(model, False, True) self.closeRequest.emit() else: self.statusLbl.setText('Model definition is good.')
class DataLoader(QWidget): closeRequest = pyqtSignal() newDatas = pyqtSignal(object, object) def __init__(self, parent, plotter, standalone=False): QWidget.__init__(self, parent) self.logger = logger.getChild('loader') self.plotter = plotter self.last_data = [] self.loader = Loader() self.createUI(standalone) self.sgroup = SettingGroup('main') # These will not do anything in standalone mode, but do not hurt. session.propsRequested.connect(self.on_session_propsRequested) session.propsUpdated.connect(self.on_session_propsUpdated) session.itemsUpdated.connect(self.on_session_itemsUpdated) session.groupAdded.connect(self.on_session_itemsUpdated) with self.sgroup as settings: data_template_path = settings.value('last_data_template', '') if data_template_path: self.templateEdit.setText(data_template_path) self.set_template(data_template_path, 0, silent=True) def createUI(self, standalone): loadUi(self, 'dataloader.ui') self.dataformatBox.addItem('auto') for fmt in sorted(data_formats): self.dataformatBox.addItem(fmt) self.buttonBox.addButton(QDialogButtonBox.Open) self.buttonBox.addButton('Preview', QDialogButtonBox.NoRole) def on_session_propsRequested(self): session.props.template = self.templateEdit.text() def on_session_propsUpdated(self): if 'template' in session.props: self.templateEdit.setText(session.props.template) def on_session_itemsUpdated(self, _ignored=None): # list of groups may have changed self.groupBox.clear() for group in session.groups: self.groupBox.addItem(group.name) def on_buttonBox_clicked(self, button): role = self.buttonBox.buttonRole(button) if role == QDialogButtonBox.RejectRole: self.closeRequest.emit() elif role == QDialogButtonBox.NoRole: # "preview" self.open_data() else: # "open" self.open_data(final=True) def on_dataformatBox_currentIndexChanged(self, i): self.loader.format = str(self.dataformatBox.currentText()) @pyqtSlot() def on_numorHelpBtn_clicked(self): QMessageBox.information( self, 'Numor Help', '''\ The numor string contains file numbers, with the following operators: , loads multiple files - loads multiple sequential files + merges multiple files > merges multiple sequential files For example: * 10-15,23 loads files 10 through 15 and 23 in 7 separate datasets. * 10+11,23+24 loads two datasets consisting of files 10 and 11 merged \ into one set, as well as files 23 and 24. * 10>15+23 merges files 10 through 15 and 23 into one single dataset. * 10,11,12+13,14 loads four sets. ''') def open_browser(self, directory): bwin = BrowseWindow(self) bwin.show() QApplication.processEvents() try: bwin.set_directory(directory) except OSError: pass bwin.activateWindow() def add_numors(self, numors): ranges = [] prev = -1 start = None last = None for num in numors: if last is not None: if num != last + 1: ranges.append((start, last)) start = num else: start = num last = num ranges.append((start, last)) s = ''.join(',%s' % ('%s' % s if s == e else '%s-%s' % (s, e)) for (s, e) in ranges) prev = self.numorsEdit.text() if prev: self.numorsEdit.setText(prev + s) else: self.numorsEdit.setText(s[1:]) self.open_data() # preview @pyqtSlot() def on_browseBtn_clicked(self): templ = path_to_str(self.templateEdit.text()) self.open_browser(path.dirname(templ)) @pyqtSlot() def on_settemplateBtn_clicked(self): previous = self.templateEdit.text() if previous: startdir = path.dirname(previous) else: startdir = '.' fn = path_to_str( QFileDialog.getOpenFileName(self, 'Choose a file', startdir, 'All files (*)')[0]) if not fn: return dtempl, numor = extract_template(fn) self.set_template(dtempl, numor) def set_template(self, dtempl, numor, silent=True): self.templateEdit.setText(str_to_path(dtempl)) with self.sgroup as settings: settings.setValue('last_data_template', dtempl) self.loader.template = dtempl try: cols, xguess, yguess, dyguess, mguess, nmon = \ self.loader.guess_cols(numor) except Exception as e: if not silent: self.logger.exception('Could not read column names') QMessageBox.information(self, 'Error', 'Could not read column names: %s' % e) return self.xcolBox.clear() self.xcolBox.addItem('auto') self.xcolBox.setCurrentIndex(0) self.ycolBox.clear() self.ycolBox.addItem('auto') self.ycolBox.setCurrentIndex(0) self.dycolBox.clear() self.dycolBox.addItem('auto') self.dycolBox.addItem('sqrt(Y)') self.dycolBox.setCurrentIndex(0) self.moncolBox.clear() self.moncolBox.addItem('auto') self.moncolBox.addItem('none') self.moncolBox.setCurrentIndex(0) self.filtercolBox.clear() self.filtercolBox.addItem('none') self.filtercolBox.setCurrentIndex(0) for i, name in enumerate(cols): self.xcolBox.addItem(name) self.ycolBox.addItem(name) self.dycolBox.addItem(name) self.moncolBox.addItem(name) self.filtercolBox.addItem(name) self.monscaleEdit.setText(str(nmon or 1)) self.numorsEdit.setText(str(numor)) self.open_data() def open_data(self, final=False): try: prec = float(self.precisionEdit.text()) except ValueError: QMessageBox.information(self, 'Error', 'Enter a valid precision.') return floatmerge = self.rbFloatMerge.isChecked() xcol = str(self.xcolBox.currentText()) ycol = str(self.ycolBox.currentText()) dycol = str(self.dycolBox.currentText()) mcol = str(self.moncolBox.currentText()) fcol = str(self.filtercolBox.currentText()) if mcol == 'none': mcol = None if dycol == 'sqrt(Y)': dycol = None try: mscale = int(self.monscaleEdit.text()) except Exception: QMessageBox.information(self, 'Error', 'Monitor scale must be integer.') return if fcol == 'none': filter = None else: try: val = float(self.filtervalEdit.text()) except ValueError: val = bytes(self.filtervalEdit.text(), 'utf-8') filter = {fcol: val} dtempl = path_to_str(self.templateEdit.text()) self.loader.template = dtempl numors = str(self.numorsEdit.text()) try: datas = self.loader.load_numors(numors, prec, xcol, ycol, dycol, mcol, mscale, floatmerge, filter) except Exception as e: self.logger.exception('Error while loading data file') QMessageBox.information(self, 'Error', str(e)) return self.last_data = datas if final: self.newDatas.emit(datas, self.groupBox.currentText()) self.closeRequest.emit() else: self.plot() def initialize(self): pass def plot(self, limits=True, canvas=None): self.plotter.reset() xlabels = set() ylabels = set() titles = set() for data in self.last_data: xlabels.add(data.xaxis) ylabels.add(data.yaxis) titles.add(data.title) if isinstance(data, ImageData): # XXX this plots only one self.plotter.plot_image(data, multi=True) break else: self.plotter.plot_data(data, multi=True) self.plotter.plot_finish(', '.join(xlabels), ', '.join(ylabels), ', '.join(titles)) self.plotter.draw()
class UfitSession(QObject): propsRequested = pyqtSignal() propsUpdated = pyqtSignal() dirtyChanged = pyqtSignal(bool) filenameChanged = pyqtSignal() modelFitted = pyqtSignal(object, object) groupAdded = pyqtSignal(object) groupUpdated = pyqtSignal(object) itemsUpdated = pyqtSignal() itemUpdated = pyqtSignal(object) itemAdded = pyqtSignal(object) def __init__(self): QObject.__init__(self) self.filename = None self.groups = [] self.all_items = set() self.clear() @property def dirname(self): if self.filename: return path.dirname(self.filename) return '' def clear(self): self.groups[:] = [ItemGroup('Default')] self.groups[0].expanded = True self.all_items.clear() self.props = attrdict() self.itemsUpdated.emit() self.propsUpdated.emit() self.dirtyChanged.emit(False) def new(self): self.filename = None self.filenameChanged.emit() self.clear() def set_filename(self, filename): self.filename = filename self.filenameChanged.emit() def _load_v0(self, info): info['version'] = 1 info['datasets'] = info.pop('panels') info['template'] = '' self._load_v1(info) def _load_v1(self, info): info['version'] = 2 datasets = info.pop('datasets') info['panels'] = [('dataset', d[0], d[1]) for d in datasets] self._load_v2(info) def _load_v2(self, info): info['version'] = 3 group = ItemGroup('Default') info['groups'] = [group] panels = info.pop('panels') from ufit.gui.mappingitem import MappingItem from ufit.gui.scanitem import ScanDataItem for panel in panels: if panel[0] == 'dataset': group.items.append(ScanDataItem(panel[1], panel[2])) elif panel[0] == 'mapping': group.items.append(MappingItem(panel[1], panel[2])) info['props'] = attrdict() info['props'].template = info.pop('template') self._load_v3(info) def _load_v3(self, info): self.props = info['props'] self.groups[:] = info['groups'] def load(self, filename): self.clear() # unpickle everything with open(filename, 'rb') as fp: if six.PY3: info = pickle.load(fp, encoding='latin1') else: info = pickle.load(fp) # load with the respective method savever = info.get('version', 0) try: getattr(self, '_load_v%d' % savever)(info) except AttributeError: raise UFitError('save version %d not supported' % savever) self.filename = filename # reassign indices (also to regenerate descriptions) for group in self.groups: for i, item in enumerate(group.items): item.set_group(group, i + 1) item.after_load() self.all_items.add(item) group.update_htmldesc() # let GUI elements update from propsdata self.itemsUpdated.emit() self.propsUpdated.emit() self.filenameChanged.emit() def save(self): # let GUI elements update the stored propsdata self.propsRequested.emit() if self.filename is None: raise UFitError('session has no filename yet') info = { 'version': SAVE_VERSION, 'groups': self.groups, 'props': self.props, } with open(self.filename, 'wb') as fp: pickle.dump(info, fp, protocol=pickle.HIGHEST_PROTOCOL) self.dirtyChanged.emit(False) def add_group(self, name): group = ItemGroup(name) self.groups.append(group) self.set_dirty() self.groupAdded.emit(group) return group def remove_group(self, group): self.groups.remove(group) for item in group.items: self.all_items.discard(item) self.set_dirty() self.itemsUpdated.emit() def rename_group(self, group, name): group.name = name group.update_htmldesc() self.set_dirty() self.groupUpdated.emit(group) def add_item(self, item, group=None): if group is None: group = self.groups[-1] self.all_items.add(item) group.items.append(item) group.update_htmldesc() item.set_group(group, len(group.items)) self.set_dirty() self.itemAdded.emit(item) def add_items(self, items, group=None): if not items: return if group is None: group = self.groups[-1] self.all_items.update(items) for item in items: group.items.append(item) item.set_group(group, len(group.items)) group.update_htmldesc() self.set_dirty() self.itemsUpdated.emit() self.itemAdded.emit(items[-1]) def remove_items(self, items): renumber_groups = set() for item in items: renumber_groups.add(item.group) item.group.items.remove(item) self.all_items.discard(item) for group in renumber_groups: if not group.items: self.groups.remove(group) for i, item in enumerate(group.items): item.set_group(group, i + 1) group.update_htmldesc() self.set_dirty() self.itemsUpdated.emit() def move_items(self, items, newgroup): renumber_groups = set([newgroup]) for item in items: renumber_groups.add(item.group) item.group.items.remove(item) newgroup.items.append(item) for group in renumber_groups: for i, item in enumerate(group.items): item.set_group(group, i + 1) group.update_htmldesc() self.set_dirty() self.itemsUpdated.emit() def copy_items(self, items, newgroup): from ufit.gui.scanitem import ScanDataItem for item in items: new_data = item.data.copy() new_model = item.model.copy() new_item = ScanDataItem(new_data, new_model) self.all_items.add(new_item) newgroup.items.append(new_item) newgroup.update_htmldesc() new_item.set_group(newgroup, len(newgroup.items)) self.set_dirty() self.itemsUpdated.emit() self.itemAdded.emit(items[-1]) def reorder_groups(self, new_structure): del self.groups[:] for group, items in new_structure: self.groups.append(group) group.items[:] = items for i, item in enumerate(items): item.set_group(group, i + 1) group.update_htmldesc() self.set_dirty() self.itemsUpdated.emit() def set_dirty(self): self.dirtyChanged.emit(True)