class Env(QWidget): makePlot = Signal(int, object, object, name='makePlot') def __init__(self, comm, plot_spawner, parent=None): super(__class__, self).__init__(parent) self.setWindowTitle("Env") self.comm = comm self.srcBox = QLineEdit(self) self.srcSelect = QPushButton('Select', self) self.srcFilter = QPushButton('Filter', self) self.postName = QLabel('Entry name', self) self.postBox = QLineEdit(self) self.tabs = QTabWidget(self) self.tabs.addTab(TimePlot(0, self.comm, plot_spawner, self.tabs), "Mean v Time") self.tabs.addTab(ScanPlot(1, self.comm, plot_spawner, self.tabs), "Mean v Scan") self.button = QPushButton('Plot', self) self.button.clicked.connect(self.on_click) self.src = QGroupBox("Source Channel") self.src_layout = QHBoxLayout(self.src) self.src_layout.addWidget(self.srcBox) self.src_layout.addWidget(self.srcSelect) self.src_layout.addWidget(self.srcFilter) self.post = QHBoxLayout() self.post.addWidget(self.postName) self.post.addWidget(self.postBox) self.plot = QGroupBox("Plot Type") self.plot_layout = QVBoxLayout(self.plot) self.plot_layout.addWidget(self.tabs) self.env_layout = QVBoxLayout(self) self.env_layout.addWidget(self.src) self.env_layout.addLayout(self.post) self.env_layout.addWidget(self.plot) self.env_layout.addWidget(self.button) for i in range(self.tabs.count()): self.makePlot.connect(self.tabs.widget(i).request_plot) @Slot() def on_click(self): src = self.srcBox.text() post = self.postBox.text() if src: self.makePlot.emit(self.tabs.currentIndex(), src, post)
def get_child_tabs(tab_widget: QW.QTabWidget, parent_name: str) -> list: result = [] for i in range(tab_widget.count()): widget = tab_widget.widget(i) if isinstance(widget, QW.QTabWidget): # page of pages result.extend(get_child_tabs(widget, widget.GetName())) else: selectable_media_page = widget label = selectable_media_page.GetNameForMenu() if not query in label: continue primary_text = highlight_result_text(label, query) secondary_text = 'top level page' if not parent_name else "child of '" + escape( parent_name) + "'" result.append( QLocatorSearchResult(self.result_id_counter, 'thumbnails.png', 'thumbnails.png', True, [primary_text, secondary_text])) self.result_ids_to_pages[ self.result_id_counter] = selectable_media_page self.result_id_counter += 1 return result
class DetailedProgressWidget(QWidget): def __init__(self, parent, state_colors): super(DetailedProgressWidget, self).__init__(parent) self.setWindowTitle("Realization Progress") layout = QGridLayout(self) self.iterations = QTabWidget() self.state_colors = state_colors self.single_view = SingleTableView() self.single_view.setModel( SingleProgressModel(self.single_view, state_colors)) self.single_view_label = QLabel("Realization details") layout.addWidget(self.iterations, 1, 0) layout.addWidget(self.single_view_label, 2, 0) layout.addWidget(self.single_view, 3, 0) self.setLayout(layout) self.layout().setRowStretch(1, 1) self.layout().setRowStretch(3, 1) self.progress = None self.selected_realization = -1 self.current_iteration = -1 self.resize(parent.width(), parent.height()) self.progress = {} def set_progress(self, progress, iteration): self.progress = progress for i in progress: #create all detailed views if they havent been constructed yet if self.iterations.widget(i): continue detailed_progress_widget = DetailedProgress( self.state_colors, self) detailed_progress_widget.clicked.connect(self.show_selection) detailed_progress_widget.set_progress(progress[i], i) detailed_progress_widget.show() self.iterations.addTab(detailed_progress_widget, "Realizations for iteration {}".format(i)) current_progress_widget = self.iterations.widget(iteration) current_progress_widget.set_progress(progress[iteration], iteration) self.update_single_view() self.update() def show_selection(self, iens): if not self.progress: return self.current_iteration = self.iterations.currentIndex() for i in range(0, self.iterations.count()): if i == self.current_iteration: continue self.iterations.widget(i).selected_realization = -1 self.selected_realization = iens self.single_view_label.setText( "Realization id: {} in iteration {}".format( iens, self.current_iteration)) self.update_single_view() def update_single_view(self): if not self.single_view.isVisible(): return if not self.current_iteration in self.progress: return if not self.selected_realization in self.progress[ self.current_iteration]: return self.single_view.update_data( self.progress[self.current_iteration][self.selected_realization] [0], self.current_iteration, self.selected_realization)
class ImportWizard(BaseDialog): """Text data import wizard""" def __init__(self, parent, text, title=None, icon=None, contents_title=None, varname=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) if title is None: title = _("Import wizard") self.setWindowTitle(title) if icon is None: self.setWindowIcon(ima.icon('fileimport')) if contents_title is None: contents_title = _("Raw text") if varname is None: varname = _("variable_name") self.var_name, self.clip_data = None, None # Setting GUI self.tab_widget = QTabWidget(self) self.text_widget = ContentsWidget(self, text) self.table_widget = PreviewWidget(self) self.tab_widget.addTab(self.text_widget, _("text")) self.tab_widget.setTabText(0, contents_title) self.tab_widget.addTab(self.table_widget, _("table")) self.tab_widget.setTabText(1, _("Preview")) self.tab_widget.setTabEnabled(1, False) name_layout = QHBoxLayout() name_label = QLabel(_("Variable Name")) name_layout.addWidget(name_label) self.name_edt = QLineEdit() self.name_edt.setText(varname) name_layout.addWidget(self.name_edt) btns_layout = QHBoxLayout() cancel_btn = QPushButton(_("Cancel")) btns_layout.addWidget(cancel_btn) cancel_btn.clicked.connect(self.reject) h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) btns_layout.addItem(h_spacer) self.back_btn = QPushButton(_("Previous")) self.back_btn.setEnabled(False) btns_layout.addWidget(self.back_btn) self.back_btn.clicked.connect(ft_partial(self._set_step, step=-1)) self.fwd_btn = QPushButton(_("Next")) if not text: self.fwd_btn.setEnabled(False) btns_layout.addWidget(self.fwd_btn) self.fwd_btn.clicked.connect(ft_partial(self._set_step, step=1)) self.done_btn = QPushButton(_("Done")) self.done_btn.setEnabled(False) btns_layout.addWidget(self.done_btn) self.done_btn.clicked.connect(self.process) self.text_widget.asDataChanged.connect(self.fwd_btn.setEnabled) self.text_widget.asDataChanged.connect(self.done_btn.setDisabled) layout = QVBoxLayout() layout.addLayout(name_layout) layout.addWidget(self.tab_widget) layout.addLayout(btns_layout) self.setLayout(layout) def _focus_tab(self, tab_idx): """Change tab focus""" for i in range(self.tab_widget.count()): self.tab_widget.setTabEnabled(i, False) self.tab_widget.setTabEnabled(tab_idx, True) self.tab_widget.setCurrentIndex(tab_idx) def _set_step(self, step): """Proceed to a given step""" new_tab = self.tab_widget.currentIndex() + step assert new_tab < self.tab_widget.count() and new_tab >= 0 if new_tab == self.tab_widget.count() - 1: try: self.table_widget.open_data( self._get_plain_text(), self.text_widget.get_col_sep(), self.text_widget.get_row_sep(), self.text_widget.trnsp_box.isChecked(), self.text_widget.get_skiprows(), self.text_widget.get_comments()) self.done_btn.setEnabled(True) self.done_btn.setDefault(True) self.fwd_btn.setEnabled(False) self.back_btn.setEnabled(True) except (SyntaxError, AssertionError) as error: QMessageBox.critical( self, _("Import wizard"), _("<b>Unable to proceed to next step</b>" "<br><br>Please check your entries." "<br><br>Error message:<br>%s") % str(error)) return elif new_tab == 0: self.done_btn.setEnabled(False) self.fwd_btn.setEnabled(True) self.back_btn.setEnabled(False) self._focus_tab(new_tab) def get_data(self): """Return processed data""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.var_name, self.clip_data def _simplify_shape(self, alist, rec=0): """Reduce the alist dimension if needed""" if rec != 0: if len(alist) == 1: return alist[-1] return alist if len(alist) == 1: return self._simplify_shape(alist[-1], 1) return [self._simplify_shape(al, 1) for al in alist] def _get_table_data(self): """Return clipboard processed as data""" data = self._simplify_shape(self.table_widget.get_data()) if self.table_widget.array_btn.isChecked(): return array(data) elif pd and self.table_widget.df_btn.isChecked(): info = self.table_widget.pd_info buf = io.StringIO(self.table_widget.pd_text) return pd.read_csv(buf, **info) return data def _get_plain_text(self): """Return clipboard as text""" return self.text_widget.text_editor.toPlainText() @Slot() def process(self): """Process the data from clipboard""" var_name = self.name_edt.text() try: self.var_name = str(var_name) except UnicodeEncodeError: self.var_name = to_text_string(var_name) if self.text_widget.get_as_data(): self.clip_data = self._get_table_data() elif self.text_widget.get_as_code(): self.clip_data = try_to_eval(to_text_string( self._get_plain_text())) else: self.clip_data = to_text_string(self._get_plain_text()) self.accept()
class MainWindow(QMainWindow): def __init__(self, log, app): self.log = log.getChild('Main') self.app = app super().__init__() self.dark_theme = CONFIG['dark_theme_default'] self.single_tab_mode = CONFIG['single_tab_mode_default'] self.loggers_by_name = {} # name -> LoggerTab self.popped_out_loggers = {} self.server_running = False self.shutting_down = False self.setupUi() self.start_server() def setupUi(self): self.resize(800, 600) self.setWindowTitle('cutelog') self.loggerTabWidget = QTabWidget(self) self.loggerTabWidget.setTabsClosable(True) self.loggerTabWidget.setMovable(True) self.loggerTabWidget.setTabBarAutoHide(True) self.loggerTabWidget.currentChanged.connect(self.change_actions_state) self.setCentralWidget(self.loggerTabWidget) self.statusbar = QStatusBar(self) self.setStatusBar(self.statusbar) self.setup_menubar() self.setup_action_triggers() self.setup_shortcuts() self.loggerTabWidget.tabCloseRequested.connect(self.close_tab) self.reload_stylesheet() self.restore_geometry() self.show() def setup_menubar(self): self.menubar = QMenuBar(self) self.setMenuBar(self.menubar) # File menu self.menuFile = self.menubar.addMenu("File") self.actionLoadRecords = self.menuFile.addAction('Load records') self.actionSaveRecords = self.menuFile.addAction('Save records') self.menuFile.addSeparator() self.actionDarkTheme = self.menuFile.addAction('Dark theme') self.actionDarkTheme.setCheckable(True) self.actionDarkTheme.setChecked(self.dark_theme) self.actionSingleTab = self.menuFile.addAction('Single tab mode') self.actionSingleTab.setCheckable(True) self.actionSingleTab.setChecked(self.single_tab_mode) # self.actionReloadStyle = self.menuFile.addAction('Reload style') self.actionSettings = self.menuFile.addAction('Settings') self.menuFile.addSeparator() self.actionQuit = self.menuFile.addAction('Quit') # Tab menu self.menuTab = self.menubar.addMenu("Tab") self.actionCloseTab = self.menuTab.addAction('Close') self.actionPopOut = self.menuTab.addAction('Pop out') self.actionRenameTab = self.menuTab.addAction('Rename') self.menuTab.addSeparator() self.actionPopIn = self.menuTab.addAction('Pop in tabs...') self.actionMergeTabs = self.menuTab.addAction('Merge tabs...') self.menuTab.addSeparator() self.actionExtraMode = self.menuTab.addAction('Extra mode') self.actionExtraMode.setCheckable(True) # Server menu self.menuServer = self.menubar.addMenu("Server") self.actionRestartServer = self.menuServer.addAction('Restart server') self.actionStartStopServer = self.menuServer.addAction('Stop server') # Records menu self.menuRecords = self.menubar.addMenu("Records") self.actionTrimTabRecords = self.menuRecords.addAction('Trim records') self.actionSetMaxCapacity = self.menuRecords.addAction( 'Set max capacity') # Help menu self.menuHelp = self.menubar.addMenu("Help") self.actionAbout = self.menuHelp.addAction("About cutelog") self.change_actions_state( ) # to disable all logger actions, since they don't function yet def setup_action_triggers(self): # File menu self.actionLoadRecords.triggered.connect(self.open_load_records_dialog) self.actionSaveRecords.triggered.connect(self.open_save_records_dialog) self.actionDarkTheme.toggled.connect(self.toggle_dark_theme) self.actionSingleTab.triggered.connect(self.set_single_tab_mode) # self.actionReloadStyle.triggered.connect(self.reload_stylesheet) self.actionSettings.triggered.connect(self.settings_dialog) self.actionQuit.triggered.connect(self.shutdown) # Tab meny self.actionCloseTab.triggered.connect(self.close_current_tab) self.actionPopOut.triggered.connect(self.pop_out_tab) self.actionRenameTab.triggered.connect(self.rename_tab_dialog) self.actionPopIn.triggered.connect(self.pop_in_tabs_dialog) self.actionMergeTabs.triggered.connect(self.merge_tabs_dialog) self.actionExtraMode.triggered.connect(self.toggle_extra_mode) # Server menu self.actionRestartServer.triggered.connect(self.restart_server) self.actionStartStopServer.triggered.connect(self.start_or_stop_server) # Records menu self.actionTrimTabRecords.triggered.connect(self.trim_records_dialog) self.actionSetMaxCapacity.triggered.connect(self.max_capacity_dialog) # About menu self.actionAbout.triggered.connect(self.about_dialog) def change_actions_state(self, index=None): logger, _ = self.current_logger_and_index() # if there are no loggers in tabs, these actions will be disabled: actions = [ self.actionCloseTab, self.actionExtraMode, self.actionPopOut, self.actionRenameTab, self.actionPopIn, self.actionTrimTabRecords, self.actionSetMaxCapacity, self.actionSaveRecords ] if not logger: for action in actions: action.setDisabled(True) self.actionExtraMode.setChecked(False) if len(self.popped_out_loggers) > 0: self.actionPopIn.setDisabled(False) else: for action in actions: action.setDisabled(False) if len(self.loggers_by_name) == self.loggerTabWidget.count(): self.actionPopIn.setDisabled(True) self.actionExtraMode.setChecked(logger.extra_mode) # if all loggers are popped in if len(self.popped_out_loggers) == 0: self.actionPopIn.setDisabled(True) if len(self.loggers_by_name) <= 1: self.actionMergeTabs.setDisabled(True) else: self.actionMergeTabs.setDisabled(False) def set_single_tab_mode(self, enabled): self.single_tab_mode = enabled def setup_shortcuts(self): self.actionQuit.setShortcut('Ctrl+Q') self.actionDarkTheme.setShortcut('Ctrl+S') # self.actionReloadStyle.setShortcut('Ctrl+R') # self.actionSettings.setShortcut('Ctrl+A') self.actionCloseTab.setShortcut('Ctrl+W') def save_geometry(self): CONFIG.save_geometry(self.geometry()) def restore_geometry(self): geometry = CONFIG.load_geometry() if geometry: self.resize(geometry.width(), geometry.height()) def settings_dialog(self): d = SettingsDialog(self) d.setWindowModality(Qt.ApplicationModal) d.setAttribute(Qt.WA_DeleteOnClose, True) d.open() def about_dialog(self): d = AboutDialog(self) d.open() def reload_stylesheet(self): if self.dark_theme: self.reload_dark_style() else: self.reload_light_style() def reload_light_style(self): if CONFIG['light_theme_is_native']: self.set_style_to_stock() return f = QFile(":/light_theme.qss") f.open(QFile.ReadOnly | QFile.Text) ts = QTextStream(f) qss = ts.readAll() # f = open(Config.get_resource_path('light_theme.qss', 'resources/ui'), 'r') # qss = f.read() self.app.setStyleSheet(qss) def reload_dark_style(self): f = QFile(":/dark_theme.qss") f.open(QFile.ReadOnly | QFile.Text) ts = QTextStream(f) qss = ts.readAll() # f = open(Config.get_resource_path('dark_theme.qss', 'resources/ui'), 'r') # qss = f.read() self.app.setStyleSheet(qss) def set_style_to_stock(self): self.app.setStyleSheet('') def toggle_dark_theme(self, enabled): self.dark_theme = enabled self.reload_stylesheet() for logger in self.loggers_by_name.values(): logger.set_dark_theme(enabled) def toggle_extra_mode(self, enabled): logger, _ = self.current_logger_and_index() if not logger: return logger.set_extra_mode(enabled) def start_server(self): self.log.debug('Starting the server') self.server = LogServer(self, self.on_connection, self.log) self.server.start() self.server_running = True self.actionStartStopServer.setText('Stop server') def stop_server(self): if self.server_running: self.log.debug('Stopping the server') self.server.close_server() self.server_running = False del self.server self.server = None self.actionStartStopServer.setText('Start server') def restart_server(self): self.log.debug('Restarting the server') self.stop_server() self.start_server() def start_or_stop_server(self): if self.server_running: self.stop_server() else: self.start_server() def on_connection(self, conn, conn_id): self.log.debug('New connection id={}'.format(conn_id)) if self.single_tab_mode and len(self.loggers_by_name) > 0: new_logger = list(self.loggers_by_name.values())[0] new_logger.add_connection(conn) else: new_logger, index = self.create_logger(conn) self.loggerTabWidget.setCurrentIndex(index) conn.new_record.connect(new_logger.on_record) conn.connection_finished.connect(new_logger.remove_connection) if self.server.benchmark and conn_id == -1: from .listener import BenchmarkMonitor bm = BenchmarkMonitor(self, new_logger) bm.speed_readout.connect(self.set_status) conn.connection_finished.connect(bm.requestInterruption) self.server.threads.append(bm) bm.start() def create_logger(self, conn, name=None): name = self.make_logger_name_unique("Logger" if name is None else name) new_logger = LoggerTab(self.loggerTabWidget, name, conn, self.log, self) new_logger.set_dark_theme(self.dark_theme) self.loggers_by_name[name] = new_logger index = self.loggerTabWidget.addTab(new_logger, name) return new_logger, index def make_logger_name_unique(self, name): name_f = "{} {{}}".format(name) c = 1 while name in self.loggers_by_name: name = name_f.format(c) c += 1 return name def set_status(self, string, timeout=3000): self.statusBar().showMessage(string, timeout) def rename_tab_dialog(self): logger, index = self.current_logger_and_index() if not logger: return d = QInputDialog(self) d.setLabelText('Enter the new name for the "{}" tab:'.format( logger.name)) d.setWindowTitle('Rename the "{}" tab'.format(logger.name)) d.textValueSelected.connect(self.rename_current_tab) d.open() def rename_current_tab(self, new_name): logger, index = self.current_logger_and_index() if new_name in self.loggers_by_name and new_name != logger.name: show_warning_dialog( self, "Rename error", 'Logger named "{}" already exists.'.format(new_name)) return self.log.debug('Renaming logger "{}" to "{}"'.format( logger.name, new_name)) del self.loggers_by_name[logger.name] logger.name = new_name self.loggers_by_name[new_name] = logger logger.log.name = '.'.join( logger.log.name.split('.')[:-1]) + '.{}'.format(new_name) self.loggerTabWidget.setTabText(index, new_name) def trim_records_dialog(self): logger, index = self.current_logger_and_index() if not logger: return d = QInputDialog(self) d.setInputMode(QInputDialog.IntInput) d.setIntRange( 0, 100000000) # because it sets intMaximum to 99 by default. why?? d.setLabelText('Keep this many records out of {}:'.format( logger.record_model.rowCount())) d.setWindowTitle('Trim tab records of "{}" logger'.format(logger.name)) d.intValueSelected.connect(self.trim_current_tab_records) d.open() def trim_current_tab_records(self, n): logger, index = self.current_logger_and_index() logger.record_model.trim_except_last_n(n) def max_capacity_dialog(self): logger, index = self.current_logger_and_index() if not logger: return d = QInputDialog(self) d.setInputMode(QInputDialog.IntInput) d.setIntRange( 0, 100000000) # because it sets intMaximum to 99 by default. why?? max_now = logger.record_model.max_capacity max_now = "not set" if max_now is None else max_now label_str = 'Set max capacity for "{}" logger\nCurrently {}. Set to 0 to disable:' d.setLabelText(label_str.format(logger.name, max_now)) d.setWindowTitle('Set max capacity') d.intValueSelected.connect(self.set_max_capacity) d.open() def set_max_capacity(self, n): logger, index = self.current_logger_and_index() logger.set_max_capacity(n) def merge_tabs_dialog(self): d = MergeDialog(self, self.loggers_by_name) d.setWindowModality(Qt.WindowModal) d.merge_tabs_signal.connect(self.merge_tabs) d.show() def merge_tabs(self, dst, srcs, keep_alive): self.log.debug('Merging tabs: dst="{}", srcs={}, keep={}'.format( dst, srcs, keep_alive)) dst_logger = self.loggers_by_name[dst] for src_name in srcs: src_logger = self.loggers_by_name[src_name] dst_logger.merge_with_records(src_logger.record_model.records) if keep_alive: for conn in src_logger.connections: conn.new_record.disconnect(src_logger.on_record) conn.connection_finished.disconnect( src_logger.remove_connection) conn.new_record.connect(dst_logger.on_record) dst_logger.add_connection(conn) src_logger.connections.clear() self.destroy_logger(src_logger) def close_current_tab(self): _, index = self.current_logger_and_index() if index is None: return self.close_tab(index) def close_tab(self, index): self.log.debug("Tab close requested: {}".format(index)) logger = self.loggerTabWidget.widget(index) self.loggerTabWidget.removeTab(index) self.log.debug(logger.name) self.destroy_logger(logger) def destroy_logger(self, logger): del self.loggers_by_name[logger.name] logger.setParent(None) logger.destroy() del logger def close_popped_out_logger(self, logger): del self.loggers_by_name[logger.name] del self.popped_out_loggers[logger.name] del logger if len(self.popped_out_loggers): self.actionPopIn.setDisabled(True) def current_logger_and_index(self): index = self.loggerTabWidget.currentIndex() if index == -1: return None, None logger = self.loggerTabWidget.widget(index) return logger, index def pop_out_tab(self): logger, index = self.current_logger_and_index() if not logger: return self.log.debug("Tab pop out requested: {}".format(int(index))) logger.destroyed.connect(logger.closeEvent) logger.setAttribute(Qt.WA_DeleteOnClose, True) logger.setWindowFlags(Qt.Window) logger.setWindowTitle('cutelog: "{}"'.format( self.loggerTabWidget.tabText(index))) self.popped_out_loggers[logger.name] = logger self.loggerTabWidget.removeTab(index) logger.popped_out = True logger.show() center_widget_on_screen(logger) def pop_in_tabs_dialog(self): d = PopInDialog(self, self.loggers_by_name.values()) d.pop_in_tabs.connect(self.pop_in_tabs) d.setWindowModality(Qt.ApplicationModal) d.open() def pop_in_tabs(self, names): for name in names: self.log.debug('Popping in logger "{}"'.format(name)) logger = self.loggers_by_name[name] self.pop_in_tab(logger) def pop_in_tab(self, logger): logger.setWindowFlags(Qt.Widget) logger.setAttribute(Qt.WA_DeleteOnClose, False) logger.destroyed.disconnect(logger.closeEvent) logger.setWindowTitle(logger.name) logger.popped_out = False del self.popped_out_loggers[logger.name] index = self.loggerTabWidget.addTab(logger, logger.windowTitle()) self.loggerTabWidget.setCurrentIndex(index) def open_load_records_dialog(self): d = QFileDialog(self) d.setFileMode(QFileDialog.ExistingFile) d.fileSelected.connect(self.load_records) d.setWindowTitle('Load records from...') d.open() def load_records(self, load_path): import json from os import path class RecordDecoder(json.JSONDecoder): def __init__(self, *args, **kwargs): json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) def object_hook(self, obj): if '_created' in obj: obj['created'] = obj['_created'] del obj['_created'] record = LogRecord(obj) del record._logDict['created'] else: record = LogRecord(obj) return record name = path.basename(load_path) index = None try: with open(load_path, 'r') as f: records = json.load(f, cls=RecordDecoder) new_logger, index = self.create_logger(None, name) new_logger.merge_with_records(records) self.loggerTabWidget.setCurrentIndex(index) self.set_status('Records have been loaded into "{}" tab'.format( new_logger.name)) except Exception as e: if index: self.close_tab(index) text = "Error while loading records: \n{}".format(e) self.log.error(text, exc_info=True) show_critical_dialog(self, "Couldn't load records", text) def open_save_records_dialog(self): from functools import partial logger, _ = self.current_logger_and_index() if not logger: return d = QFileDialog(self) d.selectFile(logger.name + '.log') d.setFileMode(QFileDialog.AnyFile) d.fileSelected.connect(partial(self.save_records, logger)) d.setWindowTitle('Save records of "{}" tab to...'.format(logger.name)) d.open() def save_records(self, logger, path): import json # needed because a deque is not serializable class RecordList(list): def __init__(self, records): self.records = records def __len__(self): return len(self.records) def __iter__(self): for record in self.records: d = record._logDict if not d.get('created', False) and not d.get( 'time', False): d['_created'] = record.created yield d try: records = logger.record_model.records record_list = RecordList(records) with open(path, 'w') as f: json.dump(record_list, f, indent=2) self.set_status('Records have been saved to "{}"'.format(path)) except Exception as e: text = "Error while saving records: \n{}".format(e) self.log.error(text, exc_info=True) show_critical_dialog(self, "Couldn't save records", text) def closeEvent(self, event): self.log.info('Close event on main window') self.shutdown() event.ignore( ) # prevents errors due to closing the program before server has stopped def destroy_all_tabs(self): self.log.debug('Destroying tabs') delete_this = list(self.loggers_by_name.values() ) # to prevent changing during iteration for logger in delete_this: self.destroy_logger(logger) def shutdown(self): self.log.info('Shutting down') if self.shutting_down: self.log.error('Exiting forcefully') raise SystemExit self.shutting_down = True self.stop_server() self.save_geometry() self.destroy_all_tabs() self.app.quit() def signal_handler(self, *args): self.shutdown()
class CentralWidget(QWidget): """The PyNetAnalyzer central widget""" def __init__(self, parent): QWidget.__init__(self) self.parent = parent self.appdata: CnaData = parent.appdata self.map_counter = 0 self.searchbar = QLineEdit() self.searchbar.setPlaceholderText("Enter search term") self.throttler = SignalThrottler(300) self.searchbar.textChanged.connect(self.throttler.throttle) self.throttler.triggered.connect(self.update_selected) self.tabs = QTabWidget() self.reaction_list = ReactionList(self.appdata) self.metabolite_list = MetaboliteList(self.appdata) self.model_info = ModelInfo(self.appdata) self.tabs.addTab(self.reaction_list, "Reactions") self.tabs.addTab(self.metabolite_list, "Metabolites") self.tabs.addTab(self.model_info, "Model") self.map_tabs = QTabWidget() self.map_tabs.setTabsClosable(True) self.map_tabs.setMovable(True) # Create an in-process kernel kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) kernel = kernel_manager.kernel kernel.gui = 'qt' myglobals = globals() myglobals["cna"] = self.parent self.kernel_shell = kernel_manager.kernel.shell self.kernel_shell.push(myglobals) self.kernel_client = kernel_manager.client() self.kernel_client.start_channels() # Check if client is working self.kernel_client.execute('import matplotlib.pyplot as plt') self.kernel_client.execute('%matplotlib inline') self.kernel_client.execute( "%config InlineBackend.figure_format = 'svg'") self.console = RichJupyterWidget() self.console.kernel_manager = kernel_manager self.console.kernel_client = self.kernel_client self.splitter = QSplitter() self.splitter2 = QSplitter() self.splitter2.addWidget(self.map_tabs) self.mode_navigator = ModeNavigator(self.appdata) self.splitter2.addWidget(self.mode_navigator) self.splitter2.addWidget(self.console) self.splitter2.setOrientation(Qt.Vertical) self.splitter.addWidget(self.splitter2) self.splitter.addWidget(self.tabs) self.console.show() layout = QVBoxLayout() layout.addWidget(self.searchbar) layout.addWidget(self.splitter) self.setLayout(layout) self.tabs.currentChanged.connect(self.tabs_changed) self.reaction_list.jumpToMap.connect(self.jump_to_map) self.reaction_list.jumpToMetabolite.connect(self.jump_to_metabolite) self.reaction_list.reactionChanged.connect( self.handle_changed_reaction) self.reaction_list.reactionDeleted.connect( self.handle_deleted_reaction) self.metabolite_list.metaboliteChanged.connect( self.handle_changed_metabolite) self.metabolite_list.jumpToReaction.connect(self.jump_to_reaction) self.metabolite_list.computeInOutFlux.connect(self.in_out_fluxes) self.model_info.optimizationDirectionChanged.connect( self.handle_changed_optimization_direction) self.map_tabs.tabCloseRequested.connect(self.delete_map) self.mode_navigator.changedCurrentMode.connect(self.update_mode) self.mode_navigator.modeNavigatorClosed.connect(self.update) self.update() def fit_mapview(self): self.map_tabs.currentWidget().fit() def show_bottom_of_console(self): (_, r) = self.splitter2.getRange(1) self.splitter2.moveSplitter(r * 0.5, 1) vSB = self.console.children()[2].verticalScrollBar() max_scroll = vSB.maximum() vSB.setValue(max_scroll - 100) def handle_changed_reaction(self, old_id: str, reaction: cobra.Reaction): self.parent.unsaved_changes() for mmap in self.appdata.project.maps: if old_id in self.appdata.project.maps[mmap]["boxes"].keys(): self.appdata.project.maps[mmap]["boxes"][ reaction. id] = self.appdata.project.maps[mmap]["boxes"].pop(old_id) # TODO update only relevant reaction boxes on maps self.update_maps() def handle_deleted_reaction(self, reaction: cobra.Reaction): self.appdata.project.cobra_py_model.remove_reactions( [reaction], remove_orphans=True) self.parent.unsaved_changes() for mmap in self.appdata.project.maps: if reaction.id in self.appdata.project.maps[mmap]["boxes"].keys(): self.appdata.project.maps[mmap]["boxes"].pop(reaction.id) # TODO update only relevant reaction boxes on maps self.update_maps() def handle_changed_metabolite(self, old_id: str, metabolite: cobra.Metabolite): self.parent.unsaved_changes() # TODO update only relevant reaction boxes on maps self.update_maps() def handle_changed_optimization_direction(self, direction: str): self.parent.unsaved_changes() def shutdown_kernel(self): self.console.kernel_client.stop_channels() self.console.kernel_manager.shutdown_kernel() def switch_to_reaction(self, reaction: str): self.tabs.setCurrentIndex(0) self.reaction_list.set_current_item(reaction) def minimize_reaction(self, reaction: str): self.parent.fba_optimize_reaction(reaction, mmin=True) def maximize_reaction(self, reaction: str): self.parent.fba_optimize_reaction(reaction, mmin=False) def update_reaction_value(self, reaction: str, value: str): if value == "": self.appdata.scen_values_pop(reaction) self.appdata.project.comp_values.pop(reaction, None) else: try: x = float(value) self.appdata.scen_values_set(reaction, (x, x)) except ValueError: (vl, vh) = make_tuple(value) self.appdata.scen_values_set(reaction, (vl, vh)) self.reaction_list.update() def update_reaction_maps(self, _reaction: str): self.parent.unsaved_changes() self.reaction_list.reaction_mask.update_state() def handle_mapChanged(self, _reaction: str): self.parent.unsaved_changes() def tabs_changed(self, idx): if idx == 0: self.reaction_list.update() elif idx == 1: (clean_model, unused_mets) = prune_unused_metabolites( self.appdata.project.cobra_py_model) self.appdata.project.cobra_py_model = clean_model self.metabolite_list.update() elif idx == 2: self.model_info.update() def add_map(self): while True: name = "Map " + str(self.map_counter) self.map_counter += 1 if name not in self.appdata.project.maps.keys(): break m = CnaMap(name) self.appdata.project.maps[name] = m mmap = MapView(self.appdata, name) mmap.switchToReactionMask.connect(self.switch_to_reaction) mmap.minimizeReaction.connect(self.minimize_reaction) mmap.maximizeReaction.connect(self.maximize_reaction) mmap.reactionValueChanged.connect(self.update_reaction_value) mmap.reactionRemoved.connect(self.update_reaction_maps) mmap.reactionAdded.connect(self.update_reaction_maps) mmap.mapChanged.connect(self.handle_mapChanged) self.map_tabs.addTab(mmap, m["name"]) self.update_maps() self.map_tabs.setCurrentIndex(len(self.appdata.project.maps)) self.parent.unsaved_changes() def delete_map(self, idx: int): name = self.map_tabs.tabText(idx) diag = ConfirmMapDeleteDialog(self, idx, name) diag.exec() def update_selected(self): x = self.searchbar.text() idx = self.tabs.currentIndex() if idx == 0: self.reaction_list.update_selected(x) if idx == 1: self.metabolite_list.update_selected(x) idx = self.map_tabs.currentIndex() if idx >= 0: m = self.map_tabs.widget(idx) m.update_selected(x) def update_mode(self): if len(self.appdata.project.modes) > self.mode_navigator.current: values = self.appdata.project.modes[self.mode_navigator.current] # set values self.appdata.project.scen_values.clear() self.appdata.project.comp_values.clear() for i in values: self.appdata.project.comp_values[i] = (values[i], values[i]) self.appdata.modes_coloring = True self.update() self.appdata.modes_coloring = False def update(self): if len(self.appdata.project.modes) == 0: self.mode_navigator.hide() self.mode_navigator.current = 0 else: self.mode_navigator.show() self.mode_navigator.update() idx = self.tabs.currentIndex() if idx == 0: self.reaction_list.update() elif idx == 1: self.metabolite_list.update() elif idx == 2: self.model_info.update() idx = self.map_tabs.currentIndex() if idx >= 0: m = self.map_tabs.widget(idx) m.update() def update_map(self, idx): m = self.map_tabs.widget(idx) if m is not None: m.update() def update_maps(self): for idx in range(0, self.map_tabs.count()): m = self.map_tabs.widget(idx) m.update() def jump_to_map(self, identifier: str, reaction: str): for idx in range(0, self.map_tabs.count()): name = self.map_tabs.tabText(idx) if name == identifier: m = self.map_tabs.widget(idx) self.map_tabs.setCurrentIndex(idx) m.update() m.focus_reaction(reaction) m.highlight_reaction(reaction) break def jump_to_metabolite(self, metabolite: str): self.tabs.setCurrentIndex(1) m = self.tabs.widget(1) m.set_current_item(metabolite) def jump_to_reaction(self, reaction: str): self.tabs.setCurrentIndex(0) m = self.tabs.widget(0) m.set_current_item(reaction) def in_out_fluxes(self, metabolite): self.kernel_client.execute("cna.print_in_out_fluxes('" + metabolite + "')") self.show_bottom_of_console()
class ImportWizard(QDialog): """Text data import wizard""" def __init__(self, parent, text, title=None, icon=None, contents_title=None, varname=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) if title is None: title = _("Import wizard") self.setWindowTitle(title) if icon is None: self.setWindowIcon(ima.icon('fileimport')) if contents_title is None: contents_title = _("Raw text") if varname is None: varname = _("variable_name") self.var_name, self.clip_data = None, None # Setting GUI self.tab_widget = QTabWidget(self) self.text_widget = ContentsWidget(self, text) self.table_widget = PreviewWidget(self) self.tab_widget.addTab(self.text_widget, _("text")) self.tab_widget.setTabText(0, contents_title) self.tab_widget.addTab(self.table_widget, _("table")) self.tab_widget.setTabText(1, _("Preview")) self.tab_widget.setTabEnabled(1, False) name_layout = QHBoxLayout() name_label = QLabel(_("Variable Name")) name_layout.addWidget(name_label) self.name_edt = QLineEdit() self.name_edt.setText(varname) name_layout.addWidget(self.name_edt) btns_layout = QHBoxLayout() cancel_btn = QPushButton(_("Cancel")) btns_layout.addWidget(cancel_btn) cancel_btn.clicked.connect(self.reject) h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) btns_layout.addItem(h_spacer) self.back_btn = QPushButton(_("Previous")) self.back_btn.setEnabled(False) btns_layout.addWidget(self.back_btn) self.back_btn.clicked.connect(ft_partial(self._set_step, step=-1)) self.fwd_btn = QPushButton(_("Next")) btns_layout.addWidget(self.fwd_btn) self.fwd_btn.clicked.connect(ft_partial(self._set_step, step=1)) self.done_btn = QPushButton(_("Done")) self.done_btn.setEnabled(False) btns_layout.addWidget(self.done_btn) self.done_btn.clicked.connect(self.process) self.text_widget.asDataChanged.connect(self.fwd_btn.setEnabled) self.text_widget.asDataChanged.connect(self.done_btn.setDisabled) layout = QVBoxLayout() layout.addLayout(name_layout) layout.addWidget(self.tab_widget) layout.addLayout(btns_layout) self.setLayout(layout) def _focus_tab(self, tab_idx): """Change tab focus""" for i in range(self.tab_widget.count()): self.tab_widget.setTabEnabled(i, False) self.tab_widget.setTabEnabled(tab_idx, True) self.tab_widget.setCurrentIndex(tab_idx) def _set_step(self, step): """Proceed to a given step""" new_tab = self.tab_widget.currentIndex() + step assert new_tab < self.tab_widget.count() and new_tab >= 0 if new_tab == self.tab_widget.count()-1: try: self.table_widget.open_data(self._get_plain_text(), self.text_widget.get_col_sep(), self.text_widget.get_row_sep(), self.text_widget.trnsp_box.isChecked(), self.text_widget.get_skiprows(), self.text_widget.get_comments()) self.done_btn.setEnabled(True) self.done_btn.setDefault(True) self.fwd_btn.setEnabled(False) self.back_btn.setEnabled(True) except (SyntaxError, AssertionError) as error: QMessageBox.critical(self, _("Import wizard"), _("<b>Unable to proceed to next step</b>" "<br><br>Please check your entries." "<br><br>Error message:<br>%s") % str(error)) return elif new_tab == 0: self.done_btn.setEnabled(False) self.fwd_btn.setEnabled(True) self.back_btn.setEnabled(False) self._focus_tab(new_tab) def get_data(self): """Return processed data""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.var_name, self.clip_data def _simplify_shape(self, alist, rec=0): """Reduce the alist dimension if needed""" if rec != 0: if len(alist) == 1: return alist[-1] return alist if len(alist) == 1: return self._simplify_shape(alist[-1], 1) return [self._simplify_shape(al, 1) for al in alist] def _get_table_data(self): """Return clipboard processed as data""" data = self._simplify_shape( self.table_widget.get_data()) if self.table_widget.array_btn.isChecked(): return array(data) elif pd and self.table_widget.df_btn.isChecked(): info = self.table_widget.pd_info buf = io.StringIO(self.table_widget.pd_text) return pd.read_csv(buf, **info) return data def _get_plain_text(self): """Return clipboard as text""" return self.text_widget.text_editor.toPlainText() @Slot() def process(self): """Process the data from clipboard""" var_name = self.name_edt.text() try: self.var_name = str(var_name) except UnicodeEncodeError: self.var_name = to_text_string(var_name) if self.text_widget.get_as_data(): self.clip_data = self._get_table_data() elif self.text_widget.get_as_code(): self.clip_data = try_to_eval( to_text_string(self._get_plain_text())) else: self.clip_data = to_text_string(self._get_plain_text()) self.accept()
class HintTabView(QAbstractItemView): """ View that is responsible for displaying Hints in a tab-based manner. """ def __init__(self, parent=None): super(HintTabView, self).__init__(parent) self._tabWidget = QTabWidget() self._tabWidget.setParent(self) self._indexToTabMap = OrderedDict() self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self._tabWidget) def _findTab(self, tabName): """ Convenience function to find a tab by name (instead of by index as provide by Qt's API). Parameters ---------- tabName Name of the tab to attempt to find. Returns ------- QWidget If found, returns the found widget with name ``tabName``. Raises an IndexError if not found. """ for i in range(self._tabWidget.count()): if self._tabWidget.tabText(i) == tabName: return self._tabWidget.widget(i) raise IndexError def dataChanged(self, topLeft: QModelIndex, bottomRight: QModelIndex, roles=None): """ Re-implements the QAbstractItemView.dataChanged() slot. When the data attached to the Qt.CheckStateRole has been changed, this will either render a Hint or remove the Hint visualization. Parameters ---------- topLeft For now, the only index we are concerned with, which corresponds to the item's check state changing. bottomRight (Unused right now) roles List of roles attached to the data state change. """ if roles is None: roles = [] if self.model(): # empty list indicates ALL roles have changed (see documentation) if Qt.CheckStateRole in roles or len(roles) == 0: hint = topLeft.data(Qt.UserRole) if hint: if topLeft.data(Qt.CheckStateRole) == Qt.Checked: if hint.group not in [ self._tabWidget.tabText(index) for index in range(self._tabWidget.count()) ]: canvas = hint.init_canvas(addLegend=True) self._tabWidget.addTab(canvas, hint.group) else: canvas = self._findTab(hint.group) hint.visualize(canvas) else: hint.remove() super(HintTabView, self).dataChanged(topLeft, bottomRight, roles) def horizontalOffset(self): return 0 def indexAt(self, point: QPoint): return QModelIndex() def moveCursor(self, QAbstractItemView_CursorAction, Union, Qt_KeyboardModifiers=None, Qt_KeyboardModifier=None): return QModelIndex() def rowsInserted(self, index: QModelIndex, start, end): return def rowsAboutToBeRemoved(self, index: QModelIndex, start, end): return def scrollTo(self, QModelIndex, hint=None): return def verticalOffset(self): return 0 def visualRect(self, QModelIndex): from qtpy.QtCore import QRect return QRect()
class RunDialog(QDialog): simulation_done = Signal(bool, str) simulation_termination_request = Signal() def __init__(self, config_file, run_model, simulation_arguments, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setModal(True) self.setWindowModality(Qt.WindowModal) self.setWindowTitle("Simulations - {}".format(config_file)) self._snapshot_model = SnapshotModel(self) self._run_model = run_model self._isDetailedDialog = False self._minimum_width = 1200 ert = None if isinstance(run_model, BaseRunModel): ert = run_model.ert() self._simulations_argments = simulation_arguments self._ticker = QTimer(self) self._ticker.timeout.connect(self._on_ticker) progress_proxy_model = ProgressProxyModel(self._snapshot_model, parent=self) self._total_progress_label = QLabel(_TOTAL_PROGRESS_TEMPLATE.format(0), self) self._total_progress_bar = QProgressBar(self) self._total_progress_bar.setRange(0, 100) self._total_progress_bar.setTextVisible(False) self._iteration_progress_label = QLabel(self) self._progress_view = ProgressView(self) self._progress_view.setModel(progress_proxy_model) self._progress_view.setIndeterminate(True) legend_view = LegendView(self) legend_view.setModel(progress_proxy_model) self._tab_widget = QTabWidget(self) self._snapshot_model.rowsInserted.connect(self.on_new_iteration) self._job_label = QLabel(self) self._job_model = JobListProxyModel(self, 0, 0, 0, 0) self._job_model.setSourceModel(self._snapshot_model) self._job_view = QTableView(self) self._job_view.clicked.connect(self._job_clicked) self._open_files = {} self._job_view.setModel(self._job_model) self.running_time = QLabel("") self.plot_tool = PlotTool(config_file) self.plot_tool.setParent(self) self.plot_button = QPushButton(self.plot_tool.getName()) self.plot_button.clicked.connect(self.plot_tool.trigger) self.plot_button.setEnabled(ert is not None) self.kill_button = QPushButton("Kill Simulations") self.done_button = QPushButton("Done") self.done_button.setHidden(True) self.restart_button = QPushButton("Restart") self.restart_button.setHidden(True) self.show_details_button = QPushButton("Show Details") self.show_details_button.setCheckable(True) size = 20 spin_movie = resourceMovie("ide/loading.gif") spin_movie.setSpeed(60) spin_movie.setScaledSize(QSize(size, size)) spin_movie.start() self.processing_animation = QLabel() self.processing_animation.setMaximumSize(QSize(size, size)) self.processing_animation.setMinimumSize(QSize(size, size)) self.processing_animation.setMovie(spin_movie) button_layout = QHBoxLayout() button_layout.addWidget(self.processing_animation) button_layout.addWidget(self.running_time) button_layout.addStretch() button_layout.addWidget(self.show_details_button) button_layout.addWidget(self.plot_button) button_layout.addWidget(self.kill_button) button_layout.addWidget(self.done_button) button_layout.addWidget(self.restart_button) button_widget_container = QWidget() button_widget_container.setLayout(button_layout) layout = QVBoxLayout() layout.addWidget(self._total_progress_label) layout.addWidget(self._total_progress_bar) layout.addWidget(self._iteration_progress_label) layout.addWidget(self._progress_view) layout.addWidget(legend_view) layout.addWidget(self._tab_widget) layout.addWidget(self._job_label) layout.addWidget(self._job_view) layout.addWidget(button_widget_container) self.setLayout(layout) self.kill_button.clicked.connect(self.killJobs) self.done_button.clicked.connect(self.accept) self.restart_button.clicked.connect(self.restart_failed_realizations) self.show_details_button.clicked.connect(self.toggle_detailed_progress) self.simulation_done.connect(self._on_simulation_done) self.setMinimumWidth(self._minimum_width) self._setSimpleDialog() def _setSimpleDialog(self) -> None: self._isDetailedDialog = False self._tab_widget.setVisible(False) self._job_label.setVisible(False) self._job_view.setVisible(False) self.show_details_button.setText("Show Details") def _setDetailedDialog(self) -> None: self._isDetailedDialog = True self._tab_widget.setVisible(True) self._job_label.setVisible(True) self._job_view.setVisible(True) self.show_details_button.setText("Hide Details") @Slot(QModelIndex, int, int) def on_new_iteration(self, parent: QModelIndex, start: int, end: int) -> None: if not parent.isValid(): iter = start self._iteration_progress_label.setText( f"Progress for iteration {iter}") widget = RealizationWidget(iter) widget.setSnapshotModel(self._snapshot_model) widget.currentChanged.connect(self._select_real) self._tab_widget.addTab(widget, f"Realizations for iteration {iter}") @Slot(QModelIndex) def _job_clicked(self, index): if not index.isValid(): return selected_file = index.data(FileRole) if selected_file and selected_file not in self._open_files: job_name = index.siblingAtColumn(0).data() viewer = FileDialog( selected_file, job_name, index.row(), index.model().get_real(), index.model().get_iter(), self, ) self._open_files[selected_file] = viewer viewer.finished.connect( lambda _, f=selected_file: self._open_files.pop(f)) elif selected_file in self._open_files: self._open_files[selected_file].raise_() @Slot(QModelIndex) def _select_real(self, index): step = 0 stage = 0 real = index.row() iter_ = index.model().get_iter() self._job_model.set_step(iter_, real, stage, step) self._job_label.setText( f"Realization id {index.data(RealIens)} in iteration {iter_}") self._job_view.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) # Clear the selection in the other tabs for i in range(0, self._tab_widget.count()): if i != self._tab_widget.currentIndex(): self._tab_widget.widget(i).clearSelection() def reject(self): return def closeEvent(self, QCloseEvent): if self._run_model.isFinished(): self.simulation_done.emit(self._run_model.hasRunFailed(), self._run_model.getFailMessage()) else: # Kill jobs if dialog is closed if self.killJobs() != QMessageBox.Yes: QCloseEvent.ignore() def startSimulation(self): self._run_model.reset() self._snapshot_model.reset() self._tab_widget.clear() def run(): asyncio.set_event_loop(asyncio.new_event_loop()) self._run_model.startSimulations(self._simulations_argments) simulation_thread = Thread(name="ert_gui_simulation_thread") simulation_thread.setDaemon(True) simulation_thread.run = run simulation_thread.start() self._ticker.start(1000) tracker = create_tracker( self._run_model, num_realizations=self._simulations_argments["active_realizations"]. count(), ee_config=self._simulations_argments.get("ee_config", None), ) worker = TrackerWorker(tracker) worker_thread = QThread() worker.done.connect(worker_thread.quit) worker.consumed_event.connect(self._on_tracker_event) worker.moveToThread(worker_thread) self.simulation_done.connect(worker.stop) self._worker = worker self._worker_thread = worker_thread worker_thread.started.connect(worker.consume_and_emit) self._worker_thread.start() def killJobs(self): msg = "Are you sure you want to kill the currently running simulations?" if self._run_model.getQueueStatus().get( JobStatusType.JOB_QUEUE_UNKNOWN, 0) > 0: msg += "\n\nKilling a simulation with unknown status will not kill the realizations already submitted!" kill_job = QMessageBox.question(self, "Kill simulations?", msg, QMessageBox.Yes | QMessageBox.No) if kill_job == QMessageBox.Yes: # Normally this slot would be invoked by the signal/slot system, # but the worker is busy tracking the evaluation. self._worker.request_termination() self.reject() return kill_job @Slot(bool, str) def _on_simulation_done(self, failed, failed_msg): self.processing_animation.hide() self.kill_button.setHidden(True) self.done_button.setHidden(False) self.restart_button.setVisible(self.has_failed_realizations()) self.restart_button.setEnabled(self._run_model.support_restart) self._total_progress_bar.setValue(100) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(100)) if failed: QMessageBox.critical( self, "Simulations failed!", f"The simulation failed with the following error:\n\n{failed_msg}", ) @Slot() def _on_ticker(self): runtime = self._run_model.get_runtime() self.running_time.setText(format_running_time(runtime)) @Slot(object) def _on_tracker_event(self, event): if isinstance(event, EndEvent): self.simulation_done.emit(event.failed, event.failed_msg) self._worker.stop() self._ticker.stop() elif isinstance(event, FullSnapshotEvent): if event.snapshot is not None: self._snapshot_model._add_snapshot(event.snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(progress)) elif isinstance(event, SnapshotUpdateEvent): if event.partial_snapshot is not None: self._snapshot_model._add_partial_snapshot( event.partial_snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(progress)) def has_failed_realizations(self): completed = self._run_model.completed_realizations_mask initial = self._run_model.initial_realizations_mask for (index, successful) in enumerate(completed): if initial[index] and not successful: return True return False def count_successful_realizations(self): """ Counts the realizations completed in the prevoius ensemble run :return: """ completed = self._run_model.completed_realizations_mask return completed.count(True) def create_mask_from_failed_realizations(self): """ Creates a BoolVector mask representing the failed realizations :return: Type BoolVector """ completed = self._run_model.completed_realizations_mask initial = self._run_model.initial_realizations_mask inverted_mask = BoolVector(default_value=False) for (index, successful) in enumerate(completed): inverted_mask[index] = initial[index] and not successful return inverted_mask def restart_failed_realizations(self): msg = QMessageBox(self) msg.setIcon(QMessageBox.Information) msg.setText( "Note that workflows will only be executed on the restarted realizations and that this might have unexpected consequences." ) msg.setWindowTitle("Restart Failed Realizations") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) result = msg.exec_() if result == QMessageBox.Ok: self.restart_button.setVisible(False) self.kill_button.setVisible(True) self.done_button.setVisible(False) active_realizations = self.create_mask_from_failed_realizations() self._simulations_argments[ "active_realizations"] = active_realizations self._simulations_argments[ "prev_successful_realizations"] = self._simulations_argments.get( "prev_successful_realizations", 0) self._simulations_argments[ "prev_successful_realizations"] += self.count_successful_realizations( ) self.startSimulation() @Slot() def toggle_detailed_progress(self): if self._isDetailedDialog: self._setSimpleDialog() else: self._setDetailedDialog() self.adjustSize()
class RunDialog(QDialog): simulation_done = Signal(bool, str) simulation_termination_request = Signal() def __init__(self, config_file, run_model, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setModal(True) self.setWindowModality(Qt.WindowModal) self.setWindowTitle(f"Simulations - {config_file}") self._snapshot_model = SnapshotModel(self) self._run_model = run_model self._isDetailedDialog = False self._minimum_width = 1200 ert = None if isinstance(run_model, BaseRunModel): ert = run_model.ert() self._ticker = QTimer(self) self._ticker.timeout.connect(self._on_ticker) progress_proxy_model = ProgressProxyModel(self._snapshot_model, parent=self) self._total_progress_label = QLabel( _TOTAL_PROGRESS_TEMPLATE.format( total_progress=0, phase_name=run_model.getPhaseName()), self, ) self._total_progress_bar = QProgressBar(self) self._total_progress_bar.setRange(0, 100) self._total_progress_bar.setTextVisible(False) self._iteration_progress_label = QLabel(self) self._progress_view = ProgressView(self) self._progress_view.setModel(progress_proxy_model) self._progress_view.setIndeterminate(True) legend_view = LegendView(self) legend_view.setModel(progress_proxy_model) self._tab_widget = QTabWidget(self) self._tab_widget.currentChanged.connect(self._current_tab_changed) self._snapshot_model.rowsInserted.connect(self.on_new_iteration) self._job_label = QLabel(self) self._job_model = JobListProxyModel(self, 0, 0, 0, 0) self._job_model.setSourceModel(self._snapshot_model) self._job_view = QTableView(self) self._job_view.setVerticalScrollMode(QAbstractItemView.ScrollPerItem) self._job_view.setSelectionBehavior(QAbstractItemView.SelectRows) self._job_view.setSelectionMode(QAbstractItemView.SingleSelection) self._job_view.clicked.connect(self._job_clicked) self._open_files = {} self._job_view.setModel(self._job_model) self.running_time = QLabel("") self.plot_tool = PlotTool(ert, config_file) self.plot_tool.setParent(self) self.plot_button = QPushButton(self.plot_tool.getName()) self.plot_button.clicked.connect(self.plot_tool.trigger) self.plot_button.setEnabled(ert is not None) self.kill_button = QPushButton("Kill simulations") self.done_button = QPushButton("Done") self.done_button.setHidden(True) self.restart_button = QPushButton("Restart") self.restart_button.setHidden(True) self.show_details_button = QPushButton("Show details") self.show_details_button.setCheckable(True) size = 20 spin_movie = resourceMovie("loading.gif") spin_movie.setSpeed(60) spin_movie.setScaledSize(QSize(size, size)) spin_movie.start() self.processing_animation = QLabel() self.processing_animation.setMaximumSize(QSize(size, size)) self.processing_animation.setMinimumSize(QSize(size, size)) self.processing_animation.setMovie(spin_movie) button_layout = QHBoxLayout() button_layout.addWidget(self.processing_animation) button_layout.addWidget(self.running_time) button_layout.addStretch() button_layout.addWidget(self.show_details_button) button_layout.addWidget(self.plot_button) button_layout.addWidget(self.kill_button) button_layout.addWidget(self.done_button) button_layout.addWidget(self.restart_button) button_widget_container = QWidget() button_widget_container.setLayout(button_layout) layout = QVBoxLayout() layout.addWidget(self._total_progress_label) layout.addWidget(self._total_progress_bar) layout.addWidget(self._iteration_progress_label) layout.addWidget(self._progress_view) layout.addWidget(legend_view) layout.addWidget(self._tab_widget) layout.addWidget(self._job_label) layout.addWidget(self._job_view) layout.addWidget(button_widget_container) self.setLayout(layout) self.kill_button.clicked.connect(self.killJobs) self.done_button.clicked.connect(self.accept) self.restart_button.clicked.connect(self.restart_failed_realizations) self.show_details_button.clicked.connect(self.toggle_detailed_progress) self.simulation_done.connect(self._on_simulation_done) self.setMinimumWidth(self._minimum_width) self._setSimpleDialog() def _current_tab_changed(self, index: int): # Clear the selection in the other tabs for i in range(0, self._tab_widget.count()): if i != index: self._tab_widget.widget(i).clearSelection() def _setSimpleDialog(self) -> None: self._isDetailedDialog = False self._tab_widget.setVisible(False) self._job_label.setVisible(False) self._job_view.setVisible(False) self.show_details_button.setText("Show details") def _setDetailedDialog(self) -> None: self._isDetailedDialog = True self._tab_widget.setVisible(True) self._job_label.setVisible(True) self._job_view.setVisible(True) self.show_details_button.setText("Hide details") @Slot(QModelIndex, int, int) def on_new_iteration(self, parent: QModelIndex, start: int, end: int) -> None: if not parent.isValid(): index = self._snapshot_model.index(start, 0, parent) iter_row = start self._iteration_progress_label.setText( f"Progress for iteration {index.internalPointer().id}") widget = RealizationWidget(iter_row) widget.setSnapshotModel(self._snapshot_model) widget.currentChanged.connect(self._select_real) self._tab_widget.addTab( widget, f"Realizations for iteration {index.internalPointer().id}") @Slot(QModelIndex) def _job_clicked(self, index): if not index.isValid(): return selected_file = index.data(FileRole) if selected_file and selected_file not in self._open_files: job_name = index.siblingAtColumn(0).data() viewer = FileDialog( selected_file, job_name, index.row(), index.model().get_real(), index.model().get_iter(), self, ) self._open_files[selected_file] = viewer def remove_file(): """ We have sometimes seen this fail because the selected file is not in open file, without being able to reproduce the exception. """ try: self._open_files.pop(selected_file) except KeyError: logger = logging.getLogger(__name__) logger.exception( f"Failed to pop: {selected_file} from {self._open_files}" ) viewer.finished.connect(remove_file) elif selected_file in self._open_files: self._open_files[selected_file].raise_() @Slot(QModelIndex) def _select_real(self, index): step = 0 stage = 0 real = index.row() iter_ = index.model().get_iter() self._job_model.set_step(iter_, real, stage, step) self._job_label.setText( f"Realization id {index.data(RealIens)} in iteration {iter_}") self._job_view.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) def reject(self): return def closeEvent(self, QCloseEvent): if self._run_model.isFinished(): self.simulation_done.emit(self._run_model.hasRunFailed(), self._run_model.getFailMessage()) else: # Kill jobs if dialog is closed if self.killJobs() != QMessageBox.Yes: QCloseEvent.ignore() def startSimulation(self): self._run_model.reset() self._snapshot_model.reset() self._tab_widget.clear() evaluator_server_config = EvaluatorServerConfig() def run(): asyncio.set_event_loop(asyncio.new_event_loop()) self._run_model.startSimulations( evaluator_server_config=evaluator_server_config, ) simulation_thread = Thread(name="ert_gui_simulation_thread") simulation_thread.setDaemon(True) simulation_thread.run = run simulation_thread.start() self._ticker.start(1000) tracker = EvaluatorTracker( self._run_model, ee_con_info=evaluator_server_config.get_connection_info(), ) worker = TrackerWorker(tracker) worker_thread = QThread() worker.done.connect(worker_thread.quit) worker.consumed_event.connect(self._on_tracker_event) worker.moveToThread(worker_thread) self.simulation_done.connect(worker.stop) self._worker = worker self._worker_thread = worker_thread worker_thread.started.connect(worker.consume_and_emit) self._worker_thread.start() def killJobs(self): msg = "Are you sure you want to kill the currently running simulations?" kill_job = QMessageBox.question(self, "Kill simulations?", msg, QMessageBox.Yes | QMessageBox.No) if kill_job == QMessageBox.Yes: # Normally this slot would be invoked by the signal/slot system, # but the worker is busy tracking the evaluation. self._worker.request_termination() self.reject() return kill_job @Slot(bool, str) def _on_simulation_done(self, failed, failed_msg): self.processing_animation.hide() self.kill_button.setHidden(True) self.done_button.setHidden(False) self.restart_button.setVisible( self._run_model.has_failed_realizations()) self.restart_button.setEnabled(self._run_model.support_restart) self._total_progress_bar.setValue(100) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format( total_progress=100, phase_name=self._run_model.getPhaseName())) if failed: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("Simulations failed!".center(100)) msg.setDetailedText(failed_msg) msg.exec_() @Slot() def _on_ticker(self): runtime = self._run_model.get_runtime() self.running_time.setText(format_running_time(runtime)) @Slot(object) def _on_tracker_event(self, event): if isinstance(event, EndEvent): self.simulation_done.emit(event.failed, event.failed_msg) self._worker.stop() self._ticker.stop() elif isinstance(event, FullSnapshotEvent): if event.snapshot is not None: self._snapshot_model._add_snapshot(event.snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(total_progress=progress, phase_name=event.phase_name)) elif isinstance(event, SnapshotUpdateEvent): if event.partial_snapshot is not None: self._snapshot_model._add_partial_snapshot( event.partial_snapshot, event.iteration) self._progress_view.setIndeterminate(event.indeterminate) progress = int(event.progress * 100) self._total_progress_bar.setValue(progress) self._total_progress_label.setText( _TOTAL_PROGRESS_TEMPLATE.format(total_progress=progress, phase_name=event.phase_name)) def restart_failed_realizations(self): msg = QMessageBox(self) msg.setIcon(QMessageBox.Information) msg.setText( "Note that workflows will only be executed on the restarted " "realizations and that this might have unexpected consequences.") msg.setWindowTitle("Restart failed realizations") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) result = msg.exec_() if result == QMessageBox.Ok: self.restart_button.setVisible(False) self.kill_button.setVisible(True) self.done_button.setVisible(False) self._run_model.restart() self.startSimulation() @Slot() def toggle_detailed_progress(self): if self._isDetailedDialog: self._setSimpleDialog() else: self._setDetailedDialog() self.adjustSize()