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 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 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 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 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 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 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)
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 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 MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, *args, **kwargs): self.LUXEMBOURG = "LUXEMBOURG" self.MONACO = "MONACO" self.NASSAU = "NASSAU" self.GERMANY = "GERMANY" QtWidgets.QMainWindow.__init__(self, *args, **kwargs) self.title = "ARIS - JIRA Interface GUI" self.left, self.top, self.width, self.height = (0, 30, 1600, 480) self.config = ConfigManager() self.aris_config = self.open_conf_lux() self.apps = os.path.join(os.path.dirname(os.getcwd()), self.aris_config["apps"][0]) self.aris_config_no_tooltips = { key: val[0] for (key, val) in self.aris_config.items() } self.aris_config_list = [(key, value[1]) for key, value in self.aris_config.items()] self.initUI(self) self.statusBar().showMessage("Ready.") self.scrape_ARIS_widget.clicked.connect(self.scrape_ARIS) self.create_TC_widget.clicked.connect(self.create_TC) self.sync_TC_widget.clicked.connect(self.sync_TC) self.config.updated.connect(self.show_config) self.config.updated.connect(self.save_config) self.cmb.activated[str].connect(self.load_preconfigured_json) self.event_stop = threading.Event() QtWidgets.QShortcut("Ctrl+C", self, activated=self.end_button_func) def load_preconfigured_json(self, text): if text == "MONACO": self.config.config = { key: val[0] for (key, val) in self.open_conf_mco().items() } self.save_config() self.show_config() elif text == "LUXEMBOURG": self.config.config = { key: val[0] for (key, val) in self.open_conf_lux().items() } self.save_config() self.show_config() elif text == "NASSAU": self.config.config = { key: val[0] for (key, val) in self.open_conf_nas().items() } self.save_config() self.show_config() elif text == "GERMANY": self.config.config = { key: val[0] for (key, val) in self.open_conf_ger().items() } self.save_config() self.show_config() def scrape_ARIS(self) -> None: self.would_like_to_clear_everything_scrape() if self.config.config["would_like_to_clear_everything_scrape"] is False: self.would_like_to_delete_all_xls_files_from_temp_and_download() self.would_like_delete_mapping_file() self.would_like_delete_pickle() self.would_like_to_skip_url_retrieval() self.save_config() # Instantiate and feed the progress bar to the scraper instance to get real time data self.statusBar().showMessage( "Program paused, please refer to the console") runnable = scrape_aris.Scrape_ARIS(self.progressBar, self.statusBar()) # Execute QtCore.QThreadPool.globalInstance().start(runnable) def sync_TC(self) -> None: # Instantiate and feed the progress bar to the scraper instance to get real time data runnable = sync.SyncJIRA(self.progressBar, self.statusBar()) # Execute QtCore.QThreadPool.globalInstance().start(runnable) def create_TC(self) -> None: # notifications self.would_like_to_clear_everything_create() if self.config.config["would_like_to_clear_everything_create"] is False: self.would_like_delete_json_dump_file() self.would_like_delete_xlsx_files() self.save_config() # Instantiate and feed the progress bar to the creator instance to get real time data self.statusBar().showMessage( "Program paused, please refer to the console") runnable = create_tcs.CreateTCS(self.progressBar, self.statusBar()) # Execute QtCore.QThreadPool.globalInstance().start(runnable) @QtCore.pyqtSlot() def save_config(self) -> None: """ Once edited, the config file is stored in a yml file that will be used by the apps """ with open("generated_config_aris.yml", "w") as file: yaml.dump(self.config.config, file) @QtCore.pyqtSlot() def end_button_func(self): self.event_stop.set() def show_config(self) -> None: self.current_config_output.setText(str(self.config.as_dict())) def open_conf_lux(self) -> Dict[str, Tuple[str, str]]: """ load LUX default parameters """ return { "combo": (self.LUXEMBOURG, "Choose a configuration pre-set or manually configure it through the input boxes below" ), "FirefoxBinary": ("FirefoxPortableESR\\App\\Firefox64\\firefox.exe", "firefox binaries location"), "Geckodriver_binary": ("FirefoxPortableESR\\geckodriver\\geckodriver.exe", "firefox driver location"), "search_value": ("lux", "search term that you will search for on ARIS"), "log_name": ("aris lux.log", "log name of all scripts in this app"), "mapping_file_name": ("mapping -lux.xlsx", "db that holds the 1 to 1 relationship between ARIS id and JIRA id" ), "target": ("https://jira-uat4.com/", "url for ticket creation and sync"), "config_path": ("C:\\Users\\u46022\\Documents", "path that holds a configuration.py file that holds sensitive credentials" ), "master": ("ARIS", "master path that holds all of the downloads and the mapping file" ), "apps": ("ARIS_apps", "master path that holds all of the scripts logs, intermediary pickles as well as consolidated xls and csv files" ), "json_dump_file": ("data lux.json", "Pickle that holds all of the consolidated data. This is an important input for the sync script" ), "reporting": ("reporting", "path to reporting folder"), "process_url": ("http://srp07000wn.com/#default/item/", "specific ARIS url to consult a process"), "search_url": ("http://srp07000wn.com/#default/search", "ARIS url to perform a search"), "pickle_file": ("liste -lux.txt", "pickle that holds all the urls we should visit"), "download_folder": ("download lux", "download master folder that holds the renamed processes"), "temp_folder": ("temp", "download master folder that holds the soon to be renamed processes" ), "download_folder": ("download lux", "download master folder that holds the renamed processes"), "test_path": ("/Testing/LUX_processes", "specific xray field that creates a tree for the test storage"), "JIRA_project": ("T2L", "which jria project should the ticket be created in?"), "consolidated_file_name": ("Consolidated_lux", "file name of the consolidated csv and xlsx ouput"), } def open_conf_mco(self) -> Dict[str, Tuple[str, str]]: """ load MCO default parameters """ return { "combo": (self.MONACO, "Choose a configuration pre-set or manually configure it through the input boxes below" ), "FirefoxBinary": ("FirefoxPortableESR\\App\\Firefox64\\firefox.exe", "firefox binaries location"), "Geckodriver_binary": ("FirefoxPortableESR\\geckodriver\\geckodriver.exe", "firefox driver location"), "search_value": ("mco", "search term that you will search for on ARIS"), "log_name": ("aris mco.log", "log name of all scripts in this app"), "mapping_file_name": ("mapping -mco.xlsx", "db that holds the 1 to 1 relationship between ARIS id and JIRA id" ), "target": ("https://jira-uat4.com/", "url for ticket creation and sync"), "config_path": ("C:\\Users\\u46022\\Documents", "path that holds a configuration.py file that holds sensitive credentials" ), "master": ("ARIS", "master path that holds all of the downloads and the mapping file" ), "apps": ("ARIS_apps", "master path that holds all of the scripts logs, intermediary pickles as well as consolidated xls and csv files" ), "json_dump_file": ("data mco.json", "Pickle that holds all of the consolidated data. This is an important input for the sync script" ), "reporting": ("reporting", "path to reporting folder"), "process_url": ("http://srp07000wn.com/#default/item/", "specific ARIS url to consult a process"), "search_url": ("http://srp07000wn.com/#default/search", "ARIS url to perform a search"), "pickle_file": ("liste -mco.txt", "pickle that holds all the urls we should visit"), "download_folder": ("download mco", "download master folder that holds the renamed processes"), "temp_folder": ("temp", "download master folder that holds the soon to be renamed processes" ), "download_folder": ("download mco", "download master folder that holds the renamed processes"), "test_path": ("/Testing/MCO_processes", "specific xray field that creates a tree for the test storage"), "JIRA_project": ("T2L", "which jria project should the ticket be created in?"), "consolidated_file_name": ("Consolidated_mco", "file name of the consolidated csv and xlsx ouput"), } def open_conf_nas(self) -> Dict[str, Tuple[str, str]]: """ load NASSAU default parameters """ return { "combo": (self.NASSAU, "Choose a configuration pre-set or manually configure it through the input boxes below" ), "FirefoxBinary": ("FirefoxPortableESR\\App\\Firefox64\\firefox.exe", "firefox binaries location"), "Geckodriver_binary": ("FirefoxPortableESR\\geckodriver\\geckodriver.exe", "firefox driver location"), "search_value": ("nas", "search term that you will search for on ARIS"), "log_name": ("aris nas.log", "log name of all scripts in this app"), "mapping_file_name": ("mapping -nas.xlsx", "db that holds the 1 to 1 relationship between ARIS id and JIRA id" ), "target": ("https://jira-uat4.com/", "url for ticket creation and sync"), "config_path": ("C:\\Users\\u46022\\Documents", "path that holds a configuration.py file that holds sensitive credentials" ), "master": ("ARIS", "master path that holds all of the downloads and the mapping file" ), "apps": ("ARIS_apps", "master path that holds all of the scripts logs, intermediary pickles as well as consolidated xls and csv files" ), "json_dump_file": ("data nas.json", "Pickle that holds all of the consolidated data. This is an important input for the sync script" ), "reporting": ("reporting", "path to reporting folder"), "process_url": ("http://srp07000wn.com/#default/item/", "specific ARIS url to consult a process"), "search_url": ("http://srp07000wn.com/#default/search", "ARIS url to perform a search"), "pickle_file": ("liste -nas.txt", "pickle that holds all the urls we should visit"), "download_folder": ("download nas", "download master folder that holds the renamed processes"), "temp_folder": ("temp", "download master folder that holds the soon to be renamed processes" ), "download_folder": ("download nas", "download master folder that holds the renamed processes"), "test_path": ("/Testing/NAS_processes", "specific xray field that creates a tree for the test storage"), "JIRA_project": ("T2L", "which jria project should the ticket be created in?"), "consolidated_file_name": ("Consolidated_nas", "file name of the consolidated csv and xlsx ouput"), } def open_conf_ger(self) -> Dict[str, Tuple[str, str]]: """ load GERMANY default parameters """ return { "combo": (self.GERMANY, "Choose a configuration pre-set or manually configure it through the input boxes below" ), "FirefoxBinary": ("FirefoxPortableESR\\App\\Firefox64\\firefox.exe", "firefox binaries location"), "Geckodriver_binary": ("FirefoxPortableESR\\geckodriver\\geckodriver.exe", "firefox driver location"), "search_value": ("ger", "search term that you will search for on ARIS"), "log_name": ("aris ger.log", "log name of all scripts in this app"), "mapping_file_name": ("mapping -ger.xlsx", "db that holds the 1 to 1 relationship between ARIS id and JIRA id" ), "target": ("https://jira-uat4.com/", "url for ticket creation and sync"), "config_path": ("C:\\Users\\u46022\\Documents", "path that holds a configuration.py file that holds sensitive credentials" ), "master": ("ARIS", "master path that holds all of the downloads and the mapping file" ), "apps": ("ARIS_apps", "master path that holds all of the scripts logs, intermediary pickles as well as consolidated xls and csv files" ), "json_dump_file": ("data ger.json", "Pickle that holds all of the consolidated data. This is an important input for the sync script" ), "reporting": ("reporting", "path to reporting folder"), "process_url": ("http://srp07000wn.com/#default/item/", "specific ARIS url to consult a process"), "search_url": ("http://srp07000wn.com/#default/search", "ARIS url to perform a search"), "pickle_file": ("liste -ger.txt", "pickle that holds all the urls we should visit"), "download_folder": ("download ger", "download master folder that holds the renamed processes"), "temp_folder": ("temp", "download master folder that holds the soon to be renamed processes" ), "download_folder": ("download ger", "download master folder that holds the renamed processes"), "test_path": ("/Testing/GER_processes", "specific xray field that creates a tree for the test storage"), "JIRA_project": ("T2L", "which jria project should the ticket be created in?"), "consolidated_file_name": ("Consolidated_ger", "file name of the consolidated csv and xlsx ouput"), } def would_like_delete_pickle(self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would you like to delete the pickle_file?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config["delete_pickle"] = True elif buttonReply == QtWidgets.QMessageBox.No: self.config.config["delete_pickle"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit() def would_like_to_skip_url_retrieval(self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would you like to skip url retrieval and jump straight into ARIS download?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config["skip_url_retrieval"] = True elif buttonReply == QtWidgets.QMessageBox.No: self.config.config["skip_url_retrieval"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit() def would_like_to_delete_all_xls_files_from_temp_and_download( self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would like to delete all xls files from temp and download?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config[ "delete_all_xls_files_from_temp_and_download"] = True elif buttonReply == QtWidgets.QMessageBox.No: self.config.config[ "delete_all_xls_files_from_temp_and_download"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit() def would_like_delete_mapping_file(self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would you like to delete the mapping_file?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config["delete_mapping_file"] = True elif buttonReply == QtWidgets.QMessageBox.No: self.config.config["delete_mapping_file"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit() def would_like_delete_json_dump_file(self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would you like to delete the json database file?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config["delete_json_dump_file"] = True elif buttonReply == QtWidgets.QMessageBox.No: self.config.config["delete_json_dump_file"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit() def would_like_delete_xlsx_files(self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would like to delete all xlsx files from the download folder?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config[ "delete_all_xlsx_files_from_temp_and_download"] = True elif buttonReply == QtWidgets.QMessageBox.No: self.config.config[ "delete_all_xlsx_files_from_temp_and_download"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit() def would_like_to_clear_everything_scrape(self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would you like to clear everything and start a fresh scrape?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config["would_like_to_clear_everything_scrape"] = True self.config.config["delete_mapping_file"] = False self.config.config[ "delete_all_xls_files_from_temp_and_download"] = False self.config.config["skip_url_retrieval"] = False self.config.config["delete_pickle"] = False elif buttonReply == QtWidgets.QMessageBox.No: self.config.config["would_like_to_clear_everything_scrape"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit() def would_like_to_clear_everything_create(self) -> None: assert isinstance(self.config.config, dict) buttonReply = QtWidgets.QMessageBox.question( self, self.title, "Would you like to clear everything and start a fresh test case creation?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if buttonReply == QtWidgets.QMessageBox.Yes: self.config.config["would_like_to_clear_everything_create"] = True self.config.config[ "delete_all_xlsx_files_from_temp_and_download"] = False self.config.config["delete_json_dump_file"] = False elif buttonReply == QtWidgets.QMessageBox.No: self.config.config["would_like_to_clear_everything_create"] = False elif buttonReply == QtWidgets.QMessageBox.Cancel: sys.exit()
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