class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('PyQtConfig Demo') self.config = ConfigManager() CHOICE_A = 1 CHOICE_B = 2 CHOICE_C = 3 CHOICE_D = 4 map_dict = { 'Choice A': CHOICE_A, 'Choice B': CHOICE_B, 'Choice C': CHOICE_C, 'Choice D': CHOICE_D, } self.config.set_defaults({ 'number': 13, 'text': 'hello', 'active': True, 'combo': CHOICE_C, }) gd = QGridLayout() sb = QSpinBox() gd.addWidget(sb, 0, 1) self.config.add_handler('number', sb) te = QLineEdit() gd.addWidget(te, 1, 1) self.config.add_handler('text', te) cb = QCheckBox() gd.addWidget(cb, 2, 1) self.config.add_handler('active', cb) cmb = QComboBox() cmb.addItems(map_dict.keys()) gd.addWidget(cmb, 3, 1) self.config.add_handler('combo', cmb, mapper=map_dict) self.current_config_output = QTextEdit() gd.addWidget(self.current_config_output, 0, 3, 3, 1) self.config.updated.connect(self.show_config) self.show_config() self.window = QWidget() self.window.setLayout(gd) self.setCentralWidget(self.window) def show_config(self): self.current_config_output.setText(str(self.config.as_dict()))
class Preferences(GenericDialog): ''' Application preferences dialog. This is passed a set of options and these are duplicated-stored internally. If the preferences dialog is exited with OK, these changes are copied and applied back to the global settings object. If exited with Cancel they are discarded. ''' def __init__(self, parent, config=None, *args, **kwargs): super(Preferences, self).__init__(parent, *args, **kwargs) if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.dialogFinalise()
class LisaConfigWindow(QDialog): # class LisaConfigWindow(QMainWindow): def __init__(self, config_in, ncols=2): super(LisaConfigWindow, self).__init__() self.setWindowTitle('Lisa Config') self.config = ConfigManager() self.ncols = ncols # used with other module. If it is set, lisa config is deleted self.reset_config = False CHOICE_A = "{pairwise_alpha_per_mm2: 45, return_only_object_with_seeds: true}" # CHOICE_A = 23 CHOICE_B = 2 CHOICE_C = 3 CHOICE_D = 4 map_dict = { 'Graph-Cut': CHOICE_A, 'Choice B': CHOICE_B, 'Choice C': CHOICE_C, 'Choice D': CHOICE_D, } config_def = { 'working_voxelsize_mm': 2.0, 'save_filetype': 'pklz', # 'manualroi': True, 'segparams': CHOICE_A, } config_def.update(config_in) self.config.set_defaults(config_def) gd = QGridLayout() gd_max_i = 0 for key, value in config_in.iteritems(): if type(value) is int: sb = QSpinBox() sb.setRange(-100000, 100000) elif type(value) is float: sb = QDoubleSpinBox() elif type(value) is str: sb = QLineEdit() elif type(value) is bool: sb = QCheckBox() else: logger.error("Unexpected type in config dictionary") row = gd_max_i / self.ncols col = (gd_max_i % self.ncols) * 2 gd.addWidget(QLabel(key),row, col +1) gd.addWidget(sb, row, col + 2) self.config.add_handler(key, sb) gd_max_i += 1 # gd.addWidget(QLabel("save filetype"), 1, 1) # te = QLineEdit() # gd.addWidget(te, 1, 2) # self.config.add_handler('save_filetype', te) # # gd.addWidget(QLabel("segmentation parameters"), 2, 1) # te = QLineEdit() # gd.addWidget(te, 2, 2) # self.config.add_handler('segparams', te) # cb = QCheckBox() # gd.addWidget(cb, 2, 2) # self.config.add_handler('active', cb) # gd.addWidget(QLabel("segmentation parameters"), 3, 1) # cmb = QComboBox() # cmb.addItems(map_dict.keys()) # gd.addWidget(cmb, 3, 2) # self.config.add_handler('segparams', cmb, mapper=map_dict) self.current_config_output = QTextEdit() # rid.setColumnMinimumWidth(3, logo.width()/2) text_col = (self.ncols * 2) + 3 gd.setColumnMinimumWidth(text_col, 500) gd.addWidget(self.current_config_output, 1, text_col, (gd_max_i/2)-1, 1) btn_reset_config = QPushButton("Reset and quit", self) btn_reset_config.clicked.connect(self.btnResetConfig) gd.addWidget(btn_reset_config, 0, text_col) btn_save_config = QPushButton("Save and quit", self) btn_save_config.clicked.connect(self.btnSaveConfig) gd.addWidget(btn_save_config, (gd_max_i / 2), text_col) self.config.updated.connect(self.show_config) self.show_config() # my line self.setLayout(gd) # self.window = QWidget() # self.window.setLayout(gd) # self.setCentralWidget(self.window) def add_grid_line(self, key, value): pass def get_config_as_dict(self): dictionary = self.config.as_dict() for key, value in dictionary.iteritems(): from PyQt4.QtCore import pyqtRemoveInputHook pyqtRemoveInputHook() # import ipdb; ipdb.set_trace() # noqa BREAKPOINT if type(value) == PyQt4.QtCore.QString: value = str(value) dictionary[key] = value return dictionary def btnResetConfig(self, event=None): self.reset_config = True self.close() def btnSaveConfig(self, event=None): self.reset_config = False self.close() def show_config(self): text = str(self.get_config_as_dict()) self.current_config_output.setText(text)
class QGraphicsSceneExtend(QGraphicsScene): def __init__(self, parent, *args, **kwargs): super(QGraphicsSceneExtend, self).__init__(parent, *args, **kwargs) self.m = parent.m self.config = ConfigManager() # These config settings are transient (ie. not stored between sessions) self.config.set_defaults({ 'mode': EDITOR_MODE_NORMAL, 'font-family': 'Arial', 'font-size': '12', 'text-bold': False, 'text-italic': False, 'text-underline': False, 'text-color': '#000000', 'color-border': None, # '#000000', 'color-background': None, }) # Pre-set these values (will be used by default) self.config.set('color-background', '#5555ff') self.background_image = QImage( os.path.join(utils.scriptdir, 'icons', 'grid100.png')) if settings.get('Editor/Show_grid'): self.showGrid() else: self.hideGrid() self.mode = EDITOR_MODE_NORMAL self.mode_current_object = None self.annotations = [] def mousePressEvent(self, e): if self.config.get('mode') != EDITOR_MODE_NORMAL: for i in self.selectedItems(): i.setSelected(False) if self.config.get('mode') == EDITOR_MODE_TEXT: tw = AnnotationTextItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_REGION: tw = AnnotationRegionItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_ARROW: tw = AnnotationRegionItem(position=e.scenePos()) self.addItem(tw) self.mode_current_object = tw tw._createFromMousePressEvent(e) tw.importStyleConfig(self.config) self.annotations.append(tw) else: super(QGraphicsSceneExtend, self).mousePressEvent(e) def mouseMoveEvent(self, e): if self.config.get( 'mode') == EDITOR_MODE_TEXT and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) elif self.config.get( 'mode') == EDITOR_MODE_REGION and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) else: super(QGraphicsSceneExtend, self).mouseMoveEvent(e) def mouseReleaseEvent(self, e): if self.config.get('mode'): self.mode_current_object.setSelected(True) self.mode_current_object.setFocus() self.config.set('mode', EDITOR_MODE_NORMAL) self.mode_current_object = None super(QGraphicsSceneExtend, self).mouseReleaseEvent(e) def showGrid(self): self.setBackgroundBrush(QBrush(self.background_image)) def hideGrid(self): self.setBackgroundBrush(QBrush(None)) def onSaveAsImage(self): filename, _ = QFileDialog.getSaveFileName( self.m, 'Save current figure', '', "Tagged Image File Format (*.tif);;\ Portable Network Graphics (*.png)" ) if filename: self.saveAsImage(filename) def saveAsImage(self, f): self.image = QImage(self.sceneRect().size().toSize(), QImage.Format_ARGB32) self.image.fill(Qt.white) painter = QPainter(self.image) self.render(painter) self.image.save(f) def addApp(self, app, position=None): i = ToolItem(self, app, position=position) self.addItem(i) return i def removeApp(self, app): i = app.editorItem i.hide() self.removeItem(i) app.editorItem = None def dragEnterEvent(self, e): if e.mimeData().hasFormat('application/x-pathomx-app') or e.mimeData( ).hasFormat('text/uri-list'): e.accept() else: e.ignore() def dragMoveEvent(self, e): e.accept() def dropEvent(self, e): scenePos = e.scenePos() - QPointF(32, 32) if e.mimeData().hasFormat('application/x-pathomx-app'): try: app_id = str(e.mimeData().data('application/x-pathomx-app'), 'utf-8') # Python 3 except: app_id = str( e.mimeData().data('application/x-pathomx-app')) # Python 2 e.setDropAction(Qt.CopyAction) a = app_launchers[app_id](self.m, position=scenePos, auto_focus=False) #self.centerOn(a.editorItem) e.accept() elif e.mimeData().hasFormat('text/uri-list'): for ufn in e.mimeData().urls(): fn = ufn.path() fnn, ext = os.path.splitext(fn) ext = ext.strip('.') if ext in file_handlers: a = file_handlers[ext](position=scenePos, auto_focus=False, filename=fn) self.centerOn(a.editorItem) e.accept() def getXMLAnnotations(self, root): # Iterate over the entire set (in order) creating a XML representation of the MatchDef and Style for annotation in self.annotations: ase = et.SubElement(root, "Annotation") ase.set('type', type(annotation).__name__) ase.set('x', str(annotation.x())) ase.set('y', str(annotation.y())) ase.set('width', str(annotation.rect().width())) ase.set('height', str(annotation.rect().height())) if hasattr(annotation, 'text'): text = et.SubElement(ase, "Text") text.text = annotation.text.toPlainText() ase = annotation.config.getXMLConfig(ase) return root def setXMLAnnotations(self, root): ANNOTATION_TYPES = { 'AnnotationTextItem': AnnotationTextItem, 'AnnotationRegionItem': AnnotationRegionItem, } for ase in root.findall('Annotation'): # Validate the class definition before creating it if ase.get('type') in ANNOTATION_TYPES: pos = QPointF(float(ase.get('x')), float(ase.get('y'))) aobj = ANNOTATION_TYPES[ase.get('type')](position=pos) aobj.setRect( QRectF(0, 0, float(ase.get('width')), float(ase.get('height')))) to = ase.find('Text') if to is not None: aobj.text.setPlainText(to.text) self.addItem(aobj) self.annotations.append(aobj) aobj.config.setXMLConfig(ase) aobj.applyStyleConfig()
class DictWidget(QtGui.QWidget): def __init__(self, config_in, ncols=2, captions=None, hide_keys=None, horizontal=False, show_captions=True, accept_button=False, config_manager=None, radiobuttons=None, dropdownboxes=None): """ :param config_in: dictionary :param ncols: :param captions: :param radiobuttons: {"name_of_radiobutton": [ ["orig_caption_1", orig_caption_2], default_value] ] } """ super(DictWidget, self).__init__() if captions is None: captions = {} if hide_keys is None: hide_keys = [] self.config_in = config_in self.ncols = ncols self.captions = captions self.accept_button = accept_button self.hide_keys = copy.copy(hide_keys) self.horizontal = horizontal self.show_captions = show_captions if radiobuttons is None: radiobuttons = {} self.radiobuttons = radiobuttons if dropdownboxes is None: dropdownboxes = {} self.dropdownboxes = dropdownboxes # hide also temp keys for lists and ndarrays # due to load default params self._get_tmp_composed_keys(config_in) self.hide_keys.extend(self._tmp_composed_keys_list) if config_manager is None: self.config = ConfigManager() self.config.set_defaults(config_in) else: self.config = config_manager self.mainLayout = QGridLayout(self) self.setLayout(self.mainLayout) self.widgets = {} self.grid_i = 0 self.init_ui() def _get_tmp_composed_keys(self, cfg): """ Get list of temporary keys for lists and ndarrays. This keys are used for reconstruction. vytvoří to seznam pomocných klíčů pro seznamy a ndarray :param cfg: :return: """ self._tmp_composed_keys_dict = {} self._tmp_composed_keys_list = [] toappend = {} for key, value in cfg.iteritems(): if key in self.dropdownboxes.keys(): continue if key in self.radiobuttons.keys(): continue if type(value) in (list, np.ndarray): self._tmp_composed_keys_dict[key] = [] array = np.asarray(value) key_array_i = 0 for val in array.tolist(): # key_i = (key, key_array_i) key_i = ComposedDictMetadata((key, key_array_i)) self._tmp_composed_keys_dict[key].append(key_i) self._tmp_composed_keys_list.append(key_i) key_array_i += 1 toappend[key_i] = val cfg.update(toappend) def _create_dropdownbox(self, key, value): # atomic_widget = QWidget() row, col = self.__calculate_new_grid_position() # layout = QBoxLayout(self.horizontal) atomic_widget = QComboBox() # atomic_widget.addItem("C") # atomic_widget.addItem("C++") values = self.dropdownboxes[key] # values = self.dropdownboxes[key][0] if value is not None and value in values: # vali = atomic_widget.findText(value) atomic_widget.findText(value) atomic_widget.addItems(values) # this does not work. I used findText() # atomic_widget.setCurrentIndex(vali) # layout.addWidget(cb) # atomic_widget.setLayout(layout) return atomic_widget def _create_radiobutton(self, key, value): atomic_widget = QWidget() layout = QBoxLayout(self.horizontal) for i, rbkey in enumerate(self.radiobuttons[key][0]): b1 = QRadioButton("Button1") if i == self.radiobuttons[key][1]: b1.setChecked(True) # b1.toggled.connect(lambda:self.btnstate(self.b1)) layout.addWidget(b1) atomic_widget.setLayout(layout) return atomic_widget def init_ui(self): # self.widgets = {} # self.grid_i = 0 grid = self.mainLayout for key, value in self.config_in.iteritems(): if key in self.hide_keys: continue if key in self.captions.keys(): caption = self.captions[key] else: caption = key atomic_widget = self.__get_widget_for_primitive_types(key, value) if atomic_widget is None: if type(value) in (list, np.ndarray): array = np.asarray(value) atomic_widget = self._create_sub_grid_from_ndarray(key, array) row, col = self.__calculate_new_grid_position() grid.addWidget(QLabel(caption), row, col + 1) grid.addLayout(atomic_widget, row, col + 2) continue else: logger.error("Unexpected type in config dictionary") continue # import ipdb; ipdb.set_trace() row, col = self.__calculate_new_grid_position() grid.addWidget(QLabel(caption), row, col + 1) grid.addWidget(atomic_widget, row, col + 2) # gd.setColumnMinimumWidth(text_col, 500) if self.accept_button: btn_accept = QPushButton("Accept", self) btn_accept.clicked.connect(self.btn_accept) text_col = (self.ncols * 2) + 3 grid.addWidget(btn_accept, (self.grid_i / 2), text_col) self.config.updated.connect(self.on_config_update) # def __add_line def __get_widget_for_primitive_types(self, key, value): """ return right widget and connect the value with config_manager :param key: :param value: :return: """ if key in self.dropdownboxes.keys(): atomic_widget = self._create_dropdownbox(key,value) self.config.add_handler(key, atomic_widget) elif key in self.radiobuttons.keys(): atomic_widget = self._create_radiobutton(key,value) self.config.add_handler(key, atomic_widget) elif type(value) is int: atomic_widget = QSpinBox() atomic_widget.setRange(-100000, 100000) self.config.add_handler(key, atomic_widget) elif type(value) is float: atomic_widget = QDoubleSpinBox() atomic_widget.setDecimals(6) atomic_widget.setMaximum(1000000000) self.config.add_handler(key, atomic_widget) elif type(value) is str: atomic_widget = QLineEdit() self.config.add_handler(key, atomic_widget) elif type(value) is bool: atomic_widget = QCheckBox() self.config.add_handler(key, atomic_widget) else: return None return atomic_widget def _create_sub_grid_from_ndarray(self, key, ndarray): hgrid = QGridLayout(self) hgrid_i = 0 for val in ndarray.tolist(): # key_i = key + str(hgrid_i) key_i = (key, hgrid_i) atomic_widget = self.__get_widget_for_primitive_types(key_i, val) hgrid.addWidget(atomic_widget, 0, hgrid_i) hgrid_i += 1 return hgrid def __calculate_new_grid_position(self): row = self.grid_i / self.ncols col = (self.grid_i % self.ncols) * 2 self.grid_i += 1 if self.horizontal: return col, row return row, col def btn_accept(self): print ("btn_accept: " + str(self.config_as_dict())) def on_config_update(self): pass def config_as_dict(self): def _primitive_type(value): if type(value) == PyQt4.QtCore.QString: value = str(value) return value dictionary = self.config.as_dict() dictionary = copy.copy(dictionary) for key, value in dictionary.iteritems(): from PyQt4.QtCore import pyqtRemoveInputHook #pyqtRemoveInputHook() # import ipdb; ipdb.set_trace() # noqa BREAKPOINT # if type(key) == tuple: if type(key) == ComposedDictMetadata: dict_key, value_index = key dict_key = (dict_key) # if dict_key not in dictionary.keys(): # dictionary[dict_key] = {} dictionary[dict_key][value_index] = _primitive_type(value) else: dictionary[key] = _primitive_type(value) for key in dictionary.keys(): # if type(key) == tuple: if type(key) == ComposedDictMetadata: dictionary.pop(key) # for key, value in dictionary.iteritems(): # if type(key) == tuple: # dictionary return dictionary
class DictGui(QDialog): # class LisaConfigWindow(QMainWindow): def __init__(self, config_in, ncols=2): super(DictGui, self).__init__() self.setWindowTitle('Lisa Config') self.config = ConfigManager() self.ncols = ncols # used with other module. If it is set, lisa config is deleted self.reset_config = False CHOICE_A = "{pairwise_alpha_per_mm2: 45, return_only_object_with_seeds: true}" # CHOICE_A = 23 CHOICE_B = 2 CHOICE_C = 3 CHOICE_D = 4 map_dict = { 'Graph-Cut': CHOICE_A, 'Choice B': CHOICE_B, 'Choice C': CHOICE_C, 'Choice D': CHOICE_D, } config_def = { 'working_voxelsize_mm': 2.0, 'save_filetype': 'pklz', # 'manualroi': True, 'segparams': CHOICE_A, } config_def.update(config_in) self.config.set_defaults(config_def) gd = QGridLayout() gd_max_i = 0 for key, value in config_in.items(): if type(value) is int: sb = QSpinBox() sb.setRange(-100000, 100000) elif type(value) is float: sb = QDoubleSpinBox() elif type(value) is str: sb = QLineEdit() elif type(value) is bool: sb = QCheckBox() else: logger.error("Unexpected type in config dictionary") row = gd_max_i / self.ncols col = (gd_max_i % self.ncols) * 2 gd.addWidget(QLabel(key), row, col + 1) gd.addWidget(sb, row, col + 2) self.config.add_handler(key, sb) gd_max_i += 1 # gd.addWidget(QLabel("save filetype"), 1, 1) # te = QLineEdit() # gd.addWidget(te, 1, 2) # self.config.add_handler('save_filetype', te) # # gd.addWidget(QLabel("segmentation parameters"), 2, 1) # te = QLineEdit() # gd.addWidget(te, 2, 2) # self.config.add_handler('segparams', te) # cb = QCheckBox() # gd.addWidget(cb, 2, 2) # self.config.add_handler('active', cb) # gd.addWidget(QLabel("segmentation parameters"), 3, 1) # cmb = QComboBox() # cmb.addItems(map_dict.keys()) # gd.addWidget(cmb, 3, 2) # self.config.add_handler('segparams', cmb, mapper=map_dict) self.current_config_output = QTextEdit() # rid.setColumnMinimumWidth(3, logo.width()/2) text_col = (self.ncols * 2) + 3 gd.setColumnMinimumWidth(text_col, 500) gd.addWidget(self.current_config_output, 1, text_col, (gd_max_i / 2) - 1, 1) btn_reset_config = QPushButton("Default", self) btn_reset_config.clicked.connect(self.btnResetConfig) gd.addWidget(btn_reset_config, 0, text_col) btn_save_config = QPushButton("Ok", self) btn_save_config.clicked.connect(self.btnSaveConfig) gd.addWidget(btn_save_config, (gd_max_i / 2), text_col) self.config.updated.connect(self.show_config) self.show_config() # my line self.setLayout(gd) # self.window = QWidget() # self.window.setLayout(gd) # self.setCentralWidget(self.window) def add_grid_line(self, key, value): pass def get_config_as_dict(self): dictionary = self.config.as_dict() for key, value in dictionary.items(): from PyQt4.QtCore import pyqtRemoveInputHook pyqtRemoveInputHook() # import ipdb; ipdb.set_trace() # noqa BREAKPOINT if type(value) == QString: value = str(value) dictionary[key] = value return dictionary def btnResetConfig(self, event=None): self.reset_config = True self.close() def btnSaveConfig(self, event=None): self.reset_config = False self.close() def show_config(self): text = str(self.get_config_as_dict()) self.current_config_output.setText(text)
class AnnotatePeaks(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotatePeaks, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Peaks') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Peaks') vbox = QVBoxLayout() # Populate the list boxes self.lw_peaks = QListWidgetAddRemove() self.lw_peaks.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_peaks) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_start = QDoubleSpinBox() self.add_start.setRange(-1, 12) self.add_start.setDecimals(3) self.add_start.setSuffix('ppm') self.add_start.setSingleStep(0.001) self.add_end = QDoubleSpinBox() self.add_end.setRange(-1, 12) self.add_end.setDecimals(3) self.add_end.setSuffix('ppm') self.add_end.setSingleStep(0.001) addc = QPushButton('Add') addc.clicked.connect(self.onPeakAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onPeakAdd) loadc = QPushButton("Import from file") loadc.setIcon( QIcon( os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onPeakImport) metabh = QPushButton("Auto match via MetaboHunter") metabh.setIcon( QIcon(os.path.join(utils.scriptdir, 'icons', 'metabohunter.png'))) metabh.clicked.connect(self.onPeakImportMetabohunter) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_start) vboxh.addWidget(self.add_end) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vbox.addWidget(loadc) vbox.addWidget(metabh) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/peaks', self.lw_peaks, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onPeakAdd(self): c = self.config.get( 'annotation/peaks' )[:] # Create new list to force refresh on reassign c.append((self.add_label.text(), float(self.add_start.value()), float(self.add_end.value()))) self.config.set('annotation/peaks', c) def onPeakRemove(self): i = self.lw_peaks.removeItemAt(self.lw_peaks.currentRow()) # c = self.map_list_fwd(i.text()) def onPeakImport(self): filename, _ = QFileDialog.getOpenFileName( self.parent(), 'Load peak annotations from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)" ) if filename: c = self.config.get( 'annotation/peaks' )[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append(row[0], float(row[1]), float(row[2])) self.config.set('annotation/peaks', c) def onPeakImportMetabohunter(self): c = self.config.get( 'annotation/peaks' )[:] # Create new list to force refresh on reassign t = self.parent().current_tool dlg = MetaboHunter(self) if dlg.exec_(): if 'spc' in t.data: # We have a spectra; calcuate mean; reduce size if required spc = t.data['spc'] n = spc.data.shape[1] ppm = spc.ppm spcd = np.mean(spc.data, axis=0) # Set a hard limit on the size of data we submit to be nice. if n > 3000: # Calculate the division required to be under the limit d = np.ceil(float(n) / 3000) # Trim axis to multiple of divisor trim = (n // d) * d spcd = spcd[:trim] ppm = ppm[:trim] # Mean d shape spcd = np.mean(spcd.reshape(-1, d), axis=1) ppm = np.mean(ppm.reshape(-1, d), axis=1) # Submit with settings hmdbs = metabohunter.request( ppm, spcd, metabotype=dlg.config.get('Metabotype'), database=dlg.config.get('Database Source'), ph=dlg.config.get('Sample pH'), solvent=dlg.config.get('Solvent'), frequency=dlg.config.get('Frequency'), method=dlg.config.get('Method'), noise=dlg.config.get('Noise Threshold'), confidence=dlg.config.get('Confidence Threshold'), tolerance=dlg.config.get('Tolerance')) ha = np.array(hmdbs) unique_hmdbs = set(hmdbs) if None in unique_hmdbs: unique_hmdbs.remove(None) # Extract out regions for hmdb in unique_hmdbs: hb = np.diff(ha == hmdb) # These are needed to ensure markers are there for objects starting and ending on array edge if ha[0] == hmdb: hb[0] == True if ha[-1] == hmdb: hb[-1] == True idx = np.nonzero(hb)[0] idx = idx.reshape(-1, 2) if dlg.config.get( 'convert_hmdb_ids_to_names' ) and hmdb in METABOHUNTER_HMDB_NAME_MAP.keys(): label = METABOHUNTER_HMDB_NAME_MAP[hmdb] else: label = hmdb # Now we have an array of all start, stop positions for this item for start, stop in idx: c.append((label, ppm[start], ppm[stop])) self.config.set('annotation/peaks', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%.2f\t%.2f" % tuple(x) self.fwd_map_cache[s] = x return s
class MetaboHunter(GenericDialog): options = { 'Metabotype': { 'All': 'All', 'Drug': 'Drug', 'Food additive': 'Food additive', 'Mammalian': 'Mammalian', 'Microbial': 'Microbial', 'Plant': 'Plant', 'Synthetic/Industrial chemical': 'Synthetic/Industrial chemical', }, 'Database Source': { 'Human Metabolome Database (HMDB)': 'HMDB', 'Madison Metabolomics Consortium Database (MMCD)': 'MMCD', }, 'Sample pH': { '10.00 - 10.99': 'ph7', '7.00 - 9.99': 'ph7', '6.00 - 6.99': 'ph6', '5.00 - 5.99': 'ph5', '4.00 - 4.99': 'ph4', '3.00 - 3.99': 'ph3', }, 'Solvent': { 'All': 'all', 'Water': 'water', 'CDCl3': 'cdcl3', 'CD3OD': '5d3od', '5% DMSO': '5dmso', }, 'Frequency': { 'All': 'all', '600 MHz': '600', '500 MHz': '500', '400 MHz': '400', }, 'Method': { 'MH1: Highest number of matched peaks': 'HighestNumber', 'MH2: Highest number of matched peaks with shift tolerance': 'HighestNumberNeighbourhood', 'MH3: Greedy selection of metabolites with disjoint peaks': 'Greedy2', 'MH4: Highest number of matched peaks with intensities': 'HighestNumberHeights', 'MH5: Greedy selection of metabolites with disjoint peaks and heights': 'Greedy2Heights', }, } def __init__(self, *args, **kwargs): super(MetaboHunter, self).__init__(*args, **kwargs) self.setWindowTitle('MetaboHunter') # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'Metabotype': 'All', 'Database Source': 'HMDB', 'Sample pH': 'ph7', 'Solvent': 'water', 'Frequency': 'all', 'Method': 'HighestNumberNeighbourhood', 'Noise Threshold': 0.0001, 'Confidence Threshold': 0.5, 'Tolerance': 0.1, 'convert_hmdb_ids_to_names': True, }) self.lw_combos = {} for o in [ 'Metabotype', 'Database Source', 'Sample pH', 'Solvent', 'Frequency', 'Method' ]: row = QVBoxLayout() cl = QLabel(o) cb = QComboBox() cb.addItems(list(self.options[o].keys())) row.addWidget(cl) row.addWidget(cb) self.config.add_handler(o, cb, self.options[o]) self.layout.addLayout(row) row = QGridLayout() self.lw_spin = {} for n, o in enumerate( ['Noise Threshold', 'Confidence Threshold', 'Tolerance']): cl = QLabel(o) cb = QDoubleSpinBox() cb.setDecimals(4) cb.setRange(0, 1) cb.setSingleStep(0.01) cb.setValue(float(self.config.get(o))) row.addWidget(cl, 0, n) row.addWidget(cb, 1, n) self.config.add_handler(o, cb) self.layout.addLayout(row) row = QHBoxLayout() row.addWidget(QLabel("Convert HMDB IDs to chemical names?")) conv = QCheckBox() self.config.add_handler('convert_hmdb_ids_to_names', conv) row.addWidget(conv) self.layout.addLayout(row) self.dialogFinalise()
class SerialDialog(QDialog): def __init__(self, settings, parent=None): super().__init__(parent) self.settings = settings self.serialports = [] # port self.portLabel = QLabel(self.tr("COM Port:")) self.portComboBox = QComboBox() self.portLabel.setBuddy(self.portComboBox) self.refresh_comports(self.portComboBox) # baudrate self.baudrateLabel = QLabel(self.tr("Baudrate:")) self.baudrateComboBox = QComboBox() self.baudrateLabel.setBuddy(self.baudrateComboBox) for br in BAUDRATES: self.baudrateComboBox.addItem(str(br), br) # buttons self.dlgbuttons = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal) self.dlgbuttons.rejected.connect(self.reject) self.dlgbuttons.accepted.connect(self.accept) # layout layout = QGridLayout() layout.addWidget(self.portLabel, 0, 0) layout.addWidget(self.portComboBox, 0, 1) layout.addWidget(self.baudrateLabel, 1, 0) layout.addWidget(self.baudrateComboBox, 1, 1) layout.addWidget(self.dlgbuttons, 2, 0, 1, 2) self.setLayout(layout) self.setWindowTitle(self.tr("Serial Settings")) # settings defaults = { PORT_SETTING: "", BAUDRATE_SETTING: "115200" } self.tmp_settings = ConfigManager() self.tmp_settings.set_defaults(defaults) self.tmp_settings.set_many( {key: self.settings.get(key) for key in defaults.keys()} ) self.tmp_settings.add_handler(PORT_SETTING, self.portComboBox) self.tmp_settings.add_handler(BAUDRATE_SETTING, self.baudrateComboBox) def accept(self): d = self.tmp_settings.as_dict() self.settings.set_many(d) super().accept() def refresh_comports(self, combobox): self.serialports = serial_ports() for port in self.serialports: combobox.addItem(port) @property def port(self): return self.portComboBox.currentText() @port.setter def port(self, value): if value in self.serialports: self.portComboBox.setCurrentIndex( self.portComboBox.findText(value)) else: raise ValueError("serial port '%s' not available" % value) @property def baudrate(self): return self.baudrateComboBox.currentData()
class Automaton(QStandardItem): def __init__(self, *args, **kwargs): super(Automaton, self).__init__(*args, **kwargs) self.setData(self, Qt.UserRole) self.watcher = QFileSystemWatcher() self.timer = QTimer() self.watch_window = {} self.latest_run = {} self.is_running = False self.config = ConfigManager() self.config.set_defaults({ 'mode': MODE_WATCH_FOLDER, 'is_active': True, 'trigger_hold': 1, 'notebook_paths': '', 'output_path': '{home}/{notebook_filename}_{datetime}_', 'output_format': 'html', 'watched_files': [], 'watched_folder': '', 'watch_window': 15, 'iterate_watched_folder': True, 'iterate_wildcard': '.csv', 'timer_seconds': 60, }) self.runner = None self.lock = None self.latest_run = { 'timestamp': None, 'success': None, } # Set up all the triggers self.watcher.fileChanged.connect(self.file_trigger_accumulator) self.watcher.directoryChanged.connect(self.trigger) self.timer.timeout.connect(self.trigger) def startup(self): if self.config.get('is_active') == False: return False if self.config.get('mode') == MODE_TIMER: self.timer.setInterval(self.config.get('timer_seconds') * 1000) self.timer.start() elif self.config.get('mode') == MODE_WATCH_FILES: current_paths = self.watcher.files() + self.watcher.directories() if current_paths: self.watcher.removePaths(current_paths) self.watch_window = {} self.watcher.addPaths(self.config.get('watched_files')) elif self.config.get('mode') == MODE_WATCH_FOLDER: current_paths = self.watcher.files() + self.watcher.directories() if current_paths: self.watcher.removePaths(current_paths) self.watcher.addPath(self.config.get('watched_folder')) def shutdown(self): if self.config.get('mode') == MODE_TIMER: self.timer.stop() elif self.config.get('mode') == MODE_WATCH_FILES or self.config.get('mode') == MODE_WATCH_FOLDER: current_paths = self.watcher.files() + self.watcher.directories() if current_paths: self.watcher.removePaths(current_paths) self.watch_window = {} def load_notebook(self, filename): try: with open(filename) as f: nb = reads(f.read(), 'json') except: return None else: return nb def file_trigger_accumulator(self, f): # Accumulate triggers from changed files: if 3 files are specified to watch # we only want to fire once _all_ files have changed (within the given watch window) current_time = datetime.now() self.watch_window[f] = current_time # timestamp self.watch_window = {k: v for k, v in self.watch_window.items() if current_time - timedelta(seconds=self.config.get('watch_window')) < v} if set(self.watch_window.keys()) == set(self.watcher.files() + self.watcher.directories()): self.trigger() def trigger(self, e=None): if self.config.get('is_active') == False: return False # Filesystemwatcher triggered # Get the file and folder information; make available in vars object for run if self.lock is None: self.is_running = True self.update() self.lock = QTimer.singleShot(self.config.get('trigger_hold') * 1000, self.run) # Shutdown so we don't get in infinite loops self.shutdown() def run(self, vars={}): default_vars = { 'home': os.path.expanduser('~'), 'version': VERSION_STRING, } default_vars_and_config = dict(list(default_vars.items()) + list(self.config.config.items())) if self.runner == None: # Postpone init to trigger for responsiveness on add/remove self.runner = NotebookRunner(None, pylab=True, mpl_inline=True) if self.config.get('mode') == MODE_WATCH_FOLDER and self.config.get('iterate_watched_folder'): for (dirpath, dirnames, filenames) in os.walk(self.config.get('watched_folder')): break filenames = [f for f in filenames if self.config.get('iterate_wildcard') in f] logging.info('Watched folder contains %d files; looping' % len(filenames)) # Filenames contains the list of files in the folder else: filenames = [None] self.latest_run['timestamp'] = datetime.now() try: for f in filenames: now = datetime.now() current_vars = { 'datetime': now.strftime("%Y-%m-%d %H.%M.%S"), 'date': now.date().strftime("%Y-%m-%d"), 'time': now.time().strftime("%H.%M.%S"), 'filename': f, } vars = dict(list(default_vars_and_config.items()) + list(current_vars.items())) for nb_path in self.config.get('notebook_paths'): nb = self.load_notebook(nb_path) if nb: # Add currently running notebook path to vars vars['notebook_path'] = nb_path vars['notebook_filename'] = os.path.basename(nb_path) vars['output_path'] = self.config.get('output_path').format(**vars) parent_folder = os.path.dirname(vars['output_path']) if parent_folder: try: utils.mkdir_p(parent_folder) except: # Can't create folder self.latest_run['success'] = False raise self.run_notebook(nb, vars) else: raise NotebookNotFound(nb_path) except: self.latest_run['success'] = False traceback.print_exc() exctype, value = sys.exc_info()[:2] logging.error("%s\n%s\n%s" % (exctype, value, traceback.format_exc())) finally: self.is_running = False self.lock = None self.update() # Restart self.startup() def run_notebook(self, nb, vars={}): if len(nb['worksheets']) == 0: nb['worksheets'] = [NotebookNode({'cells': [], 'metadata': {}})] start = nb['worksheets'][0]['cells'] start.insert(0, Struct(**{ 'cell_type': 'code', 'language': 'python', 'outputs': [], 'collapsed': False, 'prompt_number': -1, 'input': 'qtipy=%s' % vars, 'metadata': {}, })) self.runner.nb = nb try: self.runner.run_notebook() except: self.latest_run['success'] = False raise else: self.latest_run['success'] = True finally: ext = dict( html='html', slides='slides', latex='latex', markdown='md', python='py', rst='rst', ) output, resources = IPyexport(IPyexporter_map[self.config.get('output_format')], self.runner.nb) output_path = vars['output_path'] + 'notebook.%s' % ext[self.config.get('output_format')] logging.info("Exporting updated notebook to %s" % output_path) with open(output_path, "w") as f: f.write(output) def update(self): global _w _w.viewer.update(self.index())
class AnnotatePeaks(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotatePeaks, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Peaks') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Peaks') vbox = QVBoxLayout() # Populate the list boxes self.lw_peaks = QListWidgetAddRemove() self.lw_peaks.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_peaks) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_start = QDoubleSpinBox() self.add_start.setRange(-1, 12) self.add_start.setDecimals(3) self.add_start.setSuffix('ppm') self.add_start.setSingleStep(0.001) self.add_end = QDoubleSpinBox() self.add_end.setRange(-1, 12) self.add_end.setDecimals(3) self.add_end.setSuffix('ppm') self.add_end.setSingleStep(0.001) addc = QPushButton('Add') addc.clicked.connect(self.onPeakAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onPeakAdd) loadc = QPushButton("Import from file") loadc.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onPeakImport) metabh = QPushButton("Auto match via MetaboHunter") metabh.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'metabohunter.png'))) metabh.clicked.connect(self.onPeakImportMetabohunter) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_start) vboxh.addWidget(self.add_end) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vbox.addWidget(loadc) vbox.addWidget(metabh) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/peaks', self.lw_peaks, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onPeakAdd(self): c = self.config.get('annotation/peaks')[:] # Create new list to force refresh on reassign c.append( (self.add_label.text(), float(self.add_start.value()), float(self.add_end.value()) ) ) self.config.set('annotation/peaks', c) def onPeakRemove(self): i = self.lw_peaks.removeItemAt(self.lw_peaks.currentRow()) # c = self.map_list_fwd(i.text()) def onPeakImport(self): filename, _ = QFileDialog.getOpenFileName(self.parent(), 'Load peak annotations from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)") if filename: c = self.config.get('annotation/peaks')[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append( row[0], float(row[1]), float(row[2]) ) self.config.set('annotation/peaks', c) def onPeakImportMetabohunter(self): c = self.config.get('annotation/peaks')[:] # Create new list to force refresh on reassign t = self.parent().current_tool dlg = MetaboHunter(self) if dlg.exec_(): if 'spc' in t.data: # We have a spectra; calcuate mean; reduce size if required spc = t.data['spc'] n = spc.data.shape[1] ppm = spc.ppm spcd = np.mean(spc.data, axis=0) # Set a hard limit on the size of data we submit to be nice. if n > 3000: # Calculate the division required to be under the limit d = np.ceil(float(n)/3000) # Trim axis to multiple of divisor trim = (n//d)*d spcd = spcd[:trim] ppm = ppm[:trim] # Mean d shape spcd = np.mean( spcd.reshape(-1,d), axis=1) ppm = np.mean( ppm.reshape(-1,d), axis=1) # Submit with settings hmdbs = metabohunter.request(ppm, spcd, metabotype=dlg.config.get('Metabotype'), database=dlg.config.get('Database Source'), ph=dlg.config.get('Sample pH'), solvent=dlg.config.get('Solvent'), frequency=dlg.config.get('Frequency'), method=dlg.config.get('Method'), noise=dlg.config.get('Noise Threshold'), confidence=dlg.config.get('Confidence Threshold'), tolerance=dlg.config.get('Tolerance') ) ha = np.array(hmdbs) unique_hmdbs = set(hmdbs) if None in unique_hmdbs: unique_hmdbs.remove(None) # Extract out regions for hmdb in unique_hmdbs: hb = np.diff(ha == hmdb) # These are needed to ensure markers are there for objects starting and ending on array edge if ha[0] == hmdb: hb[0] == True if ha[-1] == hmdb: hb[-1] == True idx = np.nonzero(hb)[0] idx = idx.reshape(-1,2) if dlg.config.get('convert_hmdb_ids_to_names') and hmdb in METABOHUNTER_HMDB_NAME_MAP.keys(): label = METABOHUNTER_HMDB_NAME_MAP[hmdb] else: label = hmdb # Now we have an array of all start, stop positions for this item for start, stop in idx: c.append( (label, ppm[start], ppm[stop]) ) self.config.set('annotation/peaks', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%.2f\t%.2f" % tuple(x) self.fwd_map_cache[s] = x return s
class AnnotateClasses(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotateClasses, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Classes') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Sample classes') vbox = QVBoxLayout() # Populate the list boxes self.lw_classes = QListWidgetAddRemove() self.lw_classes.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_classes) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_class = QLineEdit() addc = QPushButton('Add') addc.clicked.connect(self.onClassAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onClassAdd) loadc = QPushButton('Import from file') loadc.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onClassImport) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_class) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vboxh.addWidget(loadc) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/sample_classes', self.lw_classes, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onClassAdd(self): c = self.config.get('annotation/sample_classes')[:] # Create new list to force refresh on reassign c.append( (self.add_label.text(), self.add_class.text()) ) self.config.set('annotation/sample_classes', c) def onClassRemove(self): i = self.lw_classes.removeItemAt(self.lw_classes.currentRow()) # c = self.map_list_fwd(i.text()) def onClassImport(self): filename, _ = QFileDialog.getOpenFileName(self.parent(), 'Load classifications from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)") if filename: c = self.config.get('annotation/sample_classes')[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append(row[:2]) self.config.set('annotation/sample_classes', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%s" % tuple(x) self.fwd_map_cache[s] = x return s
class MetaboHunter(GenericDialog): options = { 'Metabotype': { 'All': 'All', 'Drug': 'Drug', 'Food additive': 'Food additive', 'Mammalian': 'Mammalian', 'Microbial': 'Microbial', 'Plant': 'Plant', 'Synthetic/Industrial chemical': 'Synthetic/Industrial chemical', }, 'Database Source': { 'Human Metabolome Database (HMDB)': 'HMDB', 'Madison Metabolomics Consortium Database (MMCD)': 'MMCD', }, 'Sample pH': { '10.00 - 10.99': 'ph7', '7.00 - 9.99': 'ph7', '6.00 - 6.99': 'ph6', '5.00 - 5.99': 'ph5', '4.00 - 4.99': 'ph4', '3.00 - 3.99': 'ph3', }, 'Solvent': { 'All': 'all', 'Water': 'water', 'CDCl3': 'cdcl3', 'CD3OD': '5d3od', '5% DMSO': '5dmso', }, 'Frequency': { 'All': 'all', '600 MHz': '600', '500 MHz': '500', '400 MHz': '400', }, 'Method': { 'MH1: Highest number of matched peaks': 'HighestNumber', 'MH2: Highest number of matched peaks with shift tolerance': 'HighestNumberNeighbourhood', 'MH3: Greedy selection of metabolites with disjoint peaks': 'Greedy2', 'MH4: Highest number of matched peaks with intensities': 'HighestNumberHeights', 'MH5: Greedy selection of metabolites with disjoint peaks and heights': 'Greedy2Heights', }, } def __init__(self, *args, **kwargs): super(MetaboHunter, self).__init__(*args, **kwargs) self.setWindowTitle('MetaboHunter') # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'Metabotype': 'All', 'Database Source': 'HMDB', 'Sample pH': 'ph7', 'Solvent': 'water', 'Frequency': 'all', 'Method': 'HighestNumberNeighbourhood', 'Noise Threshold': 0.0001, 'Confidence Threshold': 0.5, 'Tolerance': 0.1, 'convert_hmdb_ids_to_names': True, }) self.lw_combos = {} for o in ['Metabotype', 'Database Source', 'Sample pH', 'Solvent', 'Frequency', 'Method']: row = QVBoxLayout() cl = QLabel(o) cb = QComboBox() cb.addItems(list(self.options[o].keys())) row.addWidget(cl) row.addWidget(cb) self.config.add_handler(o, cb, self.options[o]) self.layout.addLayout(row) row = QGridLayout() self.lw_spin = {} for n, o in enumerate(['Noise Threshold', 'Confidence Threshold', 'Tolerance']): cl = QLabel(o) cb = QDoubleSpinBox() cb.setDecimals(4) cb.setRange(0, 1) cb.setSingleStep(0.01) cb.setValue(float(self.config.get(o))) row.addWidget(cl, 0, n) row.addWidget(cb, 1, n) self.config.add_handler(o, cb) self.layout.addLayout(row) row = QHBoxLayout() row.addWidget(QLabel("Convert HMDB IDs to chemical names?")) conv = QCheckBox() self.config.add_handler('convert_hmdb_ids_to_names', conv) row.addWidget(conv) self.layout.addLayout(row) self.dialogFinalise()
class Automaton(QStandardItem): def __init__(self, *args, **kwargs): super(Automaton, self).__init__(*args, **kwargs) self.setData(self, Qt.UserRole) self.watcher = QFileSystemWatcher() self.timer = QTimer() self.watch_window = {} self.latest_run = {} self.is_running = False self.config = ConfigManager() self.config.set_defaults({ 'mode': MODE_WATCH_FOLDER, 'is_active': True, 'trigger_hold': 1, 'notebook_paths': '', 'output_path': '{home}/{notebook_filename}_{datetime}_', 'output_format': 'html', 'watched_files': [], 'watched_folder': '', 'watch_window': 15, 'iterate_watched_folder': True, 'iterate_wildcard': '.csv', 'timer_seconds': 60, }) self.runner = None self.lock = None self.latest_run = { 'timestamp': None, 'success': None, } # Set up all the triggers self.watcher.fileChanged.connect(self.file_trigger_accumulator) self.watcher.directoryChanged.connect(self.trigger) self.timer.timeout.connect(self.trigger) def startup(self): if self.config.get('is_active') == False: return False if self.config.get('mode') == MODE_TIMER: self.timer.setInterval(self.config.get('timer_seconds') * 1000) self.timer.start() elif self.config.get('mode') == MODE_WATCH_FILES: current_paths = self.watcher.files() + self.watcher.directories() if current_paths: self.watcher.removePaths(current_paths) self.watch_window = {} self.watcher.addPaths(self.config.get('watched_files')) elif self.config.get('mode') == MODE_WATCH_FOLDER: current_paths = self.watcher.files() + self.watcher.directories() if current_paths: self.watcher.removePaths(current_paths) self.watcher.addPath(self.config.get('watched_folder')) def shutdown(self): if self.config.get('mode') == MODE_TIMER: self.timer.stop() elif self.config.get('mode') == MODE_WATCH_FILES or self.config.get( 'mode') == MODE_WATCH_FOLDER: current_paths = self.watcher.files() + self.watcher.directories() if current_paths: self.watcher.removePaths(current_paths) self.watch_window = {} def load_notebook(self, filename): try: with open(filename) as f: nb = reads(f.read(), 'json') except: return None else: return nb def file_trigger_accumulator(self, f): # Accumulate triggers from changed files: if 3 files are specified to watch # we only want to fire once _all_ files have changed (within the given watch window) current_time = datetime.now() self.watch_window[f] = current_time # timestamp self.watch_window = { k: v for k, v in self.watch_window.items() if current_time - timedelta(seconds=self.config.get('watch_window')) < v } if set(self.watch_window.keys()) == set(self.watcher.files() + self.watcher.directories()): self.trigger() def trigger(self, e=None): if self.config.get('is_active') == False: return False # Filesystemwatcher triggered # Get the file and folder information; make available in vars object for run if self.lock is None: self.is_running = True self.update() self.lock = QTimer.singleShot( self.config.get('trigger_hold') * 1000, self.run) # Shutdown so we don't get in infinite loops self.shutdown() def run(self, vars={}): default_vars = { 'home': os.path.expanduser('~'), 'version': VERSION_STRING, } default_vars_and_config = dict( list(default_vars.items()) + list(self.config.config.items())) if self.runner == None: # Postpone init to trigger for responsiveness on add/remove self.runner = NotebookRunner(None, pylab=True, mpl_inline=True) if self.config.get('mode') == MODE_WATCH_FOLDER and self.config.get( 'iterate_watched_folder'): for (dirpath, dirnames, filenames) in os.walk(self.config.get('watched_folder')): break filenames = [ f for f in filenames if self.config.get('iterate_wildcard') in f ] logging.info('Watched folder contains %d files; looping' % len(filenames)) # Filenames contains the list of files in the folder else: filenames = [None] self.latest_run['timestamp'] = datetime.now() try: for f in filenames: now = datetime.now() current_vars = { 'datetime': now.strftime("%Y-%m-%d %H.%M.%S"), 'date': now.date().strftime("%Y-%m-%d"), 'time': now.time().strftime("%H.%M.%S"), 'filename': f, } vars = dict( list(default_vars_and_config.items()) + list(current_vars.items())) for nb_path in self.config.get('notebook_paths'): nb = self.load_notebook(nb_path) if nb: # Add currently running notebook path to vars vars['notebook_path'] = nb_path vars['notebook_filename'] = os.path.basename(nb_path) vars['output_path'] = self.config.get( 'output_path').format(**vars) parent_folder = os.path.dirname(vars['output_path']) if parent_folder: try: utils.mkdir_p(parent_folder) except: # Can't create folder self.latest_run['success'] = False raise self.run_notebook(nb, vars) else: raise NotebookNotFound(nb_path) except: self.latest_run['success'] = False traceback.print_exc() exctype, value = sys.exc_info()[:2] logging.error("%s\n%s\n%s" % (exctype, value, traceback.format_exc())) finally: self.is_running = False self.lock = None self.update() # Restart self.startup() def run_notebook(self, nb, vars={}): if len(nb['worksheets']) == 0: nb['worksheets'] = [NotebookNode({'cells': [], 'metadata': {}})] start = nb['worksheets'][0]['cells'] start.insert( 0, Struct( **{ 'cell_type': 'code', 'language': 'python', 'outputs': [], 'collapsed': False, 'prompt_number': -1, 'input': 'qtipy=%s' % vars, 'metadata': {}, })) self.runner.nb = nb try: self.runner.run_notebook() except: self.latest_run['success'] = False raise else: self.latest_run['success'] = True finally: ext = dict( html='html', slides='slides', latex='latex', markdown='md', python='py', rst='rst', ) output, resources = IPyexport( IPyexporter_map[self.config.get('output_format')], self.runner.nb) output_path = vars['output_path'] + 'notebook.%s' % ext[ self.config.get('output_format')] logging.info("Exporting updated notebook to %s" % output_path) with open(output_path, "w") as f: f.write(output) def update(self): global _w _w.viewer.update(self.index())
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setWindowTitle('PyQtConfig Demo') self.vis = ConfigManager() # # CHOICE_A = 1 # CHOICE_B = 2 # CHOICE_C = 3 # CHOICE_D = 4 # # map_dict = { # 'Choice A': CHOICE_A, # 'Choice B': CHOICE_B, # 'Choice C': CHOICE_C, # 'Choice D': CHOICE_D, # } self.vis.set_defaults({ 'text1': 'hello1', 'text2': 'hello2', 'text3': 'hello3', 'text4': 'hello4', }) # self.vis.set_defaults({ # 'number': 13, # 'text': 'hello', # 'active': True, # 'combo': CHOICE_C, # }) gd = QGridLayout() # # sb = QSpinBox() # gd.addWidget(sb, 0, 1) # self.vis.add_handler('number', sb) # # te = QLineEdit() # gd.addWidget(te, 1, 1) # self.vis.add_handler('text', te) # # cb = QCheckBox() # gd.addWidget(cb, 2, 1) # self.vis.add_handler('active', cb) # # cmb = QComboBox() # cmb.addItems(map_dict.keys()) # gd.addWidget(cmb, 3, 1) # self.vis.add_handler('combo', cmb, mapper=map_dict) # # self.current_config_output = QTextEdit() # gd.addWidget(self.current_config_output, 0, 3, 3, 1) i=0 print self.vis.as_dict() for item in self.vis.as_dict().keys(): te = QLineEdit() te.setEnabled(False) self.vis.add_handler(item,te) gd.addWidget(te,i,1) i=i+1 # self.vis.updated.connect(self.show_config) # self.show_config() self.window = QWidget() self.window.setLayout(gd) self.setCentralWidget(self.window)
class AnnotateClasses(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotateClasses, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Classes') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Sample classes') vbox = QVBoxLayout() # Populate the list boxes self.lw_classes = QListWidgetAddRemove() self.lw_classes.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_classes) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_class = QLineEdit() addc = QPushButton('Add') addc.clicked.connect(self.onClassAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onClassAdd) loadc = QPushButton('Import from file') loadc.setIcon( QIcon( os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onClassImport) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_class) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vboxh.addWidget(loadc) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/sample_classes', self.lw_classes, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onClassAdd(self): c = self.config.get( 'annotation/sample_classes' )[:] # Create new list to force refresh on reassign c.append((self.add_label.text(), self.add_class.text())) self.config.set('annotation/sample_classes', c) def onClassRemove(self): i = self.lw_classes.removeItemAt(self.lw_classes.currentRow()) # c = self.map_list_fwd(i.text()) def onClassImport(self): filename, _ = QFileDialog.getOpenFileName( self.parent(), 'Load classifications from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)" ) if filename: c = self.config.get( 'annotation/sample_classes' )[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append(row[:2]) self.config.set('annotation/sample_classes', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%s" % tuple(x) self.fwd_map_cache[s] = x return s
class ToolBase(QObject): ''' Base tool definition for inclusion in the UI. Define specific config settings; attach a panel widget for configuration. ''' is_manual_runnable = True is_auto_runnable = False is_auto_rerunnable = True is_disableable = True progress = pyqtSignal(float) status = pyqtSignal(str) complete = pyqtSignal() config_panel_size = 150 view = None def __init__(self, parent, *args, **kwargs): super(ToolBase, self).__init__(parent, *args, **kwargs) self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'auto_run_on_config_change': True }) self.current_status = 'inactive' self.config.updated.connect(self.auto_run_on_config_change) self.buttonBar = QWidget() self.configPanels = QWidget() self.configLayout = QVBoxLayout() self.configLayout.setContentsMargins(0,0,0,0) self.configPanels.setLayout(self.configLayout) self._previous_config_backup_ = {} self._worker_thread_ = None self._worker_thread_lock_ = False self.view = MplView(self.parent()) self.setup() self.current_progress = 0 self.progress.connect(self.progress_callback) self.status.connect(self.status_callback) def setup(self): self.data = { 'data': None, } self.view.figure.clear() self.view.redraw() self.status.emit('inactive' if self.current_status == 'inactive' else 'ready') def addConfigPanel(self, panel): self.configLayout.addWidget( panel(self) ) def addButtonBar(self, buttons): ''' Create a button bar Supplied with a list of QPushButton objects (already created using helper stubs; see below) :param buttons: :return: ''' btnlayout = QHBoxLayout() btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) for btn in buttons: btnlayout.addWidget(btn) self.configLayout.addLayout(btnlayout) btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) def run_manual(self): pass def disable(self): self.status.emit('inactive') self.item.setFlags(Qt.NoItemFlags) def reset(self): self.config.set_many( self.config.defaults ) def undo(self): self.config.set_many(self._config_backup_) def defaultButtons(self): buttons = [] reset = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Reset to defaults') reset.setToolTip('Reset to defaults') reset.pressed.connect(self.reset) buttons.append(reset) apply = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'play.png')), 'Apply') apply.setToolTip('Apply current settings to spectra') apply.pressed.connect(self.run_manual) buttons.append(apply) return buttons def enable(self): if self.current_status == 'inactive': self.current_status = 'ready' self.status.emit('ready') self.item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) def activate(self): self.parent().current_tool = self self.enable() self._config_backup_ = self.config.as_dict() self._refresh_plot_timer_ = QTimer.singleShot(0, self.plot) if self.view: self.parent().viewStack.setCurrentWidget(self.view) self.parent().configstack.setCurrentWidget(self.configPanels) self.parent().configstack.setMaximumHeight(self.config_panel_size) def set_active(self, active): self.config.set('is_active', active) def get_previous_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n-1::-1]: if tool.current_status != 'inactive': return tool else: return None def get_next_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n+1:]: if tool.current_status != 'inactive': return tool else: return None def get_previous_data(self): t = self.get_previous_tool() if t and 'data' in t.data: return t.data['data'] else: return None def plot(self, **kwargs): pass #if 'spc' in self.data: # self.parent().spectraViewer.plot(self.data['spc'], **kwargs) def get_plotitem(self): return self.parent().spectraViewer.spectraViewer.plotItem def auto_run_on_config_change(self): if self.is_auto_runnable and self.current_status == 'ready' and self.config.get('auto_run_on_config_change'): self.run_manual() def run(self, fn): ''' Run the target function, passing in the current spectra, and config settings (as dict) :param fn: :return: ''' if self._worker_thread_lock_: return False # Can't run self.progress.emit(0) self.status.emit('active') data = self.get_previous_data() self._worker_thread_lock_ = True kwargs = { 'config': self.config.as_dict(), 'progress_callback': self.progress.emit, } if data: kwargs.update(deepcopy(data)) # Close any open figures; ensure new axes. plt.close() self._worker_thread_ = Worker(fn = fn, **kwargs) self._worker_thread_.signals.finished.connect(self.finished) self._worker_thread_.signals.result.connect(self.result) self._worker_thread_.signals.error.connect(self.error) self.parent().threadpool.start(self._worker_thread_) def error(self, error): self.progress.emit(1.0) self.status.emit('error') logging.error(error) self._worker_thread_lock_ = False def result(self, result): # Apply post-processing if 'fig' in result and result['fig']: fig = result['fig'] fig.set_size_inches(self.view.get_size_inches(fig.get_dpi())) fig.set_tight_layout(False) fig.tight_layout(pad=0.5, rect=[0.25, 0.10, 0.75, 0.90]) self.view.figure = fig self.view.redraw() self.data = result self.plot() self.progress.emit(1) self.status.emit('complete') def finished(self): # Cleanup self._worker_thread_lock_ = False self.complete.emit() def progress_callback(self, progress): self.current_progress = progress self.item.setData(Qt.UserRole + 2, progress) def status_callback(self, status): self.current_status = status self.item.setData(Qt.UserRole + 3, status)
# Application settings settings = QSettingsManager() settings.set_defaults({ 'core/is_setup': False, 'core/current_version': '0.0.1', 'core/latest_version': '0.0.1', 'core/last_time_version_checked': 0, 'core/offered_registration': False, }) # GLobal processing settings (e.g. peak annotations, class groups, etc.) config = ConfigManager() config.set_defaults({ 'annotation/peaks': [], # [(label, peak, tolerance) 'annotation/sample_classes': {}, # {'sample_id': 'class_name' } 'annotation/class_colors': {}, # {'class_name': color } }) STATUS_QCOLORS = { 'ready': QColor(255, 255, 255), 'active': QColor(255, 165, 0), 'error': QColor(255, 0, 0), 'inactive': QColor(255, 255, 255), 'complete': QColor(0, 255, 0), } def _get_QLineEdit(self): return self._get_map(self.text())
class ToolBase(QObject): ''' Base tool definition for inclusion in the UI. Define specific config settings; attach a panel widget for configuration. ''' is_manual_runnable = True is_auto_runnable = True is_auto_rerunnable = True is_disableable = True progress = pyqtSignal(float) status = pyqtSignal(str) config_panel_size = 250 view_widget = 'SpectraViewer' def __init__(self, parent, *args, **kwargs): super(ToolBase, self).__init__(parent, *args, **kwargs) self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'is_active': True, 'auto_run_on_config_change': True }) self.config.updated.connect(self.auto_run_on_config_change) self.buttonBar = QWidget() self.configPanels = QWidget() self.configLayout = QVBoxLayout() self.configLayout.setContentsMargins(0, 0, 0, 0) self.configPanels.setLayout(self.configLayout) self._previous_config_backup_ = {} self._worker_thread_ = None self._worker_thread_lock_ = False self.data = { 'spc': None, } self.current_status = 'ready' self.current_progress = 0 self.progress.connect(self.progress_callback) self.status.connect(self.status_callback) def addConfigPanel(self, panel): self.configLayout.addWidget(panel(self)) def addButtonBar(self, buttons): ''' Create a button bar Supplied with a list of QPushButton objects (already created using helper stubs; see below) :param buttons: :return: ''' btnlayout = QHBoxLayout() btnlayout.addSpacerItem( QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) for btn in buttons: btnlayout.addWidget(btn) self.configLayout.addLayout(btnlayout) btnlayout.addSpacerItem( QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) def run_manual(self): pass def disable(self): self.status.emit('inactive') self.config.set('is_active', False) self.item.setFlags(Qt.NoItemFlags) def reset(self): self.config.set_many(self.config.defaults) def undo(self): self.config.set_many(self._config_backup_) def deftaultButtons(self): buttons = [] if self.is_disableable: disable = QPushButton( QIcon(os.path.join(utils.scriptdir, 'icons', 'cross.png')), 'Disable') disable.setToolTip('Disable this tool') disable.pressed.connect(self.disable) buttons.append(disable) reset = QPushButton( QIcon( os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Reset to defaults') reset.setToolTip('Reset to defaults') reset.pressed.connect(self.reset) buttons.append(reset) undo = QPushButton( QIcon( os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Undo') undo.setToolTip('Undo recent changes') undo.pressed.connect(self.undo) buttons.append(undo) if self.is_auto_runnable: auto = QPushButton( QIcon(os.path.join(utils.scriptdir, 'icons', 'lightning.png')), 'Auto') auto.setToolTip('Auto-update spectra when settings change') auto.setCheckable(True) auto.pressed.connect(self.run_manual) self.config.add_handler('auto_run_on_config_change', auto) buttons.append(auto) if self.is_manual_runnable: apply = QPushButton( QIcon(os.path.join(utils.scriptdir, 'icons', 'play.png')), 'Apply') apply.setToolTip('Apply current settings to spectra') apply.pressed.connect(self.run_manual) buttons.append(apply) return buttons def enable(self): if self.current_status == 'inactive': self.status.emit('ready') self.item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.config.set('is_active', True) def activate(self): self.parent().current_tool = self self.enable() self._config_backup_ = self.config.as_dict() self._refresh_plot_timer_ = QTimer.singleShot(0, self.plot) self.parent().viewStack.setCurrentWidget(self.parent().spectraViewer) self.parent().configstack.setCurrentWidget(self.configPanels) self.parent().configstack.setMaximumHeight(self.config_panel_size) def set_active(self, active): self.config.set('is_active', active) def get_previous_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n - 1::-1]: if tool.current_status != 'inactive': return tool else: return None def get_previous_spc(self): t = self.get_previous_tool() if t: return t.data['spc'] else: return None def plot(self, **kwargs): if 'spc' in self.data: self.parent().spectraViewer.plot(self.data['spc'], **kwargs) def get_plotitem(self): return self.parent().spectraViewer.spectraViewer.plotItem def auto_run_on_config_change(self): pass #if self.is_auto_runnable and self.config.get('is_active') and self.config.get('auto_run_on_config_change'): # self.run_manual() def run(self, fn): ''' Run the target function, passing in the current spectra, and config settings (as dict) :param fn: :return: ''' if self._worker_thread_lock_: return False # Can't run self.progress.emit(0) self.status.emit('active') spc = self.get_previous_spc() self._worker_thread_lock_ = True print(self.config.as_dict()) self._worker_thread_ = Worker(fn=fn, **{ 'spc': deepcopy(spc), 'config': self.config.as_dict(), 'progress_callback': self.progress.emit, }) self._worker_thread_.signals.finished.connect(self.finished) self._worker_thread_.signals.result.connect(self.result) self._worker_thread_.signals.error.connect(self.error) self.parent().threadpool.start(self._worker_thread_) def error(self, error): self.progress.emit(1.0) self.status.emit('error') logging.error(error) self._worker_thread_lock_ = False def result(self, result): self.progress.emit(1) self.status.emit('complete') # Apply post-processing if 'spc' in result: result['spc'] = self.post_process_spc(result['spc']) self.data = result self.plot() def finished(self): # Cleanup self._worker_thread_lock_ = False def progress_callback(self, progress): self.current_progress = progress self.item.setData(Qt.UserRole + 2, progress) def status_callback(self, status): self.current_status = status self.item.setData(Qt.UserRole + 3, status) def post_process_spc(self, spc): ''' Apply post-processing to the spectra before loading into the data store, e.g. for outlier detection, stats etc. :param spc: :return: ''' # Outliers def identify_outliers(data, m=2): return abs(data - np.mean(data, axis=0)) < (m * np.std(data, axis=0)) # Identify outliers on a point by point basis. Count up 'outliers' and score ratio of points that are # outliers for each specra > 5% (make this configurable) is an outlier. spc.outliers = np.sum(~identify_outliers(spc.data), axis=1) / float( spc.data.shape[1]) return spc
class QGraphicsSceneExtend(QGraphicsScene): def __init__(self, parent, *args, **kwargs): super(QGraphicsSceneExtend, self).__init__(parent, *args, **kwargs) self.m = parent.m self.config = ConfigManager() # These config settings are transient (ie. not stored between sessions) self.config.set_defaults({ 'mode': EDITOR_MODE_NORMAL, 'font-family': 'Arial', 'font-size': '12', 'text-bold': False, 'text-italic': False, 'text-underline': False, 'text-color': '#000000', 'color-border': None, # '#000000', 'color-background': None, }) # Pre-set these values (will be used by default) self.config.set('color-background', '#5555ff') self.background_image = QImage(os.path.join(utils.scriptdir, 'icons', 'grid100.png')) if settings.get('Editor/Show_grid'): self.showGrid() else: self.hideGrid() self.mode = EDITOR_MODE_NORMAL self.mode_current_object = None self.annotations = [] def mousePressEvent(self, e): if self.config.get('mode') != EDITOR_MODE_NORMAL: for i in self.selectedItems(): i.setSelected(False) if self.config.get('mode') == EDITOR_MODE_TEXT: tw = AnnotationTextItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_REGION: tw = AnnotationRegionItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_ARROW: tw = AnnotationRegionItem(position=e.scenePos()) self.addItem(tw) self.mode_current_object = tw tw._createFromMousePressEvent(e) tw.importStyleConfig(self.config) self.annotations.append(tw) else: for i in self.selectedItems(): i.setSelected(False) super(QGraphicsSceneExtend, self).mousePressEvent(e) def mouseMoveEvent(self, e): if self.config.get('mode') == EDITOR_MODE_TEXT and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) elif self.config.get('mode') == EDITOR_MODE_REGION and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) else: super(QGraphicsSceneExtend, self).mouseMoveEvent(e) def mouseReleaseEvent(self, e): if self.config.get('mode'): self.mode_current_object.setSelected(True) self.mode_current_object.setFocus() self.config.set('mode', EDITOR_MODE_NORMAL) self.mode_current_object = None super(QGraphicsSceneExtend, self).mouseReleaseEvent(e) def showGrid(self): self.setBackgroundBrush(QBrush(self.background_image)) def hideGrid(self): self.setBackgroundBrush(QBrush(None)) def onSaveAsImage(self): filename, _ = QFileDialog.getSaveFileName(self.m, 'Save current figure', '', "Tagged Image File Format (*.tif);;\ Portable Network Graphics (*.png)") if filename: self.saveAsImage(filename) def saveAsImage(self, f): self.image = QImage(self.sceneRect().size().toSize(), QImage.Format_ARGB32) self.image.fill(Qt.white) painter = QPainter(self.image) self.render(painter) self.image.save(f) def addApp(self, app, position=None): i = ToolItem(self, app, position=position) self.addItem(i) #i.onShow() return i def removeApp(self, app): i = app.editorItem i.hide() self.removeItem(i) app.editorItem = None def dragEnterEvent(self, e): if e.mimeData().hasFormat('application/x-pathomx-app') or e.mimeData().hasFormat('text/uri-list'): e.accept() else: e.ignore() def dragMoveEvent(self, e): e.accept() def dropEvent(self, e): scenePos = e.scenePos() - QPointF(32, 32) if e.mimeData().hasFormat('application/x-pathomx-app'): try: app_id = str(e.mimeData().data('application/x-pathomx-app'), 'utf-8') # Python 3 except: app_id = str(e.mimeData().data('application/x-pathomx-app')) # Python 2 e.setDropAction(Qt.CopyAction) a = app_launchers[app_id](self.m, position=scenePos, auto_focus=False) #self.centerOn(a.editorItem) e.accept() elif e.mimeData().hasFormat('text/uri-list'): for ufn in e.mimeData().urls(): fn = ufn.path() fnn, ext = os.path.splitext(fn) ext = ext.strip('.') if ext in file_handlers: a = file_handlers[ext](position=scenePos, auto_focus=False, filename=fn) self.centerOn(a.editorItem) e.accept() def getXMLAnnotations(self, root): # Iterate over the entire set (in order) creating a XML representation of the MatchDef and Style for annotation in self.annotations: ase = et.SubElement(root, "Annotation") ase.set('type', type(annotation).__name__) ase.set('x', str(annotation.x())) ase.set('y', str(annotation.y())) ase.set('width', str(annotation.rect().width())) ase.set('height', str(annotation.rect().height())) if hasattr(annotation, 'text'): text = et.SubElement(ase, "Text") text.text = annotation.text.toPlainText() ase = annotation.config.getXMLConfig(ase) return root def setXMLAnnotations(self, root): ANNOTATION_TYPES = { 'AnnotationTextItem': AnnotationTextItem, 'AnnotationRegionItem': AnnotationRegionItem, } for ase in root.findall('Annotation'): # Validate the class definition before creating it if ase.get('type') in ANNOTATION_TYPES: pos = QPointF(float(ase.get('x')), float(ase.get('y'))) aobj = ANNOTATION_TYPES[ase.get('type')](position=pos) aobj.setRect(QRectF(0, 0, float(ase.get('width')), float(ase.get('height')))) to = ase.find('Text') if to is not None: aobj.text.setPlainText(to.text) self.addItem(aobj) self.annotations.append(aobj) aobj.config.setXMLConfig(ase) aobj.applyStyleConfig()
class ToolBase(QObject): ''' Base tool definition for inclusion in the UI. Define specific config settings; attach a panel widget for configuration. ''' is_manual_runnable = True is_auto_runnable = True is_auto_rerunnable = True is_disableable = True progress = pyqtSignal(float) status = pyqtSignal(str) config_panel_size = 250 view_widget = 'SpectraViewer' def __init__(self, parent, *args, **kwargs): super(ToolBase, self).__init__(parent, *args, **kwargs) self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'is_active': True, 'auto_run_on_config_change': True }) self.config.updated.connect(self.auto_run_on_config_change) self.buttonBar = QWidget() self.configPanels = QWidget() self.configLayout = QVBoxLayout() self.configLayout.setContentsMargins(0,0,0,0) self.configPanels.setLayout(self.configLayout) self._previous_config_backup_ = {} self._worker_thread_ = None self._worker_thread_lock_ = False self.data = { 'spc': None, } self.current_status = 'ready' self.current_progress = 0 self.progress.connect(self.progress_callback) self.status.connect(self.status_callback) def addConfigPanel(self, panel): self.configLayout.addWidget( panel(self) ) def addButtonBar(self, buttons): ''' Create a button bar Supplied with a list of QPushButton objects (already created using helper stubs; see below) :param buttons: :return: ''' btnlayout = QHBoxLayout() btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) for btn in buttons: btnlayout.addWidget(btn) self.configLayout.addLayout(btnlayout) btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) def run_manual(self): pass def disable(self): self.status.emit('inactive') self.config.set('is_active', False) self.item.setFlags(Qt.NoItemFlags) def reset(self): self.config.set_many( self.config.defaults ) def undo(self): self.config.set_many(self._config_backup_) def deftaultButtons(self): buttons = [] if self.is_disableable: disable = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'cross.png')), 'Disable') disable.setToolTip('Disable this tool') disable.pressed.connect(self.disable) buttons.append(disable) reset = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Reset to defaults') reset.setToolTip('Reset to defaults') reset.pressed.connect(self.reset) buttons.append(reset) undo = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Undo') undo.setToolTip('Undo recent changes') undo.pressed.connect(self.undo) buttons.append(undo) if self.is_auto_runnable: auto = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'lightning.png')), 'Auto') auto.setToolTip('Auto-update spectra when settings change') auto.setCheckable(True) auto.pressed.connect(self.run_manual) self.config.add_handler('auto_run_on_config_change', auto) buttons.append(auto) if self.is_manual_runnable: apply = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'play.png')), 'Apply') apply.setToolTip('Apply current settings to spectra') apply.pressed.connect(self.run_manual) buttons.append(apply) return buttons def enable(self): if self.current_status == 'inactive': self.status.emit('ready') self.item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.config.set('is_active', True) def activate(self): self.parent().current_tool = self self.enable() self._config_backup_ = self.config.as_dict() self._refresh_plot_timer_ = QTimer.singleShot(0, self.plot) self.parent().viewStack.setCurrentWidget(self.parent().spectraViewer) self.parent().configstack.setCurrentWidget(self.configPanels) self.parent().configstack.setMaximumHeight(self.config_panel_size) def set_active(self, active): self.config.set('is_active', active) def get_previous_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n-1::-1]: if tool.current_status != 'inactive': return tool else: return None def get_previous_spc(self): t = self.get_previous_tool() if t: return t.data['spc'] else: return None def plot(self, **kwargs): if 'spc' in self.data: self.parent().spectraViewer.plot(self.data['spc'], **kwargs) def get_plotitem(self): return self.parent().spectraViewer.spectraViewer.plotItem def auto_run_on_config_change(self): pass #if self.is_auto_runnable and self.config.get('is_active') and self.config.get('auto_run_on_config_change'): # self.run_manual() def run(self, fn): ''' Run the target function, passing in the current spectra, and config settings (as dict) :param fn: :return: ''' if self._worker_thread_lock_: return False # Can't run self.progress.emit(0) self.status.emit('active') spc = self.get_previous_spc() self._worker_thread_lock_ = True print(self.config.as_dict()) self._worker_thread_ = Worker(fn = fn, **{ 'spc': deepcopy(spc), 'config': self.config.as_dict(), 'progress_callback': self.progress.emit, }) self._worker_thread_.signals.finished.connect(self.finished) self._worker_thread_.signals.result.connect(self.result) self._worker_thread_.signals.error.connect(self.error) self.parent().threadpool.start(self._worker_thread_) def error(self, error): self.progress.emit(1.0) self.status.emit('error') logging.error(error) self._worker_thread_lock_ = False def result(self, result): self.progress.emit(1) self.status.emit('complete') # Apply post-processing if 'spc' in result: result['spc'] = self.post_process_spc(result['spc']) self.data = result self.plot() def finished(self): # Cleanup self._worker_thread_lock_ = False def progress_callback(self, progress): self.current_progress = progress self.item.setData(Qt.UserRole + 2, progress) def status_callback(self, status): self.current_status = status self.item.setData(Qt.UserRole + 3, status) def post_process_spc(self, spc): ''' Apply post-processing to the spectra before loading into the data store, e.g. for outlier detection, stats etc. :param spc: :return: ''' # Outliers def identify_outliers(data, m=2): return abs(data - np.mean(data, axis=0)) < (m * np.std(data,axis=0)) # Identify outliers on a point by point basis. Count up 'outliers' and score ratio of points that are # outliers for each specra > 5% (make this configurable) is an outlier. spc.outliers = np.sum( ~identify_outliers(spc.data), axis=1 ) / float(spc.data.shape[1]) return spc