class _QtMenuBar(_AbstractMenuBar): def _menu_initialize(self, window=None): self._menus = dict() self._menu_actions = dict() self._menu_bar = QMenuBar() self._menu_bar.setNativeMenuBar(False) window = self._window if window is None else window window.setMenuBar(self._menu_bar) def _menu_add_submenu(self, name, desc): self._menus[name] = self._menu_bar.addMenu(desc) self._menu_actions[name] = dict() def _menu_add_button(self, menu_name, name, desc, func): menu = self._menus[menu_name] self._menu_actions[menu_name][name] = \ _QtAction(menu.addAction(desc, func))
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 BaseWindow(SiriusMainWindow): """Base class.""" def __init__(self, parent=None, prefix=_VACA_PREFIX): """Init.""" super().__init__(parent) self.prefix = prefix self._curr_dir = _os.path.abspath(_os.path.dirname(__file__)) def _setupUi(self): # menubar self.menubar = QMenuBar(self) self.menubar.setNativeMenuBar(False) self.setMenuBar(self.menubar) self.menu = self.menubar.addMenu("Open...") self._setupMenu() # auxiliar diagnostics widget self.auxdig_wid = None self._setupDiagWidget() # lattice widget self.lattice_wid = QSvgWidget( _os.path.join(self._curr_dir, self.SVG_FILE)) # screens view widget (create only one ScrnView) self._scrns_wids_dict = dict() self._currScrn = 0 scrn_wid = SiriusScrnView(parent=self, prefix=self.prefix, device=self._scrns[self._currScrn]) scrn_wid.setVisible(True) self._scrns_wids_dict[self._currScrn] = scrn_wid self.scrns_wid = QWidget() lay_scrns = QGridLayout(self.scrns_wid) lay_scrns.addWidget(scrn_wid) # correction widget self.corr_wid = QGroupBox('Screens and Correctors Panel') self._scrns_sel_bg = QButtonGroup(parent=self.corr_wid) self._scrns_sel_bg.setExclusive(True) self._setupScrnsCorrsWidget() vlay1 = QVBoxLayout() if self.auxdig_wid: vlay1.addWidget(self.auxdig_wid) vlay1.addWidget(self.scrns_wid) vlay2 = QVBoxLayout() vlay2.addWidget(self.lattice_wid) vlay2.addWidget(self.corr_wid) cw = QWidget() lay = QHBoxLayout(cw) lay.addLayout(vlay1) lay.addLayout(vlay2) self.setCentralWidget(cw) def _setupMenu(self): raise NotImplementedError def _setupScrnsCorrsWidget(self): raise NotImplementedError def _setupDiagWidget(self): raise NotImplementedError @Slot() def _setScrnWidget(self): scrn_obj = self._scrns_wids_dict[self._currScrn] scrn_obj.setVisible(False) sender = self.sender() self._currScrn = self._scrns_sel_bg.id(sender) if self._currScrn not in self._scrns_wids_dict.keys(): scrn_obj = SiriusScrnView(parent=self, prefix=self.prefix, device=self._scrns[self._currScrn]) self.scrns_wid.layout().addWidget(scrn_obj, 2, 0) self._scrns_wids_dict[self._currScrn] = scrn_obj else: scrn_obj = self._scrns_wids_dict[self._currScrn] self._scrns_wids_dict[self._currScrn].setVisible(True) def _create_headerline(self, labels): """Create and return a headerline.""" hl = QWidget() hl.setLayout(QHBoxLayout()) hl.layout().setContentsMargins(0, 9, 0, 0) glay = None for text, width in labels: if not width: if glay: hl.layout().addLayout(glay) hl.layout().addStretch() glay = QGridLayout() glay.setAlignment(Qt.AlignCenter) glay.setContentsMargins(0, 0, 0, 0) c = 0 else: label = QLabel(text, self) label.setStyleSheet(""" min-width:valueem; min-height:1.29em; max-height:1.29em; font-weight:bold; qproperty-alignment: AlignCenter; """.replace('value', str(width))) glay.addWidget(label, 0, c) c += 1 return hl def _create_scrn_summwidget(self, scrn_device, scrn_idx): """Create and return a screen detail widget.""" cb_scrn = QCheckBox(scrn_device.get_nickname(dev=True), self) self._scrns_sel_bg.addButton(cb_scrn) self._scrns_sel_bg.setId(cb_scrn, scrn_idx) if scrn_idx == self._currScrn: cb_scrn.setChecked(True) cb_scrn.clicked.connect(self._setScrnWidget) cb_scrn.setStyleSheet(""" min-width:6.5em; max-width:6.5em; font-weight:bold;""") led_camenbl = SiriusLedState( self, scrn_device.substitute(prefix=self.prefix, propty='CamEnbl-Sts')) led_camenbl.setStyleSheet("min-width:3.2em; max-width:3.2em;") cb_scrntype = PyDMEnumComboBox( self, scrn_device.substitute(prefix=self.prefix, propty='ScrnType-Sel')) cb_scrntype.setSizePolicy(QSzPlcy.Minimum, QSzPlcy.Fixed) cb_scrntype.setStyleSheet("min-width:4.5em;max-width:4.5em;") lb_scrntype = PyDMLabel( self, scrn_device.substitute(prefix=self.prefix, propty='ScrnType-Sts')) lb_scrntype.setStyleSheet("min-width:4.5em; max-width:4.5em;") lb_scrntype.setAlignment(Qt.AlignCenter) led_scrntype = PyDMLed(self, scrn_device.substitute(prefix=self.prefix, propty='ScrnType-Sts'), color_list=[ PyDMLed.LightGreen, PyDMLed.Red, PyDMLed.Red, PyDMLed.Yellow ]) led_scrntype.shape = 2 led_scrntype.setStyleSheet("""min-width:4.5em; max-width:4.5em;""") wid = QWidget() lay = QGridLayout(wid) lay.setAlignment(Qt.AlignCenter) lay.addWidget(cb_scrn, 1, 1) lay.addWidget(led_camenbl, 1, 2) lay.addWidget(cb_scrntype, 1, 3) lay.addWidget(lb_scrntype, 1, 4) lay.addWidget(led_scrntype, 2, 4) return wid def _create_corr_summwidget(self, corr): """Create and return a corrector detail widget.""" wid = QWidget() wid.setSizePolicy(QSzPlcy.Preferred, QSzPlcy.Maximum) lay = QGridLayout(wid) lay.setContentsMargins(0, 0, 0, 0) lay.setAlignment(Qt.AlignCenter) propty_sp = 'Current-SP' if corr.sec == 'LI' else 'Kick-SP' propty_mon = propty_sp.replace('SP', 'Mon') led = SiriusLedState( self, corr.substitute(prefix=self.prefix, propty='PwrState-Sts')) led.setStyleSheet("max-width:1.29em;") lay.addWidget(led, 1, 1) nickname = corr.get_nickname(sec=corr.sec == 'LI', dev=True) pb = QPushButton(nickname, self) if corr.dis == 'PU': util.connect_window(pb, PUDetailWindow, parent=self, devname=corr) else: util.connect_window(pb, PSDetailWindow, parent=self, psname=corr) pb.setStyleSheet(""" min-width:6em; max-width:6em; min-height:1.29em;""") lay.addWidget(pb, 1, 2) sp_kick = PyDMSpinboxScrollbar( self, corr.substitute(prefix=self.prefix, propty=propty_sp)) sp_kick.setStyleSheet("QDoubleSpinBox{min-width:4em; max-width:4em; }" "QScrollBar{max-width:4em;}") sp_kick.spinbox.precisionFromPV = False sp_kick.spinbox.precision = 1 sp_kick.scrollbar.limitsFromPV = True lay.addWidget(sp_kick, 1, 3, 2, 1) lb_kick = PyDMLabel( self, corr.substitute(prefix=self.prefix, propty=propty_mon)) lb_kick.setStyleSheet(""" min-width:5em; max-width:5em; min-height:1.29em;""") lb_kick.showUnits = True lb_kick.precisionFromPV = False lb_kick.precision = 1 lb_kick.setAlignment(Qt.AlignCenter) lay.addWidget(lb_kick, 1, 4) return wid
class BONormEdit(SiriusMainWindow): """Widget to perform optics adjust in normalized configurations.""" normConfigChanged = Signal(float, dict) def __init__(self, parent=None, prefix='', ramp_config=None, norm_config=None, time=None, energy=None, magnets=dict(), conn_sofb=None, tunecorr_configname=None, chromcorr_configname=None): """Initialize object.""" super().__init__(parent) self.setWindowTitle('Edit Normalized Configuration') self.setObjectName('BOApp') self.prefix = prefix self.ramp_config = ramp_config self.norm_config = _dcopy(norm_config) self.time = time self.energy = energy self._aux_magnets = magnets self._conn_sofb = conn_sofb self._tunecorr = BOTuneCorr(tunecorr_configname) self._chromcorr = BOChromCorr(chromcorr_configname) self._reference = _dcopy(norm_config) self._currChrom = self._estimateChrom(use_ref=True) self._deltas = { 'kicks': dict(), 'factorH': 0.0, 'factorV': 0.0, 'tuneX': 0.0, 'tuneY': 0.0, 'chromX': self._currChrom[0], 'chromY': self._currChrom[1], } self._setupUi() self._setupMenu() self.verifySync() # ---------- setup/build layout ---------- def _setupUi(self): self.label_description = QLabel( '<h2>'+self.norm_config['label']+'</h2>', self) self.label_description.setAlignment(Qt.AlignCenter) self.label_time = QLabel('<h2>T = '+str(self.time)+'ms</h2>', self) self.label_time.setAlignment(Qt.AlignCenter) self.strengths = self._setupStrengthWidget() self.orbit = self._setupOrbitWidget() self.tune = self._setupTuneWidget() self.chrom = self._setupChromWidget() self.bt_apply = QPushButton(qta.icon('fa5s.angle-right'), '', self) self.bt_apply.setToolTip('Apply Changes to Machine') self.bt_apply.setStyleSheet('icon-size: 30px 30px;') self.bt_apply.clicked.connect(self._updateRampConfig) cw = QWidget() lay = QGridLayout() lay.setVerticalSpacing(10) lay.setHorizontalSpacing(10) lay.addWidget(self.label_description, 0, 0, 1, 2) lay.addWidget(self.label_time, 1, 0, 1, 2) lay.addWidget(self.strengths, 2, 0, 4, 1) lay.addWidget(self.orbit, 2, 1) lay.addWidget(self.tune, 3, 1) lay.addWidget(self.chrom, 4, 1) lay.addWidget(self.bt_apply, 5, 1) lay.setColumnStretch(0, 2) lay.setColumnStretch(1, 2) lay.setRowStretch(0, 2) lay.setRowStretch(1, 2) lay.setRowStretch(2, 8) lay.setRowStretch(3, 8) lay.setRowStretch(4, 8) lay.setRowStretch(5, 1) cw.setLayout(lay) cw.setStyleSheet(""" QGroupBox::title{ subcontrol-origin: margin; subcontrol-position: top center; padding: 0 2px 0 2px;}""") cw.setFocusPolicy(Qt.StrongFocus) self.setCentralWidget(cw) def _setupMenu(self): self.menubar = QMenuBar(self) self.layout().setMenuBar(self.menubar) self.menu = self.menubar.addMenu('Options') self.act_saveas = self.menu.addAction('Save as...') self.act_saveas.triggered.connect(self._showSaveAsPopup) self._undo_stack = QUndoStack(self) self.act_undo = self._undo_stack.createUndoAction(self, 'Undo') self.act_undo.setShortcut(QKeySequence.Undo) self.menu.addAction(self.act_undo) self.act_redo = self._undo_stack.createRedoAction(self, 'Redo') self.act_redo.setShortcut(QKeySequence.Redo) self.menu.addAction(self.act_redo) def _setupStrengthWidget(self): scrollarea = QScrollArea() self.nconfig_data = QWidget() flay_configdata = QFormLayout() psnames = self._get_PSNames() self._map_psnames2wigdets = dict() for ps in psnames: ps = SiriusPVName(ps) if ps in ramp.BoosterRamp.PSNAME_DIPOLES: ps_value = QLabel(str(self.norm_config[ps])+' GeV', self) flay_configdata.addRow(QLabel(ps + ': ', self), ps_value) else: ps_value = QDoubleSpinBoxPlus(self.nconfig_data) ps_value.setDecimals(6) ps_value.setMinimum(-10000) ps_value.setMaximum(10000) ps_value.setValue(self.norm_config[ps]) ps_value.valueChanged.connect(self._handleStrenghtsSet) if ps.dev in {'QD', 'QF', 'QS'}: unit_txt = '1/m' elif ps.dev in {'SD', 'SF'}: unit_txt = '1/m²' else: unit_txt = 'urad' label_unit = QLabel(unit_txt, self) label_unit.setStyleSheet("min-width:2.5em; max-width:2.5em;") hb = QHBoxLayout() hb.addWidget(ps_value) hb.addWidget(label_unit) flay_configdata.addRow(QLabel(ps + ': ', self), hb) ps_value.setObjectName(ps) ps_value.setStyleSheet("min-height:1.29em; max-height:1.29em;") self._map_psnames2wigdets[ps] = ps_value self.nconfig_data.setObjectName('data') self.nconfig_data.setStyleSheet(""" #data{background-color: transparent;}""") self.nconfig_data.setLayout(flay_configdata) scrollarea.setWidget(self.nconfig_data) self.cb_checklims = QCheckBox('Set limits according to energy', self) self.cb_checklims.setChecked(False) self.cb_checklims.stateChanged.connect(self._handleStrengtsLimits) self.bt_graph = QPushButton(qta.icon('mdi.chart-line'), '', self) self.bt_graph.clicked.connect(self._show_kicks_graph) gbox = QGroupBox() gbox.setObjectName('strengths') gbox.setStyleSheet('#strengths{min-width:20em;}') glay = QGridLayout() glay.addWidget(scrollarea, 0, 0, 1, 2) glay.addWidget(self.cb_checklims, 1, 0, alignment=Qt.AlignLeft) glay.addWidget(self.bt_graph, 1, 1, alignment=Qt.AlignRight) gbox.setLayout(glay) return gbox def _setupOrbitWidget(self): self.bt_get_kicks = QPushButton('Get Kicks from SOFB', self) self.bt_get_kicks.clicked.connect(self._handleGetKicksFromSOFB) label_correctH = QLabel('Correct H', self, alignment=Qt.AlignRight | Qt.AlignVCenter) self.sb_correctH = QDoubleSpinBoxPlus(self) self.sb_correctH.setValue(self._deltas['factorH']) self.sb_correctH.setDecimals(1) self.sb_correctH.setMinimum(-10000) self.sb_correctH.setMaximum(10000) self.sb_correctH.setSingleStep(0.1) self.sb_correctH.setObjectName('factorH') self.sb_correctH.editingFinished.connect(self._handleCorrFactorsSet) labelH = QLabel('%', self) label_correctV = QLabel('Correct V', self, alignment=Qt.AlignRight | Qt.AlignVCenter) self.sb_correctV = QDoubleSpinBoxPlus(self) self.sb_correctV.setValue(self._deltas['factorV']) self.sb_correctV.setDecimals(1) self.sb_correctV.setMinimum(-10000) self.sb_correctV.setMaximum(10000) self.sb_correctV.setSingleStep(0.1) self.sb_correctV.setObjectName('factorV') self.sb_correctV.editingFinished.connect(self._handleCorrFactorsSet) labelV = QLabel('%', self) self.bt_update_ref_orbit = QPushButton('Update reference', self) self.bt_update_ref_orbit.clicked.connect( _part(self._updateReference, 'corrs')) gbox = QGroupBox('Orbit', self) lay = QGridLayout() lay.addWidget(self.bt_get_kicks, 0, 0, 1, 4) lay.addWidget(label_correctH, 1, 0) lay.addWidget(self.sb_correctH, 1, 2) lay.addWidget(labelH, 1, 3) lay.addWidget(label_correctV, 2, 0) lay.addWidget(self.sb_correctV, 2, 2) lay.addWidget(labelV, 2, 3) lay.addWidget(self.bt_update_ref_orbit, 3, 2, 1, 2) lay.setColumnStretch(0, 16) lay.setColumnStretch(1, 1) lay.setColumnStretch(2, 14) lay.setColumnStretch(3, 2) gbox.setLayout(lay) return gbox def _setupTuneWidget(self): for cord in ['X', 'Y']: setattr(self, 'label_deltaTune'+cord, QLabel('Δν<sub>'+cord+'</sub>: ')) lab = getattr(self, 'label_deltaTune'+cord) lab.setStyleSheet("min-width:1.55em; max-width:1.55em;") setattr(self, 'sb_deltaTune'+cord, QDoubleSpinBoxPlus(self)) sb = getattr(self, 'sb_deltaTune'+cord) sb.setDecimals(6) sb.setMinimum(-5) sb.setMaximum(5) sb.setSingleStep(0.0001) sb.setObjectName('tune'+cord) sb.editingFinished.connect(self._handleDeltaTuneSet) label_KL = QLabel('<h4>ΔKL [1/m]</h4>', self) label_KL.setStyleSheet("""min-height:1.55em; max-height:1.55em; qproperty-alignment: AlignCenter;""") self.l_deltaKLQF = QLabel('', self) self.l_deltaKLQD = QLabel('', self) self.bt_update_ref_deltaKL = QPushButton('Update reference', self) self.bt_update_ref_deltaKL.clicked.connect( _part(self._updateReference, 'quads')) gbox = QGroupBox('Tune', self) lay = QGridLayout() lay.addWidget(self.label_deltaTuneX, 1, 0) lay.addWidget(self.sb_deltaTuneX, 1, 1) lay.addWidget(self.label_deltaTuneY, 1, 3) lay.addWidget(self.sb_deltaTuneY, 1, 4) lay.addWidget(label_KL, 3, 0, 1, 5) lay.addWidget(QLabel('QF: '), 4, 0) lay.addWidget(self.l_deltaKLQF, 4, 1) lay.addWidget(QLabel('QD: '), 4, 3) lay.addWidget(self.l_deltaKLQD, 4, 4) lay.addWidget(self.bt_update_ref_deltaKL, 6, 3, 1, 2) lay.setVerticalSpacing(6) lay.setColumnStretch(0, 2) lay.setColumnStretch(1, 4) lay.setColumnStretch(2, 1) lay.setColumnStretch(3, 2) lay.setColumnStretch(4, 4) lay.setRowStretch(0, 1) lay.setRowStretch(1, 2) lay.setRowStretch(2, 1) lay.setRowStretch(3, 2) lay.setRowStretch(4, 2) lay.setRowStretch(5, 1) lay.setRowStretch(6, 2) gbox.setLayout(lay) return gbox def _setupChromWidget(self): for cord in ['X', 'Y']: setattr(self, 'label_Chrom'+cord, QLabel('ξ<sub>'+cord+'</sub>: ')) lab = getattr(self, 'label_Chrom'+cord) lab.setStyleSheet("min-width:1.55em; max-width:1.55em;") setattr(self, 'sb_Chrom'+cord, QDoubleSpinBoxPlus(self)) sb = getattr(self, 'sb_Chrom'+cord) sb.setDecimals(6) sb.setMinimum(-5) sb.setMaximum(5) sb.setSingleStep(0.0001) sb.setObjectName('chrom'+cord) sb.setValue(self._deltas['chrom'+cord]) sb.editingFinished.connect(self._handleChromSet) label_SL = QLabel('<h4>ΔSL [1/m<sup>2</sup>]</h4>', self) label_SL.setStyleSheet("""min-height:1.55em; max-height:1.55em; qproperty-alignment: AlignCenter;""") self.l_deltaSLSF = QLabel('', self) self.l_deltaSLSD = QLabel('', self) self.bt_update_ref_deltaSL = QPushButton('Update reference', self) self.bt_update_ref_deltaSL.clicked.connect( _part(self._updateReference, 'sexts')) gbox = QGroupBox('Chromaticity', self) lay = QGridLayout() lay.addWidget(self.label_ChromX, 1, 0) lay.addWidget(self.sb_ChromX, 1, 1) lay.addWidget(self.label_ChromY, 1, 3) lay.addWidget(self.sb_ChromY, 1, 4) lay.addWidget(label_SL, 3, 0, 1, 5) lay.addWidget(QLabel('SF: '), 4, 0) lay.addWidget(self.l_deltaSLSF, 4, 1) lay.addWidget(QLabel('SD: '), 4, 3) lay.addWidget(self.l_deltaSLSD, 4, 4) lay.addWidget(self.bt_update_ref_deltaSL, 6, 3, 1, 2) lay.setVerticalSpacing(6) lay.setColumnStretch(0, 2) lay.setColumnStretch(1, 4) lay.setColumnStretch(2, 1) lay.setColumnStretch(3, 2) lay.setColumnStretch(4, 4) lay.setRowStretch(0, 1) lay.setRowStretch(1, 2) lay.setRowStretch(2, 1) lay.setRowStretch(3, 2) lay.setRowStretch(4, 2) lay.setRowStretch(5, 1) lay.setRowStretch(6, 2) gbox.setLayout(lay) return gbox # ---------- server communication ---------- def _save(self, name): try: nconf = ramp.BoosterNormalized() nconf.value = self.norm_config nconf.save(new_name=name) except _ConfigDBException as err: QMessageBox.critical(self, 'Error', str(err), QMessageBox.Ok) def _showSaveAsPopup(self): self._saveAsPopup = _SaveConfigDialog('bo_normalized', self) self._saveAsPopup.configname.connect(self._save) self._saveAsPopup.open() def verifySync(self): if self.ramp_config is None: return if not self.ramp_config.verify_ps_normalized_synchronized( self.time, value=self.norm_config): self.label_time.setStyleSheet('color: red;') self.label_description.setStyleSheet('color: red;') self.setToolTip("There are unsaved changes") else: self.label_time.setStyleSheet('color: black;') self.label_description.setStyleSheet('color: black;') self.setToolTip("") # ---------- strengths ---------- def _handleStrenghtsSet(self, new_value): psname = self.sender().objectName() self._stack_command( self.sender(), self.norm_config[psname], new_value, message='set '+psname+' strength to {}'.format(new_value)) self.norm_config[psname] = new_value self.verifySync() def _handleStrengtsLimits(self, state): psnames = list(self.norm_config.keys()) psnames.remove('BO-Fam:PS-B-1') psnames.remove('BO-Fam:PS-B-2') psnames.remove('label') if state: for ps in psnames: ps_value = self.nconfig_data.findChild( QDoubleSpinBoxPlus, name=ps) ma = _MASearch.conv_psname_2_psmaname(ps) aux = self._aux_magnets[ma] currs = (aux.current_min, aux.current_max) lims = aux.conv_current_2_strength( currents=currs, strengths_dipole=self.energy) ps_value.setMinimum(min(lims)) ps_value.setMaximum(max(lims)) else: for ps in psnames: ps_value = self.nconfig_data.findChild( QDoubleSpinBoxPlus, name=ps) ps_value.setMinimum(-10000) ps_value.setMaximum(10000) def _updateStrenghtsWidget(self, pstype): psnames = self._get_PSNames(pstype) wid2change = psnames if psnames else list(self.norm_config.keys()) for wid in wid2change: value = self.norm_config[wid] self._map_psnames2wigdets[wid].setValue(value) # ---------- orbit ---------- def _updateCorrKicks(self): for psname, dkick in self._deltas['kicks'].items(): corr_factor = self._deltas['factorV'] if 'CV' in psname \ else self._deltas['factorH'] corr_factor /= 100 self.norm_config[psname] = self._reference[psname] + \ dkick*corr_factor def _handleGetKicksFromSOFB(self): if not self._conn_sofb.connected: QMessageBox.warning( self, 'Not Connected', 'There are not connected PVs!', QMessageBox.Ok) return dkicks = self._conn_sofb.get_deltakicks() if not dkicks: QMessageBox.warning( self, 'Could not get kicks', 'Could not get kicks from SOFB!', QMessageBox.Ok) return self._deltas['kicks'] = dkicks self._updateCorrKicks() self._updateStrenghtsWidget('corrs') self.verifySync() def _handleCorrFactorsSet(self): widget = self.sender() factor = widget.objectName() dim = ' vertical ' if factor == 'factorV' else ' horizantal ' new_value = widget.value() self._stack_command( widget, self._deltas[factor], new_value, message='set'+dim+'orbit correction factor to {}'.format( new_value)) self._deltas[factor] = new_value self._updateCorrKicks() self._updateStrenghtsWidget('corrs') self.verifySync() def _resetOrbitChanges(self): self._deltas['kicks'] = dict() self._deltas['factorH'] = 0.0 self._deltas['factorV'] = 0.0 self.sb_correctH.setValue(0.0) self.sb_correctV.setValue(0.0) # ---------- tune ---------- def _handleDeltaTuneSet(self): widget = self.sender() tune = widget.objectName() dim = ' vertical ' if tune == 'tuneY' else ' horizantal ' new_value = widget.value() self._stack_command( widget, self._deltas[tune], new_value, message='set'+dim+'delta tune to {}'.format( new_value)) self._deltas[tune] = new_value self._updateDeltaKL() def _updateDeltaKL(self): self._deltaKL = self._tunecorr.calculate_deltaKL( [self._deltas['tuneX'], self._deltas['tuneY']]) self.l_deltaKLQF.setText('{: 4f}'.format(self._deltaKL[0])) self.l_deltaKLQD.setText('{: 4f}'.format(self._deltaKL[1])) self.norm_config['BO-Fam:PS-QF'] = \ self._reference['BO-Fam:PS-QF'] + self._deltaKL[0] self.norm_config['BO-Fam:PS-QD'] = \ self._reference['BO-Fam:PS-QD'] + self._deltaKL[1] self._updateStrenghtsWidget('quads') self.verifySync() def _resetTuneChanges(self): self.sb_deltaTuneX.setValue(0) self.sb_deltaTuneY.setValue(0) self._deltaKL = [0.0, 0.0] self.l_deltaKLQF.setText('{: 6f}'.format(self._deltaKL[0])) self.l_deltaKLQD.setText('{: 6f}'.format(self._deltaKL[1])) # ---------- chromaticity ---------- def _estimateChrom(self, use_ref=False): nom_SL = self._chromcorr.nominal_intstrengths.flatten() if use_ref: curr_SL = _np.array([self._reference['BO-Fam:PS-SF'], self._reference['BO-Fam:PS-SD']]) else: curr_SL = _np.array([self.norm_config['BO-Fam:PS-SF'], self.norm_config['BO-Fam:PS-SD']]) delta_SL = curr_SL - nom_SL return self._chromcorr.calculate_Chrom(delta_SL) def _handleChromSet(self): widget = self.sender() chrom = widget.objectName() dim = ' vertical ' if chrom == 'chromY' else ' horizantal ' new_value = widget.value() self._stack_command( widget, self._deltas[chrom], new_value, message='set'+dim+'chromaticity to {}'.format( new_value)) self._deltas[chrom] = new_value self._updateDeltaSL() def _updateDeltaSL(self): desired_Chrom = _np.array([self._deltas['chromX'], self._deltas['chromY']]) deltas = desired_Chrom - self._currChrom self._deltaSL = self._chromcorr.calculate_deltaSL( [deltas[0], deltas[1]]) self.l_deltaSLSF.setText('{: 4f}'.format(self._deltaSL[0])) self.l_deltaSLSD.setText('{: 4f}'.format(self._deltaSL[1])) self.norm_config['BO-Fam:PS-SF'] = \ self._reference['BO-Fam:PS-SF'] + self._deltaSL[0] self.norm_config['BO-Fam:PS-SD'] = \ self._reference['BO-Fam:PS-SD'] + self._deltaSL[1] self._updateStrenghtsWidget('sexts') self.verifySync() def _resetChromChanges(self): self._currChrom = self._estimateChrom(use_ref=True) self.sb_ChromX.setValue(self._currChrom[0]) self.sb_ChromY.setValue(self._currChrom[1]) self._deltaSL = [0.0, 0.0] self.l_deltaSLSF.setText('{: 6f}'.format(self._deltaSL[0])) self.l_deltaSLSD.setText('{: 6f}'.format(self._deltaSL[1])) # ---------- update methods ---------- def _updateReference(self, pstype): psnames = self._get_PSNames(pstype) for ps in psnames: self._reference[ps] = self.norm_config[ps] if pstype == 'corrs': self._resetOrbitChanges() elif pstype == 'quads': self._resetTuneChanges() elif pstype == 'sexts': self._resetChromChanges() else: self._resetOrbitChanges() self._resetTuneChanges() self._resetChromChanges() self.verifySync() def _updateRampConfig(self): if self.norm_config is not None: self.normConfigChanged.emit(self.time, _dcopy(self.norm_config)) def updateTime(self, time): """Update norm config time.""" self.time = time self.label_time.setText('<h2>T = '+str(time)+'ms</h2>') self.energy = self.ramp_config.ps_waveform_interp_energy(time) self._handleStrengtsLimits(self.cb_checklims.checkState()) self.verifySync() def updateLabel(self, label): """Update norm config label.""" self.norm_config['label'] = label self.label_description.setText('<h2>'+label+'</h2>') self.verifySync() @Slot(str, str) def updateSettings(self, tunecorr_configname, chromcorr_configname): self._tunecorr = BOTuneCorr(tunecorr_configname) self._chromcorr = BOChromCorr(chromcorr_configname) self._updateDeltaKL() self._estimateChrom(use_ref=True) self._updateDeltaSL() # ---------- handle undo redo stack ---------- def _stack_command(self, widget, old_value, new_value, message): global _flag_stack_next_command, _flag_stacking if _flag_stack_next_command and (old_value != new_value): _flag_stacking = True command = _UndoRedoSpinbox(widget, old_value, new_value, message) self._undo_stack.push(command) else: _flag_stack_next_command = True # ---------- helper methods ---------- def _get_PSNames(self, pstype=None): psnames = list() if pstype == 'corrs': psnames = _PSSearch.get_psnames({'sec': 'BO', 'dev': 'C(V|H)'}) elif pstype == 'quads': psnames = ['BO-Fam:PS-QF', 'BO-Fam:PS-QD'] elif pstype == 'sexts': psnames = ['BO-Fam:PS-SF', 'BO-Fam:PS-SD'] else: psnames = _PSSearch.get_psnames({'sec': 'BO', 'sub': 'Fam'}) psnames.extend(_PSSearch.get_psnames({'sec': 'BO', 'dev': 'QS'})) psnames.extend(_PSSearch.get_psnames({'sec': 'BO', 'dev': 'CH'})) psnames.extend(_PSSearch.get_psnames({'sec': 'BO', 'dev': 'CV'})) return psnames def _show_kicks_graph(self): strenghts_dict = _dcopy(self.norm_config) strenghts_dict.pop('label') graph = _ShowCorrectorKicks(self, self.time, strenghts_dict) graph.show()