class MonoidPreferencesWindow(QDialog): """ Applications Preferences window. """ def __init__(self, app, *args, **kwargs): super(MonoidPreferencesWindow, self).__init__(*args, **kwargs) self.setWindowTitle("Settings") self.app = app # Place the tab widget inside layout to get some margin. self.tabs = QTabWidget(self) self.tabs.currentChanged.connect(self.updateHeight) layout = QVBoxLayout(self) layout.addWidget(self.tabs) # Create each tab. self.generalTab = GeneralTab(self, app.settings) self.headerTab = HeaderTab(self, app.settings) # Add all the tabs. self.tabs.addTab(self.generalTab, "General") self.tabs.addTab(self.headerTab, "Header") def show(self, *args): """ Set the size to a fixed one after showing the window to disable the minimize and maximize button. """ super(MonoidPreferencesWindow, self).show(*args) self.setFixedSize(self.minimumSizeHint()) def updateHeight(self, index): """ Set the window height to the minimum required space. :param index: index of the currently selected tab """ # Resize all tabs. for i in range(self.tabs.count()): if i != index: widget = self.tabs.widget(i) widget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) widget.resize(widget.sizeHint()) widget.adjustSize() widget = self.tabs.widget(index) if widget: widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) widget.resize(widget.minimumSizeHint()) widget.adjustSize() # Resize the tab widget. self.tabs.resize(self.tabs.minimumSizeHint()) self.tabs.adjustSize() # Resize the window to the fixed minimum size. self.setFixedSize(self.minimumSizeHint()) self.adjustSize()
class MainGUI(QMainWindow): """The main GUI for azimuthal integration.""" _root_dir = osp.dirname(osp.abspath(__file__)) start_sgn = pyqtSignal() stop_sgn = pyqtSignal() quit_sgn = pyqtSignal() _db = RedisConnection() _SPECIAL_ANALYSIS_ICON_WIDTH = 100 _WIDTH, _HEIGHT = config['GUI_MAIN_GUI_SIZE'] def __init__(self, pause_ev, close_ev): """Initialization.""" super().__init__() self._pause_ev = pause_ev self._close_ev = close_ev self._input_update_ev = Event() self._input = MpInQueue(self._input_update_ev, pause_ev, close_ev) self._pulse_resolved = config["PULSE_RESOLVED"] self._queue = deque(maxlen=1) self.setAttribute(Qt.WA_DeleteOnClose) self.title = f"EXtra-foam {__version__} ({config['DETECTOR']})" self.setWindowTitle(self.title + " - main GUI") # ************************************************************* # Central widget # ************************************************************* self._ctrl_widgets = [] # book-keeping control widgets self._cw = QSplitter() self._cw.setChildrenCollapsible(False) self.setCentralWidget(self._cw) self._left_cw_container = QScrollArea() self._left_cw_container.setFrameShape(QFrame.NoFrame) self._left_cw = QTabWidget() self._right_cw_container = QScrollArea() self._right_cw_container.setFrameShape(QFrame.NoFrame) self._right_cw = QSplitter(Qt.Vertical) self._right_cw.setChildrenCollapsible(False) self._source_cw = self.createCtrlWidget(DataSourceWidget) self._ctrl_panel_cw = QTabWidget() self._analysis_cw = QWidget() self._statistics_cw = QWidget() self._special_analysis_cw = QWidget() self._util_panel_container = QWidget() self._util_panel_cw = QTabWidget() # ************************************************************* # Menu bar # ************************************************************* # self._menu_bar = self.menuBar() # file_menu = self._menu_bar.addMenu('&Config') # save_cfg = QAction('Save config', self) # file_menu.addAction(save_cfg) # load_cfg = QAction('Load config', self) # file_menu.addAction(load_cfg) # ************************************************************* # Tool bar # Note: the order of 'addAction` affect the unittest!!! # ************************************************************* self._tool_bar = self.addToolBar("Control") # make icon a bit larger self._tool_bar.setIconSize(1.25 * self._tool_bar.iconSize()) self._start_at = self.addAction("Start bridge", "start.png") self._start_at.triggered.connect(self.onStart) self._stop_at = self.addAction("Stop bridge", "stop.png") self._stop_at.triggered.connect(self.onStop) self._stop_at.setEnabled(False) self._tool_bar.addSeparator() image_tool_at = self.addAction("Image tool", "image_tool.png") image_tool_at.triggered.connect(lambda: (self._image_tool.show( ), self._image_tool.activateWindow())) open_poi_window_at = self.addAction("Pulse-of-interest", "poi.png") open_poi_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, PulseOfInterestWindow)) if not self._pulse_resolved: open_poi_window_at.setEnabled(False) pump_probe_window_at = self.addAction("Pump-probe", "pump-probe.png") pump_probe_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, PumpProbeWindow)) open_statistics_window_at = self.addAction("Correlation", "correlation.png") open_statistics_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, CorrelationWindow)) open_statistics_window_at = self.addAction("Histogram", "histogram.png") open_statistics_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, HistogramWindow)) open_bin2d_window_at = self.addAction("Binning", "binning.png") open_bin2d_window_at.triggered.connect( functools.partial(self.onOpenPlotWindow, BinningWindow)) self._tool_bar.addSeparator() open_file_stream_window_at = self.addAction("File streamer", "file_streamer.png") open_file_stream_window_at.triggered.connect( lambda: self.onOpenSatelliteWindow(FileStreamControllerWindow)) open_about_at = self.addAction("About EXtra-foam", "about.png") open_about_at.triggered.connect( lambda: self.onOpenSatelliteWindow(AboutWindow)) # ************************************************************* # Special analysis # ************************************************************* self._trxas_btn = self.addSpecial("tr_xas.png", TrXasWindow) # ************************************************************* # Miscellaneous # ************************************************************* # book-keeping opened windows self._plot_windows = WeakKeyDictionary() self._satellite_windows = WeakKeyDictionary() self._special_windows = WeakKeyDictionary() self._logger = GuiLogger(parent=self) logging.getLogger().addHandler(self._logger) self._thread_logger = ThreadLoggerBridge() self.quit_sgn.connect(self._thread_logger.stop) self._thread_logger_t = QThread() self._thread_logger.moveToThread(self._thread_logger_t) self._thread_logger_t.started.connect(self._thread_logger.recv) self._thread_logger.connectToMainThread(self) # For real time plot self._running = False self._plot_timer = QTimer() self._plot_timer.timeout.connect(self.updateAll) # For checking the connection to the Redis server self._redis_timer = QTimer() self._redis_timer.timeout.connect(self.pingRedisServer) self.__redis_connection_fails = 0 self._mon_proxy = MonProxy() # ************************************************************* # control widgets # ************************************************************* # analysis control widgets self.analysis_ctrl_widget = self.createCtrlWidget(AnalysisCtrlWidget) self.pump_probe_ctrl_widget = self.createCtrlWidget( PumpProbeCtrlWidget) self.pulse_filter_ctrl_widget = self.createCtrlWidget( PulseFilterCtrlWidget) # statistics control widgets self.bin_ctrl_widget = self.createCtrlWidget(BinCtrlWidget) self.histogram_ctrl_widget = self.createCtrlWidget(HistogramCtrlWidget) self.correlation_ctrl_widget = self.createCtrlWidget( CorrelationCtrlWidget) # special analysis control widgets (do not register them!!!) self._trxas_ctrl_widget = TrXasCtrlWidget() # ************************************************************* # status bar # ************************************************************* # StatusBar to display topic name self.statusBar().showMessage(f"TOPIC: {config['TOPIC']}") self.statusBar().setStyleSheet("QStatusBar{font-weight:bold;}") # ImageToolWindow is treated differently since it is the second # control window. self._image_tool = ImageToolWindow(queue=self._queue, pulse_resolved=self._pulse_resolved, parent=self) self.initUI() self.initConnections() self.updateMetaData() self.setMinimumSize(640, 480) self.resize(self._WIDTH, self._HEIGHT) self.show() def createCtrlWidget(self, widget_class): widget = widget_class(pulse_resolved=self._pulse_resolved) self._ctrl_widgets.append(widget) return widget def initUI(self): self.initLeftUI() self.initRightUI() self._cw.addWidget(self._left_cw_container) self._cw.addWidget(self._right_cw_container) self._cw.setSizes([self._WIDTH * 0.5, self._WIDTH * 0.5]) def initLeftUI(self): self._left_cw.setTabPosition(QTabWidget.TabPosition.West) self._left_cw.addTab(self._source_cw, "Data source") self._left_cw_container.setWidget(self._left_cw) self._left_cw_container.setWidgetResizable(True) def initRightUI(self): self.initCtrlUI() self.initUtilUI() self._right_cw.addWidget(self._ctrl_panel_cw) self._right_cw.addWidget(self._util_panel_container) self._ctrl_panel_cw.setFixedHeight( self._ctrl_panel_cw.minimumSizeHint().height()) self._right_cw_container.setWidget(self._right_cw) self._right_cw_container.setWidgetResizable(True) def initCtrlUI(self): self.initGeneralAnalysisUI() self.initStatisticsAnalysisUI() self.initSpecialAnalysisUI() self._ctrl_panel_cw.addTab(self._analysis_cw, "General analysis") self._ctrl_panel_cw.addTab(self._statistics_cw, "Statistics analysis") self._ctrl_panel_cw.addTab(self._special_analysis_cw, "Special analysis") def initGeneralAnalysisUI(self): layout = QVBoxLayout() layout.addWidget(self.analysis_ctrl_widget) layout.addWidget(self.pump_probe_ctrl_widget) layout.addWidget(self.pulse_filter_ctrl_widget) self._analysis_cw.setLayout(layout) def initStatisticsAnalysisUI(self): layout = QVBoxLayout() layout.addWidget(self.correlation_ctrl_widget) layout.addWidget(self.bin_ctrl_widget) layout.addWidget(self.histogram_ctrl_widget) self._statistics_cw.setLayout(layout) def initSpecialAnalysisUI(self): ctrl_widget = QTabWidget() ctrl_widget.setTabPosition(QTabWidget.TabPosition.East) ctrl_widget.addTab(self._trxas_ctrl_widget, "tr-XAS") icon_layout = QVBoxLayout() icon_layout.addWidget(self._trxas_btn) icon_layout.addStretch(1) layout = QHBoxLayout() layout.addWidget(ctrl_widget) layout.addLayout(icon_layout) self._special_analysis_cw.setLayout(layout) def initUtilUI(self): self._util_panel_cw.addTab(self._logger.widget, "Logger") self._util_panel_cw.setTabPosition(QTabWidget.TabPosition.South) layout = QVBoxLayout() layout.addWidget(self._util_panel_cw) self._util_panel_container.setLayout(layout) def initConnections(self): pass def connect_input_to_output(self, output): self._input.connect(output) @profiler("Update Plots", process_time=True) def updateAll(self): """Update all the plots in the main and child windows.""" if not self._running: return try: processed = self._input.get() self._queue.append(processed) except Empty: return # clear the previous plots no matter what comes next # for w in self._plot_windows.keys(): # w.reset() data = self._queue[0] self._image_tool.updateWidgetsF() for w in itertools.chain(self._special_windows, self._plot_windows): try: w.updateWidgetsF() except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() logger.debug( repr(traceback.format_tb(exc_traceback)) + repr(e)) logger.error(f"[Update plots] {repr(e)}") logger.debug(f"Plot train with ID: {data.tid}") def pingRedisServer(self): try: self._db.ping() if self.__redis_connection_fails > 0: # Note: Indeed, we do not have mechanism to recover from # a Redis server crash. It is recommended to restart # Extra-foam if you encounter this situation. logger.info("Reconnect to the Redis server!") self.__redis_connection_fails = 0 except ConnectionError: self.__redis_connection_fails += 1 rest_attempts = config["REDIS_MAX_PING_ATTEMPTS"] - \ self.__redis_connection_fails if rest_attempts > 0: logger.warning(f"No response from the Redis server! Shut " f"down after {rest_attempts} attempts ...") else: logger.warning(f"No response from the Redis server! " f"Shutting down!") self.close() def addAction(self, description, filename): icon = QIcon(osp.join(self._root_dir, "icons/" + filename)) action = QAction(icon, description, self) self._tool_bar.addAction(action) return action def addSpecial(self, filename, instance_type): btn = create_icon_button(filename, self._SPECIAL_ANALYSIS_ICON_WIDTH) btn.clicked.connect( lambda: self.openSpecialAnalysisWindow(instance_type)) return btn def onOpenPlotWindow(self, instance_type): """Open a plot window if it does not exist. Otherwise bring the opened window to the table top. """ if self.checkWindowExistence(instance_type, self._plot_windows): return return instance_type(self._queue, pulse_resolved=self._pulse_resolved, parent=self) def onOpenSatelliteWindow(self, instance_type): """Open a satellite window if it does not exist. Otherwise bring the opened window to the table top. """ if self.checkWindowExistence(instance_type, self._satellite_windows): return return instance_type(parent=self) def openSpecialAnalysisWindow(self, instance_type): """Open a special analysis window if it does not exist. Otherwise bring the opened window to the table top. """ if self.checkWindowExistence(instance_type, self._special_windows): return return instance_type(self._queue, parent=self) def checkWindowExistence(self, instance_type, windows): for key in windows: if isinstance(key, instance_type): key.activateWindow() return True return False def registerWindow(self, instance): self._plot_windows[instance] = 1 def unregisterWindow(self, instance): del self._plot_windows[instance] def registerSatelliteWindow(self, instance): self._satellite_windows[instance] = 1 def unregisterSatelliteWindow(self, instance): del self._satellite_windows[instance] def registerSpecialWindow(self, instance): self._special_windows[instance] = 1 def unregisterSpecialWindow(self, instance): del self._special_windows[instance] @property def input(self): return self._input def start(self): """Start running. ProcessWorker interface. """ self._thread_logger_t.start() self._plot_timer.start(config["GUI_PLOT_UPDATE_TIMER"]) self._redis_timer.start(config["REDIS_PING_ATTEMPT_INTERVAL"]) self._input.start() def onStart(self): if not self.updateMetaData(): return self.start_sgn.emit() self._start_at.setEnabled(False) self._stop_at.setEnabled(True) for widget in self._ctrl_widgets: widget.onStart() self._image_tool.onStart() self._running = True # starting to update plots self._input_update_ev.set() # notify update def onStop(self): """Actions taken before the end of a 'run'.""" self._running = False self.stop_sgn.emit() # TODO: wait for some signal self._start_at.setEnabled(True) self._stop_at.setEnabled(False) for widget in self._ctrl_widgets: widget.onStop() self._image_tool.onStop() def updateMetaData(self): """Update metadata from all the ctrl widgets. :returns bool: True if all metadata successfully parsed and emitted, otherwise False. """ for widget in self._ctrl_widgets: succeeded = widget.updateMetaData() if not succeeded: return False return self._image_tool.updateMetaData() @pyqtSlot(str, str) def onLogMsgReceived(self, ch, msg): if ch == 'log:debug': logger.debug(msg) elif ch == 'log:info': logger.info(msg) elif ch == 'log:warning': logger.warning(msg) elif ch == 'log:error': logger.error(msg) def closeEvent(self, QCloseEvent): # prevent from logging in the GUI when it has been closed logging.getLogger().removeHandler(self._logger) # tell all processes to close self._close_ev.set() # clean up the logger thread self.quit_sgn.emit() self._thread_logger_t.quit() self._thread_logger_t.wait() # shutdown pipeline workers and Redis server shutdown_all() super().closeEvent(QCloseEvent)