def setup_plot_spectrum_actions(self, context_menu, table): plot_spectrum_action = QAction(self.GRAPH_ICON, "Plot spectrum (values only)", self) plot_spectrum_action.triggered.connect( partial(self.presenter.action_plot_spectrum, table)) plot_spectrum_with_errors_action = QAction( self.GRAPH_ICON, "Plot spectrum (values + errors)", self) plot_spectrum_with_errors_action.triggered.connect( partial(self.presenter.action_plot_spectrum_with_errors, table)) overplot_spectrum_action = QAction(self.GRAPH_ICON, "Overplot spectrum (values only)", self) overplot_spectrum_action.triggered.connect( partial(self.presenter.action_overplot_spectrum, table)) overplot_spectrum_with_errors_action = QAction( self.GRAPH_ICON, "Overplot spectrum (values + errors)", self) overplot_spectrum_with_errors_action.triggered.connect( partial(self.presenter.action_overplot_spectrum_with_errors, table)) overplot_spectrum_action.setEnabled(can_overplot()) overplot_spectrum_with_errors_action.setEnabled(can_overplot()) separator = QAction(self) separator.setSeparator(True) list( map(context_menu.addAction, [ plot_spectrum_action, plot_spectrum_with_errors_action, separator, overplot_spectrum_action, overplot_spectrum_with_errors_action ]))
class Tool(object): def __init__( self, name, help_link="", icon=None, enabled=True, checkable=False, popup_menu=False, ): super(Tool, self).__init__() self.__icon = icon self.__name = name self.__parent = None self.__enabled = enabled self.__checkable = checkable self.__help_link = help_link self.__is_popup_menu = popup_menu self.__action = QAction(self.getIcon(), self.getName(), None) self.__action.setIconText(self.getName()) self.__action.setEnabled(self.isEnabled()) self.__action.setCheckable(checkable) self.__action.triggered.connect(self.trigger) HelpCenter.addHelpToAction(self.__action, self.getHelpLink()) def getIcon(self): return self.__icon def getName(self): return self.__name def trigger(self): raise NotImplementedError() def setParent(self, parent): self.__parent = parent self.__action.setParent(parent) def parent(self): return self.__parent def isEnabled(self): return self.__enabled def getHelpLink(self): return self.__help_link def getAction(self): return self.__action def setVisible(self, visible): self.__action.setVisible(visible) def setEnabled(self, enabled): self.__action.setEnabled(enabled) def isPopupMenu(self): return self.__is_popup_menu
def __init__(self, env): super().__init__() self._env = env self._extra_menu_actions = [] self._extra_toolbar_actions = [] self.update_title() main_menu = self.menuBar() self.main_menu = main_menu project_menu = main_menu.addMenu("&Project") toolbar = self.addToolBar("Top") self.toolbar = toolbar new_action = QAction("&New", self) new_action.setShortcut("Ctrl+N") new_action.setStatusTip("Create a new empty project") new_action.triggered.connect(lambda: env.new()) project_menu.addAction(new_action) open_action = QAction("&Open...", self) open_action.setShortcut("Ctrl+O") open_action.setStatusTip("Open an existing project") open_action.triggered.connect(lambda: env.open()) project_menu.addAction(open_action) save_action = QAction("&Save", self) self.save_action = save_action save_action.setShortcut("Ctrl+S") save_action.setIcon(QIcon.fromTheme("document-save")) #save_action.setIcon(self.style().standardIcon( # self.style().SP_DialogSaveButton)) save_action.setStatusTip("Save project") save_action.setEnabled(False) save_action.triggered.connect(lambda: env.save()) project_menu.addAction(save_action) toolbar.addAction(save_action) save_as_action = QAction("Save &As...", self) save_as_action.setStatusTip("Save project under a new name") save_as_action.triggered.connect(lambda: env.save_as()) project_menu.addAction(save_as_action) quit_action = QAction("&Quit", self) quit_action.setShortcut("Ctrl+Q") quit_action.setStatusTip("Quit Hildegard") quit_action.triggered.connect(self.handle_quit) project_menu.addAction(quit_action) self.tabs = QTabWidget() self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect( lambda index: env.close(self.tabs.widget(index).entity, quit=True)) self.tabs.currentChanged.connect(self._handle_switch_to_tab) self.setCentralWidget(self.tabs) self.statusBar()
def customMenuRequested(self, position): """Builds a custom menu for items items""" index = self.indexAt(position) # type: QModelIndex menu = QMenu(self) if index.isValid(): if index.data(EnsembleModel.data_type_role ) == WorkspaceDataType.Ensemble: # Allow renaming the ensemble via the context menu rename_action = QAction("Rename Collection", menu) rename_action.triggered.connect(self._rename_action) menu.addAction(rename_action) # Allow toggling the active ensemble via the context menu # * there can only be at most 1 active ensemble # * there are only 0 active ensembles when data has not yet been loaded ??? # * opening data updates the active ensemble to that data is_active = index.data(EnsembleModel.active_role) active_text = "Active" toggle_active_action = QAction(active_text, menu) toggle_active_action.setCheckable(True) if is_active is True: toggle_active_action.setChecked(True) else: toggle_active_action.setChecked(False) toggle_active_action.setText(f"Not {active_text}") # Make sure to update the model with the active / deactivated ensemble toggle_active_action.toggled.connect(self._set_active_action) # Don't allow deactivating the active ensemble if there is only one loaded if self.model().rowCount() == 1: toggle_active_action.setEnabled(False) menu.addAction(toggle_active_action) menu.addSeparator() remove_text = "Remove " data_type_role = index.data(EnsembleModel.data_type_role) if data_type_role == WorkspaceDataType.Ensemble: remove_text += "Ensemble" elif data_type_role == WorkspaceDataType.Catalog: remove_text += "Catalog" elif data_type_role == WorkspaceDataType.Intent: remove_text += "Item" remove_action = QAction(remove_text, menu) remove_action.triggered.connect(self._remove_action) menu.addAction(remove_action) else: create_ensemble_action = QAction("Create New Collection", menu) create_ensemble_action.triggered.connect( self._create_ensemble_action) menu.addAction(create_ensemble_action) # Display menu wherever the user right-clicked menu.popup(self.viewport().mapToGlobal(position))
class TreeViewMenu(QMenu): def __init__(self, *args): QMenu.__init__(self, *args) # create new group action self.actionCreate_New_Group = QAction(self) self.actionCreate_New_Group.setEnabled(True) self.actionCreate_New_Group.setObjectName("actionCreate_New_Group") self.addAction(self.actionCreate_New_Group) self.actionDismiss_Group = QAction(self) self.actionDismiss_Group.setEnabled(True) self.actionDismiss_Group.setObjectName("actionDismiss_Group") self.addAction(self.actionDismiss_Group) self.actionAdd_To_Group = QAction(self) self.actionAdd_To_Group.setEnabled(True) self.actionAdd_To_Group.setObjectName("actionAdd_To_Group") self.addAction(self.actionAdd_To_Group) self.addSeparator() # item operations self.actionRemove_Station = QAction(self) self.actionRemove_Station.setEnabled(True) self.actionRemove_Station.setObjectName("actionRemove_Station") self.addAction(self.actionRemove_Station) self.addSeparator() # plot menu action self.actionPlot = QAction(self) self.actionPlot.setEnabled(False) self.actionPlot.setObjectName("actionPlot") self.addAction(self.actionPlot) self.retranslateUi() def retranslateUi(self): self.actionCreate_New_Group.setText( _translate("StationViewer", "Create New Group...", None)) self.actionDismiss_Group.setText( _translate("StationViewer", "Dismiss Selected Group", None)) self.actionAdd_To_Group.setText( _translate("StationViewer", "Add Selected to Group...", None)) self.actionRemove_Station.setText( _translate("StationViewer", "Unload Selected Stations...", None)) self.actionPlot.setText( _translate("StationViewer", "Plot...", None))
def create_action( parent, title, triggered=None, toggled=None, shortcut=None, icon=None, tip=None, checkable=None, context=Qt.WindowShortcut, enabled=None, ): """ Create a new QAction """ action = QAction(title, parent) if triggered: if checkable: action.triggered.connect(triggered) else: action.triggered.connect(lambda checked=False: triggered()) if checkable is not None: # Action may be checkable even if the toggled signal is not connected action.setCheckable(checkable) if toggled: action.toggled.connect(toggled) action.setCheckable(True) if icon is not None: assert isinstance(icon, QIcon) action.setIcon(icon) if shortcut is not None: action.setShortcut(shortcut) if tip is not None: action.setToolTip(tip) action.setStatusTip(tip) if enabled is not None: action.setEnabled(enabled) action.setShortcutContext(context) return action
def _popup_menu(self, event): axes = self._find_calling_axes(event) # find axes calling right click if axes is None: return pos = self.parent.mapFromGlobal(QtGui.QCursor().pos()) popup_menu = QMenu(self.parent) xScaleMenu = popup_menu.addMenu('x-scale') yScaleMenu = popup_menu.addMenu('y-scale') for coord in ['x', 'y']: menu = eval(coord + 'ScaleMenu') for type in axes.scale[coord].keys(): action = QAction(type, menu, checkable=True) if axes.scale[coord][type]: # if it's checked action.setEnabled(False) else: action.setEnabled(True) menu.addAction(action) action.setChecked(axes.scale[coord][type]) fcn = lambda event, coord=coord, type=type: self._set_scale( coord, type, axes, True) action.triggered.connect(fcn) # Create menu for AutoScale options X Y All popup_menu.addSeparator() autoscale_options = ['AutoScale X', 'AutoScale Y', 'AutoScale All'] for n, text in enumerate(autoscale_options): action = QAction(text, menu, checkable=True) if n < len(self.autoScale): action.setChecked(self.autoScale[n]) else: action.setChecked(all(self.autoScale)) popup_menu.addAction(action) action.toggled.connect( lambda event, n=n: self._setAutoScale(n, event, axes)) popup_menu.exec_(self.parent.mapToGlobal(pos))
class BasePSControlWidget(QWidget): """Base widget class to control power supply.""" HORIZONTAL = 0 VERTICAL = 1 def __init__(self, subsection=None, orientation=0, parent=None): """Class constructor. Parameters: psname_list - a list of power supplies, will be filtered based on patterns defined in the subclass; orientation - how the different groups(defined in subclasses) will be laid out. """ super(BasePSControlWidget, self).__init__(parent) self._orientation = orientation self._subsection = subsection self._dev_list = PSSearch.get_psnames(self._getFilter(subsection)) dev0 = PVName(self._dev_list[0]) if dev0.sec == 'LI': if dev0.dev == 'Slnd': idcs = [int(PVName(dev).idx) for dev in self._dev_list] self._dev_list = [ x for _, x in sorted(zip(idcs, self._dev_list)) ] if 'Q' in dev0.dev: all_props = dict() for dev in self._dev_list: all_props.update(get_prop2label(dev)) self.all_props = sort_propties(all_props) else: self.all_props = get_prop2label(self._dev_list[0]) else: self.all_props = get_prop2label(self._dev_list[0]) self.visible_props = self._getVisibleProps() if 'trim' in self.all_props: self.visible_props.append('trim') self.visible_props = sort_propties(self.visible_props) # Data used to filter the widgets self.ps_widgets_dict = dict() self.containers_dict = dict() self.filtered_widgets = set() # Set with key of visible widgets # Setup the UI self.groups = self._getGroups() self._setup_ui() self._create_actions() self._enable_actions() if len(self.groups) in [1, 3]: self.setObjectName('cw') self.setStyleSheet('#cw{min-height: 40em;}') def _setup_ui(self): self.layout = QVBoxLayout() # Create filters self.search_le = QLineEdit(parent=self) self.search_le.setObjectName("search_lineedit") self.search_le.setPlaceholderText("Search for a power supply...") self.search_le.textEdited.connect(self._filter_pwrsupplies) self.filter_pb = QPushButton(qta.icon('mdi.view-column'), '', self) self.search_menu = QMenu(self.filter_pb) self.filter_pb.setMenu(self.search_menu) for prop, label in self.all_props.items(): act = self.search_menu.addAction(label) act.setObjectName(prop) act.setCheckable(True) act.setChecked(prop in self.visible_props) act.toggled.connect(self._set_widgets_visibility) hlay_filter = QHBoxLayout() hlay_filter.addWidget(self.search_le) hlay_filter.addWidget(self.filter_pb) self.layout.addLayout(hlay_filter) self.count_label = QLabel(parent=self) self.count_label.setSizePolicy(QSzPlcy.Maximum, QSzPlcy.Maximum) self.layout.addWidget(self.count_label) self.pwrsupplies_layout = self._getSplitter() self.layout.addWidget(self.pwrsupplies_layout) if len(self.groups) == 3: splitt_v = QSplitter(Qt.Vertical) # Build power supply Layout # Create group boxes and pop. layout for idx, group in enumerate(self.groups): # Get power supplies that belong to group pwrsupplies = list() pattern = re.compile(group[1]) for el in self._dev_list: if pattern.search(el): pwrsupplies.append(el) # Create header header = SummaryHeader(pwrsupplies[0], visible_props=self.visible_props, parent=self) self.containers_dict['header ' + group[0]] = header self.filtered_widgets.add('header ' + group[0]) # Loop power supply to create all the widgets of a groupbox group_widgets = list() for psname in pwrsupplies: ps_widget = SummaryWidget(name=psname, visible_props=self.visible_props, parent=self) pscontainer = PSContainer(ps_widget, self) group_widgets.append(pscontainer) self.containers_dict[psname] = pscontainer self.filtered_widgets.add(psname) self.ps_widgets_dict[psname] = ps_widget # Create group wid_type = 'groupbox' if group[0] else 'widget' group_wid = self._createGroupWidget(group[0], header, group_widgets, wid_type=wid_type) # Add group box to grid layout if len(self.groups) == 3: if idx in [0, 1]: splitt_v.addWidget(group_wid) else: self.pwrsupplies_layout.addWidget(splitt_v) self.pwrsupplies_layout.addWidget(group_wid) else: self.pwrsupplies_layout.addWidget(group_wid) self.count_label.setText("Showing {} power supplies.".format( len(self.filtered_widgets) - len(self.groups))) self.setLayout(self.layout) def _createGroupWidget(self, title, header, widget_group, wid_type='groupbox'): scr_area_wid = QWidget(self) scr_area_wid.setObjectName('scr_ar_wid') scr_area_wid.setStyleSheet( '#scr_ar_wid {background-color: transparent;}') w_lay = QVBoxLayout(scr_area_wid) w_lay.setSpacing(0) w_lay.setContentsMargins(0, 0, 0, 0) for widget in widget_group: w_lay.addWidget(widget, alignment=Qt.AlignLeft) w_lay.addStretch() scr_area = QScrollArea(self) scr_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scr_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) scr_area.setWidgetResizable(True) scr_area.setFrameShape(QFrame.NoFrame) scr_area.setWidget(scr_area_wid) wid = QGroupBox(title, self) if wid_type == 'groupbox' \ else QWidget(self) gb_lay = QVBoxLayout(wid) gb_lay.addWidget(header, alignment=Qt.AlignLeft) gb_lay.addWidget(scr_area) return wid def _getSplitter(self): if self._orientation == self.HORIZONTAL: return QSplitter(Qt.Horizontal) else: return QSplitter(Qt.Vertical) def _getVisibleProps(self): """Default visible properties.""" return [ 'detail', 'state', 'intlk', 'setpoint', 'monitor', 'strength_sp', 'strength_mon' ] def _filter_pwrsupplies(self, text): """Filter power supply widgets based on text inserted at line edit.""" try: pattern = re.compile(text, re.I) except Exception: # Ignore malformed patterns? pattern = re.compile("malformed") # Clear filtered widgets and add the ones that match the new pattern self.filtered_widgets.clear() for name, container in self.containers_dict.items(): cond = 'header' in name if not cond: cond |= bool(pattern.search(name)) cond |= bool(pattern.search(container.bbbname)) cond |= bool(pattern.search(container.udcname)) for dc in container.dclinks: cond |= bool(pattern.search(dc)) for dc in container.dclinksbbbname: cond |= bool(pattern.search(dc)) for dc in container.dclinksudcname: cond |= bool(pattern.search(dc)) if cond: self.filtered_widgets.add(name) # Set widgets visibility and the number of widgets matched self._set_widgets_visibility() self.count_label.setText("Showing {} power supplies".format( len(self.filtered_widgets) - len(self.groups))) # Scroll to top for scroll_area in self.findChildren(QScrollArea): scroll_area.verticalScrollBar().setValue(0) def _set_widgets_visibility(self): """Set visibility of the widgets.""" props = [ act.objectName() for act in self.search_menu.actions() if act.isChecked() ] self.visible_props = sort_propties(props) self._enable_actions() for key, wid in self.containers_dict.items(): wid.update_visible_props(props) if 'header' in key: for ob in wid.findChildren(QWidget): name = ob.objectName() ob.setVisible(name in props or 'Hidden' in name) else: vis = key in self.filtered_widgets wid.setVisible(vis) if not vis: continue objs = wid.findChildren(SummaryWidget) objs.extend(wid.findChildren(SummaryHeader)) for ob in objs: chil = ob.findChildren(QWidget, options=Qt.FindDirectChildrenOnly) for c in chil: name = c.objectName() if isinstance(ob, SummaryWidget) and name in props: ob.fillWidget(name) c.setVisible(name in props) # Actions methods def _create_actions(self): self.turn_on_act = QAction("Turn On", self) self.turn_on_act.triggered.connect(lambda: self._set_pwrstate(True)) self.turn_on_act.setEnabled(False) self.turn_off_act = QAction("Turn Off", self) self.turn_off_act.triggered.connect(lambda: self._set_pwrstate(False)) self.turn_off_act.setEnabled(False) self.ctrlloop_close_act = QAction("Close Control Loop", self) self.ctrlloop_close_act.triggered.connect( lambda: self._set_ctrlloop(True)) self.ctrlloop_close_act.setEnabled(False) self.ctrlloop_open_act = QAction("Open Control Loop", self) self.ctrlloop_open_act.triggered.connect( lambda: self._set_ctrlloop(False)) self.ctrlloop_open_act.setEnabled(False) self.set_slowref_act = QAction("Set OpMode to SlowRef", self) self.set_slowref_act.triggered.connect(self._set_slowref) self.set_slowref_act.setEnabled(False) self.set_current_sp_act = QAction("Set Current SP", self) self.set_current_sp_act.triggered.connect(self._set_current_sp) self.set_current_sp_act.setEnabled(False) self.reset_act = QAction("Reset Interlocks", self) self.reset_act.triggered.connect(self._reset_interlocks) self.reset_act.setEnabled(False) self.wfmupdate_on_act = QAction("Wfm Update Auto Enable", self) self.wfmupdate_on_act.triggered.connect( lambda: self._set_wfmupdate(True)) self.wfmupdate_on_act.setEnabled(False) self.wfmupdate_off_act = QAction("Wfm Update Auto Disable", self) self.wfmupdate_off_act.triggered.connect( lambda: self._set_wfmupdate(False)) self.wfmupdate_off_act.setEnabled(False) self.updparms_act = QAction("Update Parameters", self) self.updparms_act.triggered.connect(self._update_params) self.updparms_act.setEnabled(False) def _enable_actions(self): if 'state' in self.visible_props and \ not self.turn_on_act.isEnabled(): self.turn_on_act.setEnabled(True) self.turn_off_act.setEnabled(True) if 'ctrlloop' in self.visible_props and \ not self.ctrlloop_close_act.isEnabled(): self.ctrlloop_close_act.setEnabled(True) self.ctrlloop_open_act.setEnabled(True) if 'opmode' in self.visible_props and \ not self.set_slowref_act.isEnabled(): self.set_slowref_act.setEnabled(True) if 'setpoint' in self.visible_props and \ not self.set_current_sp_act.isEnabled(): self.set_current_sp_act.setEnabled(True) if 'reset' in self.visible_props and \ not self.reset_act.isEnabled(): self.reset_act.setEnabled(True) if 'wfmupdate' in self.visible_props and \ not self.wfmupdate_on_act.isEnabled(): self.wfmupdate_on_act.setEnabled(True) self.wfmupdate_off_act.setEnabled(True) if 'updparms' in self.visible_props and \ not self.updparms_act.isEnabled(): self.updparms_act.setEnabled(True) @Slot(bool) def _set_pwrstate(self, state): """Execute turn on/off actions.""" for key, widget in self.ps_widgets_dict.items(): if key in self.filtered_widgets: try: if state: widget.turn_on() else: widget.turn_off() except TypeError: pass @Slot(bool) def _set_ctrlloop(self, state): """Execute close/open control loop actions.""" for key, widget in self.ps_widgets_dict.items(): if key in self.filtered_widgets: try: if state: widget.ctrlloop_close() else: widget.ctrlloop_open() except TypeError: pass @Slot() def _set_slowref(self): """Set opmode to SlowRef for every visible widget.""" for key, widget in self.ps_widgets_dict.items(): if key in self.filtered_widgets: try: widget.set_opmode_slowref() except TypeError: pass @Slot() def _set_current_sp(self): """Set current setpoint for every visible widget.""" dlg = QInputDialog(self) dlg.setLocale(QLocale(QLocale.English)) new_value, ok = dlg.getDouble(self, "Insert current setpoint", "Value") if ok: for key, widget in self.ps_widgets_dict.items(): if key in self.filtered_widgets: sp = widget.setpoint.sp_lineedit sp.setText(str(new_value)) try: sp.send_value() except TypeError: pass @Slot() def _reset_interlocks(self): """Reset interlocks.""" for key, widget in self.ps_widgets_dict.items(): if key in self.filtered_widgets: try: widget.reset() except TypeError: pass @Slot(bool) def _set_wfmupdate(self, state): """Execute turn WfmUpdateAuto on/off actions.""" for key, widget in self.ps_widgets_dict.items(): if key in self.filtered_widgets: try: if state: widget.wfmupdate_on() else: widget.wfmupdate_off() except TypeError: pass @Slot() def _update_params(self): """Update parameters.""" for key, widget in self.ps_widgets_dict.items(): if key in self.filtered_widgets: try: widget.update_params() except TypeError: pass # Overloaded method def contextMenuEvent(self, event): """Show a custom context menu.""" point = event.pos() menu = QMenu("Actions", self) menu.addAction(self.turn_on_act) menu.addAction(self.turn_off_act) menu.addAction(self.ctrlloop_close_act) menu.addAction(self.ctrlloop_open_act) menu.addAction(self.set_current_sp_act) if not self._dev_list[0].dev in ('FCH', 'FCV'): menu.addAction(self.set_slowref_act) menu.addAction(self.reset_act) menu.addAction(self.wfmupdate_on_act) menu.addAction(self.wfmupdate_off_act) menu.addAction(self.updparms_act) menu.addSeparator() action = menu.addAction('Show Connections...') action.triggered.connect(self.show_connections) menu.popup(self.mapToGlobal(point)) def show_connections(self, checked): """.""" _ = checked conn = ConnectionInspector(self) conn.show() def get_summary_widgets(self): """Return Summary Widgets.""" return self.findChildren(SummaryWidget)
class PSContainer(QWidget): """PSContainer.""" def __init__(self, widget, parent=None): super().__init__(parent) self._widget = widget self.name = widget.devname self.bbbname = widget.bbbname self.udcname = widget.udcname self.dclinks = list() self.dclinks_type = '' self.dclink_widgets = list() self.dclinksbbbname = set() self.dclinksudcname = set() dclinks = PSSearch.conv_psname_2_dclink(self.name) if dclinks: self.dclinks = dclinks self.dclinks_type = PSSearch.conv_psname_2_psmodel(dclinks[0]) if self.dclinks_type != 'REGATRON_DCLink': for dc in dclinks: self.dclinksbbbname.add(PSSearch.conv_psname_2_bbbname(dc)) self.dclinksudcname.add(PSSearch.conv_psname_2_udc(dc)) self.all_props = get_prop2label(PVName(dclinks[0])) self.visible_props = sort_propties( ['detail', 'state', 'intlk', 'setpoint', 'monitor']) self._setup_ui() self._create_actions() self._enable_actions() self.setStyleSheet(""" #HideButton { min-width: 10px; max-width: 10px; } #DCLinkContainer { background-color: lightgrey; } """) def _setup_ui(self): """Setup widget UI.""" self._layout = QGridLayout() self._layout.setSpacing(10) self._layout.setContentsMargins(0, 0, 0, 0) self.setLayout(self._layout) self._dclink_container = QWidget(self) self._dclink_container.setObjectName('DCLinkContainer') self._dclink_container.setLayout(QVBoxLayout()) self._dclink_is_filled = False if self.dclinks: self._hide = QPushButton(qta.icon('mdi.plus'), '', self) else: self._hide = QPushButton('', self) self._hide.setEnabled(False) self._hide.setObjectName('HideButton') self._hide.setSizePolicy(QSzPlcy.Maximum, QSzPlcy.Maximum) self._hide.setFlat(True) self._layout.addWidget(self._hide, 0, 0, Qt.AlignCenter) self._layout.addWidget(self._widget, 0, 1) self._layout.addWidget(self._dclink_container, 1, 1) # Configure self._dclink_container.setHidden(True) self._hide.clicked.connect(self._toggle_dclink) def _toggle_dclink(self): if self._dclink_container.isHidden(): if not self._dclink_is_filled: self._fill_dclink_container() self._enable_actions() self._hide.setIcon(qta.icon('mdi.minus')) self._dclink_container.setHidden(False) else: self._hide.setIcon(qta.icon('mdi.plus')) self._dclink_container.setHidden(True) def _fill_dclink_container(self): self._dclink_is_filled = True self._dclink_container.layout().addWidget( SummaryHeader(self.dclinks[0], self.visible_props, self)) for dclink_name in self.dclinks: w = SummaryWidget(dclink_name, self.visible_props, self) if self.dclinks_type == 'REGATRON_DCLink': connect_newprocess(w.detail_bt, [ 'sirius-hla-as-ps-regatron-individual', '-dev', dclink_name ], parent=self, is_pydm=True) else: connect_window(w.detail_bt, PSDetailWindow, self, psname=dclink_name) self._dclink_container.layout().addWidget(w) self.dclink_widgets.append(w) def update_visible_props(self, new_value): self.visible_props = sort_propties(new_value) self._enable_actions() # Action methods def _create_actions(self): self._turn_on_action = QAction('Turn DCLinks On', self) self._turn_on_action.triggered.connect( lambda: self._set_dclink_pwrstate(True)) self._turn_on_action.setEnabled(False) self._turn_off_action = QAction('Turn DCLinks Off', self) self._turn_off_action.triggered.connect( lambda: self._set_dclink_pwrstate(False)) self._turn_off_action.setEnabled(False) self._open_loop_action = QAction('Open DCLinks Control Loop', self) self._open_loop_action.triggered.connect( lambda: self._set_dclink_control_loop(False)) self._open_loop_action.setEnabled(False) self._close_loop_action = QAction('Close DCLinks Control Loop', self) self._close_loop_action.triggered.connect( lambda: self._set_dclink_control_loop(True)) self._close_loop_action.setEnabled(False) self._set_setpoint_action = QAction('Set DCLinks Voltage', self) self._set_setpoint_action.triggered.connect(self._set_setpoint) self._set_setpoint_action.setEnabled(False) self._reset_intlk_action = QAction('Reset DCLinks Interlocks', self) self._reset_intlk_action.triggered.connect(self._reset_intlk) self._reset_intlk_action.setEnabled(False) def _enable_actions(self): if 'state' in self.visible_props and \ not self._turn_on_action.isEnabled(): self._turn_on_action.setEnabled(True) self._turn_off_action.setEnabled(True) if 'ctrlloop' in self.visible_props and \ not self._open_loop_action.isEnabled(): self._open_loop_action.setEnabled(True) self._close_loop_action.setEnabled(True) if 'setpoint' in self.visible_props and \ not self._set_setpoint_action.isEnabled(): self._set_setpoint_action.setEnabled(True) if 'reset' in self.visible_props and \ not self._reset_intlk_action.isEnabled(): self._reset_intlk_action.setEnabled(True) def _set_dclink_pwrstate(self, value): for dclink in self.dclink_widgets: if value: dclink.turn_on() else: dclink.turn_off() def _set_dclink_control_loop(self, value): for dclink in self.dclink_widgets: btn = dclink.ctrlloop_bt if value: if btn._bit_val: btn.send_value() else: if not btn._bit_val: btn.send_value() def _set_setpoint(self): """Set current setpoint for every visible widget.""" dlg = QInputDialog(self) dlg.setLocale(QLocale(QLocale.English)) new_value, ok = dlg.getDouble(self, "New setpoint", "Value") if ok: for dclink in self.dclink_widgets: sp = dclink.setpoint.sp_lineedit sp.setText(str(new_value)) try: sp.send_value() except TypeError: pass def _reset_intlk(self): for dclink in self.dclink_widgets: dclink.reset() # Overloaded method def contextMenuEvent(self, event): """Overload to create a custom context menu.""" widget = self.childAt(event.pos()) parent = widget.parent() grand_parent = parent.parent() if widget.objectName() == 'DCLinkContainer' or \ parent.objectName() == 'DCLinkContainer' or \ grand_parent.objectName() == 'DCLinkContainer': menu = QMenu(self) menu.addAction(self._turn_on_action) menu.addAction(self._turn_off_action) menu.addSeparator() menu.addAction(self._close_loop_action) menu.addAction(self._open_loop_action) menu.addSeparator() menu.addAction(self._set_setpoint_action) menu.addSeparator() menu.addAction(self._reset_intlk_action) menu.addSeparator() action = menu.addAction('Show Connections...') action.triggered.connect(self.show_connections) menu.popup(event.globalPos()) else: super().contextMenuEvent(event) def show_connections(self, checked): """.""" _ = checked c = ConnectionInspector(self) c.show()
class Viewer(ConfigurableMoveableTabContainer): """ Contains multiple TabbedViewingAreas """ tab_titles = Signal([tuple]) num_viewing_areas = Integer(2, config=True) def __init__(self, *args, menuBar, **kwargs): super().__init__(*args, **kwargs) self._run_to_tabs = collections.defaultdict(list) self._title_to_tab = {} self._tabs_from_streaming = [] self._overplot = OverPlotState.individual_tab self._overplot_target = None self._live_enabled = False self._live_run_router = RunRouter([self.route_live_stream]) self._containers = [ TabbedViewingArea(viewer=self, menuBar=menuBar) for _ in range(self.num_viewing_areas) ] layout = QVBoxLayout() splitter = QSplitter(Qt.Vertical) layout.addWidget(splitter) for container in self._containers: splitter.addWidget(container) self.setLayout(layout) overplot_group = QActionGroup(self) self.off = QAction('&Off', self) self.off.setStatusTip('Drop streaming data.') self.individual_tab = QAction('&New Tab', self) self.individual_tab.setStatusTip('Open a new viewer tab for each Run.') self.latest_live = QAction('&Latest Live Tab', self) self.latest_live.setStatusTip( 'Attempt to overplot on the most recent live Run.') self.fixed = QAction('&Fixed Tab...', self) self.fixed.setStatusTip('Attempt to overplot on a specific tab.') self.fixed.setEnabled(False) overplot_group.addAction(self.off) overplot_group.addAction(self.individual_tab) overplot_group.addAction(self.latest_live) overplot_group.addAction(self.fixed) for action in overplot_group.actions(): action.setCheckable(True) overplot_group.setExclusive(True) self.off.setChecked(True) overplot_menu = menuBar().addMenu('&Streaming') overplot_menu.addActions(overplot_group.actions()) self.off.triggered.connect(self.disable_live) self.individual_tab.triggered.connect( partial(self.set_overplot_state, OverPlotState.individual_tab)) self.latest_live.triggered.connect( partial(self.set_overplot_state, OverPlotState.latest_live)) def set_overplot_target(): item, ok = QInputDialog.getItem(self, "Select Tab", "Tab", tuple(self._title_to_tab), 0, False) if not ok: # Abort and fallback to Off. Would be better to fall back to # previous state (which could be latest_live) but it's not # clear how to know what that state was. self.off.setChecked(True) return self.set_overplot_state(OverPlotState.fixed) self._overplot_target = item self.fixed.triggered.connect(set_overplot_target) def enable_live(self): self._live_enabled = True def disable_live(self): self._live_enabled = False def consumer(self, item): """Slot that receives (name, doc) and unpacks it into RunRouter.""" self._live_run_router(*item) def route_live_stream(self, name, start_doc): """Create or choose a Viewer to receive this Run.""" if not self._live_enabled: log.debug("Streaming Run ignored because Streaming is disabled.") return [], [] self.fixed.setEnabled(True) target_area = self._containers[0] uid = start_doc['uid'] if self._overplot == OverPlotState.individual_tab: viewer = RunViewer() tab_title = uid[:8] index = target_area.addTab(viewer, tab_title) self._title_to_tab[tab_title] = viewer self._tabs_from_streaming.append(viewer) target_area.setCurrentIndex(index) self.tab_titles.emit(tuple(self._title_to_tab)) elif self._overplot == OverPlotState.fixed: viewer = self._title_to_tab[self._overplot_target] elif self._overplot == OverPlotState.latest_live: if self._tabs_from_streaming: viewer = self._tabs_from_streaming[-1] else: viewer = RunViewer() tab_title = uid[:8] index = target_area.addTab(viewer, tab_title) self._title_to_tab[tab_title] = viewer self._tabs_from_streaming.append(viewer) target_area.setCurrentIndex(index) self.tab_titles.emit(tuple(self._title_to_tab)) self._run_to_tabs[uid].append(viewer) viewer.run_router('start', start_doc) return [viewer.run_router], [] def show_entries(self, target, entries): self.fixed.setEnabled(True) target_area = self._containers[0] if not target: # Add new Viewer tab. viewer = RunViewer() if len(entries) == 1: entry, = entries uid = entry.describe()['metadata']['start']['uid'] tab_title = uid[:8] else: tab_title = self.get_title() index = target_area.addTab(viewer, tab_title) self._title_to_tab[tab_title] = viewer target_area.setCurrentIndex(index) self.tab_titles.emit(tuple(self._title_to_tab)) else: viewer = self._title_to_tab[target] for entry in entries: viewer.load_entry(entry) uid = entry.describe()['metadata']['start']['uid'] self._run_to_tabs[uid].append(viewer) # TODO Make last entry in the list the current widget. def get_title(self): for i in itertools.count(1): title = f'Group {i}' if title in self._title_to_tab: continue return title def set_overplot_state(self, state): self.enable_live() log.debug('Overplot state is %s', state) self._overplot = state def close_run_viewer(self, widget): try: self._tabs_from_streaming.remove(widget) except ValueError: pass for uid in widget.uids: self._run_to_tabs[uid].remove(widget) for title, tab in list(self._title_to_tab.items()): if tab == widget: del self._title_to_tab[title] self.tab_titles.emit(tuple(self._title_to_tab)) if title == self._overplot_target: self.set_overplot_state(OverPlotState.off) if not self._title_to_tab: self.fixed.setEnabled(False)
class ObjectExplorer(QTreeView): nodevalue_changed = pyqtSignal(AbstractJsonItem) nodeproperty_changed = pyqtSignal(AbstractJsonItem) def __init__(self, rootnode: JsonNode, parent): super().__init__(parent) self.mainwindow = parent self.setModel(JsonDataModel(rootnode, self.mainwindow)) self.model().dataChanged.connect(self.data_changed) self.setItemDelegate(MyItemDelegate()) self.setDragDropMode(QTreeView.DragDrop) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) self.doubleClicked.connect(self.double_clicked) # context menu self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_contextmenu) # actions # properties action self.propertiesAction = QAction(self.tr("properties"), self) self.propertiesAction.setIcon(QIcon(pixmap("document_properties.png"))) self.propertiesAction.triggered.connect(self.show_properties) # edit action self.editAction = QAction(self.tr("edit value"), self) self.editAction.setShortcut("F2") self.editAction.setIcon(QIcon(pixmap("edit.png"))) self.editAction.triggered.connect(self.edit_value) # edit key action self.editkeyAction = QAction(self.tr("edit key"), self) self.editkeyAction.setIcon(QIcon(pixmap("kgpg_key1_kgpg.png"))) self.editkeyAction.triggered.connect(self.edit_key) # insert item action self.insertitemAction = QAction(self.tr("insert item"), self) self.insertitemAction.setIcon(QIcon(pixmap("list_add.png"))) self.insertitemAction.triggered.connect(self.insert_item) # insert node action self.insertnodeAction = QAction(self.tr("insert node"), self) self.insertnodeAction.setIcon(QIcon(pixmap("list_add.png"))) self.insertnodeAction.triggered.connect(self.insert_node) # remove item action self.removeitemAction = QAction(self.tr("remove"), self) self.removeitemAction.setIcon(QIcon(pixmap("list_remove"))) self.removeitemAction.triggered.connect(self.remove_item) def data_changed(self, topleft, bottomright, *args): node = topleft.internalPointer() if node is not None and isinstance(node, JsonItem): self.nodevalue_changed.emit(node) def double_clicked(self, *args, **kwargs): index = self.currentIndex() if not index.isValid(): return column = self.model().columns[index.column()] if column.name == "value": self.edit_value() else: self.show_properties() def edit_key(self): index = self.currentIndex() if index.isValid(): node = index.internalPointer() key, b = QInputDialog.getText( self, "Edit Json item", "Insert new key for item:", text=node.key ) if not b: return node.key = key try: # PyQt5 self.model().dataChanged.emit(index, index, [Qt.DisplayRole]) except TypeError: # PyQt4, PySide self.model().dataChanged.emit(index, index) def edit_value(self): index = self.currentIndex() if not index.isValid(): return i = self.model().index(index.row(), 2, index.parent()) self.edit(i) def insert_item(self): index = self.currentIndex() if index.isValid(): node = index.internalPointer() else: node = self.model().root key, b = QInputDialog.getText( self, "Insert Json item", "Insert key for new item:") if not b: return item = JsonItem(key) node.add(item) row = node.index(item) self.model().beginInsertRows(index, row, row) self.model().endInsertRows() def insert_node(self): index = self.currentIndex() parentnode = index.internalPointer() or self.model().root key, b = QInputDialog.getText( self, "Insert Json node", "Insert key for new node:") if not b: return node = JsonNode(key) parentnode.add(node) row = parentnode.index(node) self.model().beginInsertRows(index, row, row) self.model().endInsertRows() def mousePressEvent(self, event): index = self.indexAt(event.pos()) if not index.isValid(): self.setCurrentIndex(QModelIndex()) super().mousePressEvent(event) def refresh(self): self.model().refresh() def remove_item(self): index = self.currentIndex() self.model().beginRemoveRows(index.parent(), index.row(), index.row()) if index.isValid(): node = index.internalPointer() if node.parent is not None: node.parent.remove(node.key) self.model().refresh() self.model().endRemoveRows() def show_contextmenu(self, pos: QPoint): menu = QMenu(self) index = self.currentIndex() node = index.internalPointer() # insert item and node menu.addAction(self.insertitemAction) menu.addAction(self.insertnodeAction) # edit key if isinstance(node, (JsonNode, JsonItem)): menu.addSeparator() menu.addAction(self.editkeyAction) if isinstance(node, JsonItem): menu.addAction(self.editAction) self.editAction.setEnabled(not node.readonly) # remove if isinstance(node, (JsonNode, JsonItem)): menu.addSeparator() menu.addAction(self.removeitemAction) # properties if isinstance(node, JsonItem): menu.addSeparator() menu.addAction(self.propertiesAction) menu.setDefaultAction(self.propertiesAction) menu.popup(self.viewport().mapToGlobal(pos), self.editAction) def show_properties(self): index = self.currentIndex() node = index.internalPointer() if not (index.isValid() and isinstance(node, JsonItem)): return dlg = ItemPropertyDialog(node, self.parent()) if dlg.exec_() == QDialog.Accepted: self.nodeproperty_changed.emit(node)
class TyphosSidebarItem(ptypes.ParameterItem): """ Class to display a Device or Tool in the sidebar """ def __init__(self, param, depth): super().__init__(param, depth) # Configure a QToolbar self.toolbar = QToolBar() self.toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly) self.toolbar.setIconSize(QSize(15, 15)) # Setup the action to open the widget self.open_action = QAction(qta.icon('fa.square', color='green'), 'Open', self.toolbar) self.open_action.triggered.connect(self.open_requested) # Setup the action to embed the widget self.embed_action = QAction(qta.icon('fa.th-large', color='yellow'), 'Embed', self.toolbar) self.embed_action.triggered.connect(self.embed_requested) # Setup the action to hide the widget self.hide_action = QAction(qta.icon('fa.times-circle', color='red'), 'Close', self.toolbar) self.hide_action.triggered.connect(self.hide_requested) self.hide_action.setEnabled(False) # Add actions to toolbars self.toolbar.addAction(self.open_action) self.toolbar.addAction(self.hide_action) if self.param.embeddable: self.toolbar.insertAction(self.hide_action, self.embed_action) def open_requested(self, triggered): """Request to open display for sidebar item""" self.param.sigOpen.emit(self) self._mark_shown() def embed_requested(self, triggered): """Request to open embedded display for sidebar item""" self.param.sigEmbed.emit(self) self._mark_shown() def hide_requested(self, triggered): """Request to hide display for sidebar item""" self.param.sigHide.emit(self) self._mark_hidden() def _mark_shown(self): self.open_action.setEnabled(False) self.embed_action.setEnabled(False) self.hide_action.setEnabled(True) def _mark_hidden(self): self.open_action.setEnabled(True) self.embed_action.setEnabled(True) self.hide_action.setEnabled(False) def treeWidgetChanged(self): """Update the widget when add to a QTreeWidget""" super().treeWidgetChanged() tree = self.treeWidget() if tree is None: return tree.setItemWidget(self, 1, self.toolbar)
class PyDMAlarmTree(QTreeView, PyDMWritableWidget): def __init__(self, parent, init_channel=None, config_name=None, edit_mode=False): super(PyDMAlarmTree, self).__init__() QTreeView.__init__(self, parent) PyDMWritableWidget.__init__(self) self.setup_ui() self._nodes = [] self.config_name = config_name self.tree_model = AlarmTreeModel(self) self.setModel(self.tree_model) self.edit_mode = edit_mode self.setContextMenuPolicy(Qt.CustomContextMenu) if not edit_mode: self.customContextMenuRequested.connect(self._open_menu) self.expandAll() def setup_ui(self): self.setEditTriggers(QAbstractItemView.NoEditTriggers) self.setDragDropOverwriteMode(False) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) # self.setHeaderHidden(True) self.setColumnWidth(0, 160) self.setColumnWidth(1, 160) self.setColumnWidth(2, 160) def get_configuration_name(self): return self.config_name def set_configuration_name(self, config_name): self.config_name = config_name configuration_name = Property(str, get_configuration_name, set_configuration_name, designable=False) def _open_menu(self, point): menu = QMenu() index = self.indexAt(point) item = self.model().getItem(index) self.value_action = QAction(item.status, self) self.value_action.setEnabled(False) menu.addAction(self.value_action) self.acknowledge_action = QAction("Acknowledge", self) self.acknowledge_action.triggered.connect( partial(self._acknowledge_at_index, index)) menu.addAction(self.acknowledge_action) self.remove_acknowledge_action = QAction("Remove Acknowledge", self) self.remove_acknowledge_action.triggered.connect( partial(self._remove_acknowledge_at_index, index)) self.remove_acknowledge_action.setEnabled(False) menu.addAction(self.remove_acknowledge_action) menu.exec_(self.viewport().mapToGlobal(point)) def _acknowledge_at_index(self, index): item = self.tree_model.getItem(index) item.acknowledge() def _remove_acknowledge_at_index(self, index): item = self.tree_model.getItem(index) item.unacknowledge() def mousePressEvent(self, event): self.clearSelection() self.selectionModel().reset() QTreeView.mousePressEvent(self, event)
class StrainStressViewer(QMainWindow): def __init__(self, model, ctrl, parent=None): self._model = model self._model.propertyUpdated.connect(self.updatePropertyFromModel) self._model.failureMsg.connect(self.show_failure_msg) self._ctrl = ctrl super().__init__(parent) self.setWindowTitle("PyRS Strain-Stress Viewer") mainMenu = self.menuBar() fileMenu = mainMenu.addMenu('File') self.saveAction = QAction('&Save state', self) self.saveAction.setShortcut('Ctrl+S') self.saveAction.setStatusTip('Save application state') self.saveAction.triggered.connect(self.save) self.saveAction.setEnabled(False) fileMenu.addAction(self.saveAction) self.loadAction = QAction('&Load state', self) self.loadAction.setStatusTip('Load application state') self.loadAction.triggered.connect(self.load) fileMenu.addAction(self.loadAction) fileMenu.addSeparator() exitAction = QAction('&Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') exitAction.triggered.connect(self.close) fileMenu.addAction(exitAction) self.splitter = QSplitter() self.splitter.setHandleWidth(10) self.setCentralWidget(self.splitter) left = QWidget() left_layout = QVBoxLayout() self.stressCase = StressCase(self) self.stressCase.dimChanged.connect(self.dimChanged) left_layout.addWidget(self.stressCase) self.fileLoading = FileLoading(self) left_layout.addWidget(self.fileLoading) self.peak_selection = PeakSelection(self) self.peak_selection.peak_select.currentTextChanged.connect( self.controller.peakSelected) left_layout.addWidget(self.peak_selection) self.d0 = D0(self) left_layout.addWidget(self.d0) self.mechanicalConstants = MechanicalConstants(self) self.mechanicalConstants.youngModulus.editingFinished.connect( self.update_plot) self.mechanicalConstants.poissonsRatio.editingFinished.connect( self.update_plot) left_layout.addWidget(self.mechanicalConstants) self.csvExport = CSVExport(self) left_layout.addWidget(self.csvExport) left_layout.addStretch(0) left.setLayout(left_layout) self.splitter.addWidget(left) right = QWidget() right_layout = QVBoxLayout() self.plot_select = PlotSelect(self) self.plot_select.measure_dir.currentTextChanged.connect( self.update_plot) self.plot_select.plot_param.currentTextChanged.connect( self.update_plot) right_layout.addWidget(self.plot_select) self.viz_tab = VizTabs(self) right_layout.addWidget(self.viz_tab) right.setLayout(right_layout) self.splitter.addWidget(right) self.splitter.setStretchFactor(0, 1) self.splitter.setStretchFactor(1, 5) @property def controller(self): return self._ctrl @property def model(self): return self._model def dimChanged(self, bool2d): self.fileLoading.file_load_e33.setDisabled(bool2d) self.update_plot() def measure_dir_changed(self): self.update_plot() def update_plot(self): if self.plot_select.get_plot_param() == 'stress' or ( self.plot_select.get_plot_param() == 'strain' and self.plot_select.get_direction() == "33" and self.stressCase.get_stress_case() == "In-plane stress"): validated = self.controller.validate_stress_selection( self.stressCase.get_stress_case(), self.mechanicalConstants.youngModulus.text(), self.mechanicalConstants.poissonsRatio.text()) else: validated = self.controller.validate_selection( self.plot_select.get_direction(), self.stressCase.get_stress_case() != 'diagonal') if validated is None: if self.plot_select.get_plot_param() == 'stress' or ( self.plot_select.get_plot_param() == 'strain' and self.plot_select.get_direction() == "33" and self.stressCase.get_stress_case() == "In-plane stress"): self.calculate_stress() self.viz_tab.set_ws( self.model.get_field( direction=self.plot_select.get_direction(), plot_param=self.plot_select.get_plot_param(), stress_case=self.stressCase.get_stress_case())) else: self.viz_tab.set_ws(None) self.viz_tab.set_message(validated) if self.controller.validate_stress_selection( self.stressCase.get_stress_case(), self.mechanicalConstants.youngModulus.text(), self.mechanicalConstants.poissonsRatio.text()) is None: self.calculate_stress() self.enable_stress_output(True) else: self.enable_stress_output(False) def enable_stress_output(self, enable): self.csvExport.setEnabled(enable) self.d0.setEnabled(enable) self.saveAction.setEnabled(enable) def updatePropertyFromModel(self, name): getattr(self, name)(getattr(self.model, name)) def peakTags(self, peak_tags): self.peak_selection.peak_select.currentTextChanged.disconnect() self.peak_selection.clear_peak_tags() self.peak_selection.peak_select.currentTextChanged.connect( self.controller.peakSelected) self.peak_selection.set_peak_tags(peak_tags) def selectedPeak(self, peak): self.update_plot() def modelUpdated(self, modelUpdated): stressCase, selectedPeak, youngs_modulus, poisson_ratio = modelUpdated self.mechanicalConstants.set_values(youngs_modulus, poisson_ratio) self.peak_selection.peak_select.currentTextChanged.disconnect() self.peak_selection.set_selected_peak(selectedPeak) self.peak_selection.peak_select.currentTextChanged.connect( self.controller.peakSelected) self.stressCase.dimChanged.disconnect() self.stressCase.set_stress_case(stressCase) self.stressCase.dimChanged.connect(self.dimChanged) self.fileLoading.file_load_e33.setDisabled( stressCase.lower() != "diagonal") for d in ("11", "22", "33"): self.fileLoading.set_text_values( d, ", ".join( os.path.basename(filename) for filename in self.model.get_filenames_for_direction(d))) self.update_d0_from_model() self.update_plot() def show_failure_msg(self, msg, info, details): self.viz_tab.set_message(msg) msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Critical) msgBox.setText(msg) msgBox.setInformativeText(info) msgBox.setDetailedText(details) msgBox.exec() def calculate_stress(self): self.controller.calculate_stress( self.stressCase.get_stress_case(), self.mechanicalConstants.youngModulus.text(), self.mechanicalConstants.poissonsRatio.text(), self.d0.get_d0()) self.update_d0_from_model() def update_d0_from_model(self): d0 = self.model.d0 if d0 is None: self.d0.set_d0(None, None) self.d0.set_d0_field(None, None, None, None, None) else: self.d0.set_d0(d0.values[0], d0.errors[0]) self.d0.set_d0_field(d0.x, d0.y, d0.z, d0.values, d0.errors) def save(self): filename, _ = QFileDialog.getSaveFileName( self, "Save Stress state", "", "JSON (*.json);;All Files (*)") if filename: self.controller.save(filename) def load(self): filename, _ = QFileDialog.getOpenFileName( self, "Load Stress state", "", "JSON (*.json);;All Files (*)") if filename: self.controller.load(filename)
class PayloadWindow(FramelessWindow): get_build_options = Signal() start_build = Signal(str, str, list) stop_build = Signal() def __init__(self, parent): super(PayloadWindow, self).__init__(parent) self.logger = logging.getLogger(self.__class__.__name__) self.setContentsMargins(11, 11, 11, 11) self.progress_windows = [] self.content_widget = QWidget(self) self.addContentWidget(self.content_widget) self.widget_layout = QFormLayout(self.content_widget) self.content_widget.setLayout(self.widget_layout) self.spinner = WaitingSpinner(self, modality=Qt.WindowModal, roundness=70.0, fade=70.0, radius=15.0, lines=6, line_length=25.0, line_width=4.0, speed=1.0) self.build_name_label = QLabel("Name", self.content_widget) self.build_name_edit = QLineEdit(self.content_widget) self.widget_layout.addRow(self.build_name_label, self.build_name_edit) self.icon_label = QLabel("Icon", self.content_widget) self.icon_combobox = QComboBox(self.content_widget) self.widget_layout.addRow(self.icon_label, self.icon_combobox) self.generators_label = QLabel("Generators", self.content_widget) self.generators_list = QListWidget(self.content_widget) self.widget_layout.addRow(self.generators_label, self.generators_list) self.build_button = QPushButton("Build", self.content_widget) self.build_button.clicked.connect(self.on_build_button_clicked) self.widget_layout.addWidget(self.build_button) self.menu = QMenu(self) self.menu.setTitle("Payload") self.reload_action = QAction(self.menu) self.reload_action.setText("Reload options") self.reload_action.triggered.connect(self.setupUi) self.menu.addAction(self.reload_action) self.addMenu(self.menu) @Slot() def setupUi(self): self.spinner.start() self.get_build_options.emit() @Slot(dict) def process_build_message(self, message): if self.isVisible(): event = message.get("event") if event == "options": options = message.get("options") self.set_options(options.get("generators"), options.get("icons")) elif event == "started": self.set_started(message.get("generator_name")) elif event == "stopped": self.set_stopped() elif event == "progress": self.set_progress(message.get("generator_name"), message.get("progress")) elif event == "error": self.set_error(message.get("error")) elif event == "generator_finished": self.set_generator_finished(message.get("generator_name"), message.get("exit_code")) elif event == "build_finished": self.set_build_finished() @Slot(str, str) def set_progress(self, generator_name, progress): for win in self.progress_windows: if win.generator() == generator_name: win.appendProgress(progress) @Slot(str) def set_started(self, generator_name): win = ProgressWindow(self, generator_name) win.show() self.progress_windows.append(win) self.logger.info("Generator {} started!".format(generator_name)) self.reload_action.setEnabled(False) self.spinner.start() @Slot(str, int) def set_generator_finished(self, generator_name, exit_code): self.logger.info("Generator {} finished with exit code {}.".format( generator_name, exit_code)) @Slot(list, list) def set_options(self, generators, icons): self.build_name_edit.clear() self.generators_list.clear() self.icon_combobox.clear() for generator in generators: item = QListWidgetItem(generator, self.generators_list) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) self.generators_list.addItem(item) for icon in icons: name = icon.get("name") pix = QPixmap() pix.loadFromData(base64.b64decode(icon.get("ico"))) ico = QIcon() ico.addPixmap(pix) self.icon_combobox.addItem(ico, name) self.spinner.stop() @Slot() def set_stopped(self): self.on_build_finished() self.build_button.setText("Build") self.stopped_messagebox = FramelessInformationMessageBox(self) self.stopped_messagebox.setText( "Build process has been stopped successfully.") self.stopped_messagebox.setStandardButtons(QDialogButtonBox.Ok) self.stopped_messagebox.button(QDialogButtonBox.Ok).clicked.connect( self.stopped_messagebox.close) self.stopped_messagebox.show() @Slot(str) def set_error(self, error): self.on_build_finished() self.build_button.setText("Build") self.error_messagebox = FramelessCriticalMessageBox(self) self.error_messagebox.setText(error) self.error_messagebox.setStandardButtons(QDialogButtonBox.Ok) self.error_messagebox.button(QDialogButtonBox.Ok).clicked.connect( self.error_messagebox.close) self.error_messagebox.show() @Slot() def set_build_finished(self): self.on_build_finished() self.build_button.setText("Build") self.build_finished_messagebox = FramelessInformationMessageBox(self) self.build_finished_messagebox.setText( "Build process has been finished.") self.build_finished_messagebox.setStandardButtons(QDialogButtonBox.Ok) self.build_finished_messagebox.button( QDialogButtonBox.Ok).clicked.connect( self.build_finished_messagebox.close) self.build_finished_messagebox.show() @Slot() def on_build_button_clicked(self): if self.build_button.text() == "Build": generators = [] for i in range(self.generators_list.count()): item = self.generators_list.item(i) if item.checkState() == Qt.Checked: generators.append(item.text()) self.start_build.emit(self.build_name_edit.text(), self.icon_combobox.currentText(), generators) self.build_button.setText("Stop") else: self.stop_build_messagebox = FramelessQuestionMessageBox(self) self.stop_build_messagebox.setText( "Do you want to stop build process?") self.stop_build_messagebox.setStandardButtons( QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.stop_build_messagebox.button( QDialogButtonBox.Cancel).clicked.connect( self.stop_build_messagebox.close) self.stop_build_messagebox.button( QDialogButtonBox.Ok).clicked.connect(self.stop_build.emit) self.stop_build_messagebox.button( QDialogButtonBox.Ok).clicked.connect( self.stop_build_messagebox.close) self.stop_build_messagebox.show() @Slot() def on_build_finished(self): self.reload_action.setEnabled(True) self.spinner.stop() @Slot() def close(self) -> bool: for win in self.progress_windows: win.close() return super().close()
class MainWindow(QMainWindow): signal_fitting_parameters_changed = Signal() def __init__(self, *, gpc): """ Parameters ---------- gpc: object reference to a class that holds references to processing classes. """ super().__init__() self._cursor_set = False # Indicates if temporary 'wait' cursor is set self.gpc = gpc self.gui_vars = global_gui_variables self.gui_vars["ref_main_window"] = self self.wnd_manage_emission_lines = WndManageEmissionLines(gpc=self.gpc, gui_vars=self.gui_vars) self.wnd_compute_roi_maps = WndComputeRoiMaps(gpc=self.gpc, gui_vars=self.gui_vars) self.wnd_image_wizard = WndImageWizard(gpc=self.gpc, gui_vars=self.gui_vars) self.wnd_load_quantitative_calibration = WndLoadQuantitativeCalibration( gpc=self.gpc, gui_vars=self.gui_vars ) self.wnd_general_fitting_settings = WndGeneralFittingSettings(gpc=self.gpc, gui_vars=self.gui_vars) self.wnd_fitting_parameters_shared = WndDetailedFittingParamsShared(gpc=self.gpc, gui_vars=self.gui_vars) self.wnd_fitting_parameters_lines = WndDetailedFittingParamsLines(gpc=self.gpc, gui_vars=self.gui_vars) # Indicates that the window was closed (used mostly for testing) self._is_closed = False global_gui_variables["gui_state"]["databroker_available"] = self.gpc.is_databroker_available() self.initialize() self.central_widget.left_panel.load_data_widget.update_main_window_title.connect(self.update_window_title) # Set the callback for update forms with fitting parameters def update_fitting_parameter_forms(): self.signal_fitting_parameters_changed.emit() gpc.add_parameters_changed_cb(update_fitting_parameter_forms) gpc.param_model.parameters_changed() def initialize(self): self.resize(_main_window_geometry["initial_width"], _main_window_geometry["initial_height"]) self.setMinimumWidth(_main_window_geometry["min_width"]) self.setMinimumHeight(_main_window_geometry["min_height"]) self.setWindowTitle(self.gpc.get_window_title()) self.central_widget = TwoPanelWidget(gpc=self.gpc, gui_vars=self.gui_vars) self.setCentralWidget(self.central_widget) # Status bar self.statusLabel = QLabel() self.statusBar().addWidget(self.statusLabel) self.statusProgressBar = QProgressBar() self.statusProgressBar.setFixedWidth(200) self.statusBar().addPermanentWidget(self.statusProgressBar) self.statusLabelDefaultText = "No data is loaded" self.statusLabel.setText(self.statusLabelDefaultText) # 'Scan Data' menu item self.action_read_file = QAction("&Read File...", self) self.action_read_file.setStatusTip("Load data from HDF5 file") self.action_read_file.triggered.connect(self.central_widget.left_panel.load_data_widget.pb_file.clicked) self.action_load_run = QAction("&Load Run...", self) self.action_load_run.setEnabled(self.gui_vars["gui_state"]["databroker_available"]) self.action_load_run.setStatusTip("Load data from database (Databroker)") self.action_load_run.triggered.connect(self.central_widget.left_panel.load_data_widget.pb_dbase.clicked) self.action_view_metadata = QAction("View Metadata...", self) self.action_view_metadata.setEnabled(self.gpc.is_scan_metadata_available()) self.action_view_metadata.setStatusTip("View metadata for loaded run") self.action_view_metadata.triggered.connect( self.central_widget.left_panel.load_data_widget.pb_view_metadata.clicked ) # Main menu menubar = self.menuBar() # Disable native menu bar (it doesn't work on MacOS 10.15 with PyQt<=5.11) # It may work with later versions of PyQt when they become available. menubar.setNativeMenuBar(False) loadData = menubar.addMenu("Scan &Data") loadData.addAction(self.action_read_file) loadData.addAction(self.action_load_run) loadData.addSeparator() loadData.addAction(self.action_view_metadata) # 'Fitting Model' menu item self.action_lines_find_automatically = QAction("Find &Automatically...", self) self.action_lines_find_automatically.setStatusTip("Automatically find emission lines in total spectrum") self.action_lines_find_automatically.triggered.connect( self.central_widget.left_panel.model_widget.pb_find_elines.clicked ) self.action_lines_load_from_file = QAction("Load From &File...", self) self.action_lines_load_from_file.setStatusTip( "Load processing parameters, including selected emission lines, from JSON file" ) self.action_lines_load_from_file.triggered.connect( self.central_widget.left_panel.model_widget.pb_load_elines.clicked ) self.action_lines_load_quant_standard = QAction("Load &Quantitative Standards...", self) self.action_lines_load_quant_standard.setStatusTip( "Load quantitative standard. The emission lines from the standard are automatically selected" ) self.action_lines_load_quant_standard.triggered.connect( self.central_widget.left_panel.model_widget.pb_load_qstandard.clicked ) self.action_add_remove_emission_lines = QAction("&Add/Remove Emission Lines...", self) self.action_add_remove_emission_lines.setStatusTip("Manually add and remove emission lines") self.action_add_remove_emission_lines.triggered.connect( self.central_widget.left_panel.model_widget.pb_manage_emission_lines.clicked ) self.action_save_model_params = QAction("&Save Model Parameters...", self) self.action_save_model_params.setStatusTip("Save model parameters to JSON file") self.action_save_model_params.triggered.connect( self.central_widget.left_panel.model_widget.pb_save_elines.clicked ) self.action_add_remove_emission_lines = QAction("Start Model &Fitting", self) self.action_add_remove_emission_lines.setStatusTip("Run computations: start fitting for total spectrum") self.action_add_remove_emission_lines.triggered.connect( self.central_widget.left_panel.model_widget.pb_start_fitting.clicked ) fittingModel = menubar.addMenu("Fitting &Model") emissionLines = fittingModel.addMenu("&Emission Lines") emissionLines.addAction(self.action_lines_find_automatically) emissionLines.addAction(self.action_lines_load_from_file) emissionLines.addAction(self.action_lines_load_quant_standard) fittingModel.addAction(self.action_add_remove_emission_lines) fittingModel.addSeparator() fittingModel.addAction(self.action_save_model_params) fittingModel.addSeparator() fittingModel.addAction(self.action_add_remove_emission_lines) # "XRF Maps" menu item self.action_start_xrf_map_fitting = QAction("Start XRF Map &Fitting", self) self.action_start_xrf_map_fitting.setStatusTip("Run computations: start fitting for XRF maps") self.action_start_xrf_map_fitting.triggered.connect( self.central_widget.left_panel.fit_maps_widget.pb_start_map_fitting.clicked ) self.action_compute_rois = QAction("Compute &ROIs...", self) self.action_compute_rois.setStatusTip("Compute XRF Maps based on spectral ROIs") self.action_compute_rois.triggered.connect( self.central_widget.left_panel.fit_maps_widget.pb_compute_roi_maps.clicked ) self.action_load_quant_calibration = QAction("&Load Quantitative Calibration...", self) self.action_load_quant_calibration.setStatusTip( "Load quantitative calibration from JSON file. Calibration is used for scaling of XRF Maps" ) self.action_load_quant_calibration.triggered.connect( self.central_widget.left_panel.fit_maps_widget.pb_load_quant_calib.clicked ) self.action_save_quant_calibration = QAction("&Save Quantitative Calibration...", self) self.action_save_quant_calibration.setStatusTip( "Save Quantitative Calibration based on XRF map of the standard sample" ) self.action_save_quant_calibration.triggered.connect( self.central_widget.left_panel.fit_maps_widget.pb_save_q_calibration.clicked ) self.action_export_to_tiff_and_txt = QAction("&Export to TIFF and TXT...", self) self.action_export_to_tiff_and_txt.setStatusTip("Export XRF Maps as TIFF and/or TXT files") self.action_export_to_tiff_and_txt.triggered.connect( self.central_widget.left_panel.fit_maps_widget.pb_export_to_tiff_and_txt.clicked ) xrfMaps = menubar.addMenu("XRF &Maps") xrfMaps.addAction(self.action_start_xrf_map_fitting) xrfMaps.addAction(self.action_compute_rois) xrfMaps.addSeparator() xrfMaps.addAction(self.action_load_quant_calibration) xrfMaps.addSeparator() xrfMaps.addAction(self.action_save_quant_calibration) xrfMaps.addAction(self.action_export_to_tiff_and_txt) # "View" menu item self.action_show_matplotlib_toolbar = QAction("Show &Matplotlib Toolbar", self) self.action_show_matplotlib_toolbar.setCheckable(True) self.action_show_matplotlib_toolbar.setChecked(True) self.action_show_matplotlib_toolbar.setStatusTip("Show Matplotlib Toolbar on the plots") self.action_show_matplotlib_toolbar.toggled.connect(self.action_show_matplotlib_toolbar_toggled) self.action_show_widget_tooltips = QAction("Show &Tooltips", self) self.action_show_widget_tooltips.setCheckable(True) self.action_show_widget_tooltips.setChecked(self.gui_vars["show_tooltip"]) self.action_show_widget_tooltips.setStatusTip("Show widget tooltips") self.action_show_widget_tooltips.toggled.connect(self.action_show_widget_tooltips_toggled) options = menubar.addMenu("&Options") options.addAction(self.action_show_widget_tooltips) options.addAction(self.action_show_matplotlib_toolbar) # "Help" menu item self.action_online_docs = QAction("Online &Documentation", self) self.action_online_docs.setStatusTip("Open online documentation in the default browser") self.action_online_docs.triggered.connect(self.action_online_docs_triggered) self.action_about = QAction("&About PyXRF", self) self.action_about.setStatusTip("Show information about this program") self.action_about.triggered.connect(self.action_about_triggered) help = menubar.addMenu("&Help") help.addAction(self.action_online_docs) help.addSeparator() help.addAction(self.action_about) self.update_widget_state() # Connect signals self.central_widget.left_panel.load_data_widget.update_preview_map_range.connect( self.central_widget.right_panel.tab_preview_plots.preview_plot_count.update_map_range ) # Before loading a new file or run self.central_widget.left_panel.load_data_widget.signal_loading_new_run.connect( self.central_widget.right_panel.slot_activate_tab_preview ) # Open a new file or run self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.central_widget.left_panel.slot_activate_load_data_tab ) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.central_widget.right_panel.slot_activate_tab_preview ) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect(self.slot_new_run_loaded) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.wnd_image_wizard.slot_update_table ) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.central_widget.right_panel.tab_plot_xrf_maps.slot_update_dataset_info ) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_dataset_info ) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.wnd_load_quantitative_calibration.update_all_data ) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.central_widget.left_panel.fit_maps_widget.slot_update_for_new_loaded_run ) # New model is loaded or processing parameters (incident energy) was changed self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect( self.central_widget.right_panel.tab_preview_plots.preview_plot_spectrum.redraw_preview_plot ) self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect( self.wnd_manage_emission_lines.update_widget_data ) self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect( self.central_widget.right_panel.tab_plot_fitting_model.redraw_plot_fit ) self.central_widget.left_panel.model_widget.signal_model_loaded.connect( self.central_widget.right_panel.tab_preview_plots.preview_plot_spectrum.redraw_preview_plot ) self.central_widget.left_panel.model_widget.signal_model_loaded.connect( self.central_widget.right_panel.slot_activate_tab_fitting_model ) self.central_widget.left_panel.model_widget.signal_model_loaded.connect( self.central_widget.right_panel.tab_plot_fitting_model.update_controls ) self.central_widget.left_panel.model_widget.signal_model_loaded.connect( self.wnd_manage_emission_lines.update_widget_data ) # XRF Maps dataset changed self.central_widget.right_panel.tab_plot_xrf_maps.signal_maps_dataset_selection_changed.connect( self.wnd_image_wizard.slot_update_table ) self.central_widget.right_panel.tab_plot_xrf_maps.signal_maps_dataset_selection_changed.connect( self.central_widget.right_panel.tab_plot_rgb_maps.combo_select_dataset_update_current_index ) self.central_widget.right_panel.tab_plot_rgb_maps.signal_rgb_maps_dataset_selection_changed.connect( self.central_widget.right_panel.tab_plot_xrf_maps.combo_select_dataset_update_current_index ) self.central_widget.right_panel.tab_plot_xrf_maps.signal_maps_norm_changed.connect( self.wnd_image_wizard.slot_update_ranges ) # Quantitative calibration changed self.wnd_load_quantitative_calibration.signal_quantitative_calibration_changed.connect( self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_ranges ) self.wnd_load_quantitative_calibration.signal_quantitative_calibration_changed.connect( self.wnd_image_wizard.slot_update_ranges ) # Selected element is changed (tools for emission line selection) self.wnd_manage_emission_lines.signal_selected_element_changed.connect( self.central_widget.right_panel.tab_plot_fitting_model.slot_selection_item_changed ) self.central_widget.right_panel.tab_plot_fitting_model.signal_selected_element_changed.connect( self.wnd_manage_emission_lines.slot_selection_item_changed ) self.central_widget.right_panel.tab_plot_fitting_model.signal_add_line.connect( self.wnd_manage_emission_lines.pb_add_eline_clicked ) self.central_widget.right_panel.tab_plot_fitting_model.signal_remove_line.connect( self.wnd_manage_emission_lines.pb_remove_eline_clicked ) self.wnd_manage_emission_lines.signal_update_element_selection_list.connect( self.central_widget.right_panel.tab_plot_fitting_model.slot_update_eline_selection_list ) self.wnd_manage_emission_lines.signal_update_add_remove_btn_state.connect( self.central_widget.right_panel.tab_plot_fitting_model.slot_update_add_remove_btn_state ) self.wnd_manage_emission_lines.signal_selected_element_changed.connect( self.central_widget.left_panel.model_widget.slot_selection_item_changed ) self.central_widget.right_panel.tab_plot_fitting_model.signal_selected_element_changed.connect( self.central_widget.left_panel.model_widget.slot_selection_item_changed ) # Total spectrum fitting completed self.central_widget.left_panel.model_widget.signal_total_spectrum_fitting_completed.connect( self.wnd_manage_emission_lines.update_eline_table ) # Total spectrum invalidated self.wnd_manage_emission_lines.signal_parameters_changed.connect( self.central_widget.left_panel.model_widget.update_fit_status ) # New dataset loaded or different channel selected. Compute fit parameters. self.central_widget.left_panel.load_data_widget.signal_data_channel_changed.connect( self.central_widget.left_panel.model_widget.clear_fit_status ) self.central_widget.left_panel.load_data_widget.signal_new_run_loaded.connect( self.central_widget.left_panel.model_widget.clear_fit_status ) self.central_widget.left_panel.model_widget.signal_incident_energy_or_range_changed.connect( self.central_widget.left_panel.model_widget.clear_fit_status ) # Update map datasets (Fitted maps) self.central_widget.left_panel.fit_maps_widget.signal_map_fitting_complete.connect( self.central_widget.right_panel.tab_plot_xrf_maps.slot_update_dataset_info ) self.central_widget.left_panel.fit_maps_widget.signal_map_fitting_complete.connect( self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_dataset_info ) self.central_widget.left_panel.fit_maps_widget.signal_activate_tab_xrf_maps.connect( self.central_widget.right_panel.slot_activate_tab_xrf_maps ) # Update map datasets (ROI maps) self.wnd_compute_roi_maps.signal_roi_computation_complete.connect( self.central_widget.right_panel.tab_plot_xrf_maps.slot_update_dataset_info ) self.wnd_compute_roi_maps.signal_roi_computation_complete.connect( self.central_widget.right_panel.tab_plot_rgb_maps.slot_update_dataset_info ) self.wnd_compute_roi_maps.signal_activate_tab_xrf_maps.connect( self.central_widget.right_panel.slot_activate_tab_xrf_maps ) self.signal_fitting_parameters_changed.connect(self.wnd_general_fitting_settings.update_form_data) self.signal_fitting_parameters_changed.connect(self.wnd_fitting_parameters_shared.update_form_data) self.signal_fitting_parameters_changed.connect(self.wnd_fitting_parameters_lines.update_form_data) self.signal_fitting_parameters_changed.connect(self.wnd_manage_emission_lines.update_widget_data) @Slot() @Slot(str) def update_widget_state(self, condition=None): # Update the state of the menu bar state = not self.gui_vars["gui_state"]["running_computations"] self.menuBar().setEnabled(state) state_computations = self.gui_vars["gui_state"]["running_computations"] if state_computations: if not self._cursor_set: QGuiApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self._cursor_set = True else: if self._cursor_set: QGuiApplication.restoreOverrideCursor() self._cursor_set = False # Forward to children self.central_widget.update_widget_state(condition) # Forward the updates to open windows self.wnd_manage_emission_lines.update_widget_state(condition) self.wnd_compute_roi_maps.update_widget_state(condition) self.wnd_image_wizard.update_widget_state(condition) self.wnd_load_quantitative_calibration.update_widget_state(condition) self.wnd_general_fitting_settings.update_widget_state(condition) self.wnd_fitting_parameters_shared.update_widget_state(condition) self.wnd_fitting_parameters_lines.update_widget_state(condition) def closeEvent(self, event): mb_close = QMessageBox( QMessageBox.Question, "Exit", "Are you sure you want to EXIT the program?", QMessageBox.Yes | QMessageBox.No, parent=self, ) mb_close.setDefaultButton(QMessageBox.No) if mb_close.exec() == QMessageBox.Yes: event.accept() self.wnd_manage_emission_lines.close() self.wnd_compute_roi_maps.close() self.wnd_image_wizard.close() self.wnd_load_quantitative_calibration.close() self.wnd_general_fitting_settings.close() self.wnd_fitting_parameters_shared.close() self.wnd_fitting_parameters_lines.close() # This flag is used for CI tests self._is_closed = True else: event.ignore() def action_online_docs_triggered(self): """ Display online documentation: open the URL in the default browser. """ doc_url = "http://nsls-ii.github.io/PyXRF/" try: webbrowser.open(doc_url, autoraise=True) except Exception as ex: logger.error(f"Error occurred while opening URL '{doc_url}' in the default browser") msg = f"Failed to Open Online Documentation. \n Exception: {str(ex)}" msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() def action_about_triggered(self): """ Display 'About' dialog box """ dlg = DialogAbout() dlg.exec() def action_show_matplotlib_toolbar_toggled(self, state): """ Turn tooltips on or off """ self.gui_vars["show_matplotlib_toolbar"] = state self.update_widget_state() def action_show_widget_tooltips_toggled(self, state): """ Turn tooltips on or off """ self.gui_vars["show_tooltip"] = state self.update_widget_state("tooltips") @Slot() def update_window_title(self): self.setWindowTitle(self.gpc.get_window_title()) @Slot(bool) def slot_new_run_loaded(self, success): if success: # Update status bar file_name = self.gpc.get_loaded_file_name() run_id, run_uid = "", "" if self.gpc.is_scan_metadata_available(): try: run_id = self.gpc.get_metadata_scan_id() except Exception: pass try: run_uid = self.gpc.get_metadata_scan_uid() except Exception: pass else: if self.gpc.get_current_run_id() >= 0: run_id = self.gpc.get_current_run_id() s = "" if run_id: s += f"ID: {run_id} " if run_uid: if run_id: s += f"({run_uid}) " else: s += f"UID: {run_uid} " if file_name: s += f"File: '{file_name}'" self._set_status_bar_text(s) # Activate/deactivate "View Metadata ..." menu item if self.gpc.is_scan_metadata_available(): self.action_view_metadata.setEnabled(True) else: self.action_view_metadata.setEnabled(False) else: self._set_status_bar_text() def _set_status_bar_text(self, text=None): if text is None: text = self.statusLabelDefaultText self.statusLabel.setText(text)
class StructureSynthesis(QWidget, Ui_Form): """Number and type synthesis widget. Calculate the combinations of mechanism family and show the atlas. """ assortment: Dict[Assortment, List[Assortment]] answer: List[Graph] def __init__(self, parent: MainWindowBase): """Reference names: + IO functions from main window. + Table data from PMKS expression. + Graph data function from main window. """ super(StructureSynthesis, self).__init__(parent) self.setupUi(self) header = self.link_assortment_list.header() header.setSectionResizeMode(QHeaderView.ResizeToContents) # Function references self.output_to = parent.output_to self.save_reply_box = parent.save_reply_box self.input_from_multiple = parent.input_from_multiple self.vpoints = parent.vpoint_list self.vlinks = parent.vlink_list self.get_graph = parent.get_graph self.prefer = parent.prefer self.add_collection = parent.collections.structure_widget.add_collection # Answer list self.assortment = {} self.answer = [] # Signals self.nl_input.valueChanged.connect(self.__adjust_structure_data) self.nj_input.valueChanged.connect(self.__adjust_structure_data) self.graph_engine.addItems(engines) self.structure_list.customContextMenuRequested.connect( self.__structure_list_context_menu ) # Context menu self.pop_menu_topo = QMenu(self) self.to_collection = QAction( QIcon(QPixmap(":/icons/collections.png")), "Add to collections", self ) self.copy_edges = QAction("Copy edges", self) self.copy_image = QAction("Copy image", self) self.pop_menu_topo.addActions([ self.to_collection, self.copy_edges, self.copy_image, ]) self.nl_input_old_value = 0 self.nj_input_old_value = 0 self.clear() def clear(self) -> None: """Clear all sub-widgets.""" self.edges_text.clear() self.__clear_assortment() self.__clear_structure_list() self.nl_input.setValue(0) self.nj_input.setValue(0) self.nl_input_old_value = 0 self.nj_input_old_value = 0 self.dof.setValue(1) @Slot(name='on_assortment_clear_button_clicked') def __clear_assortment(self) -> None: """Clear the number synthesis list.""" self.link_assortment_list.clear() self.assortment.clear() @Slot(name='on_structure_list_clear_button_clicked') def __clear_structure_list(self) -> None: """Clear the structure list.""" self.answer.clear() self.structure_list.clear() self.time_label.setText("") @Slot(name='on_from_mechanism_button_clicked') def __from_mechanism(self) -> None: """From a generalized mechanism of main canvas.""" if self.vpoints and self.vlinks: graph, _, _, _, _, _ = self.get_graph() else: graph = Graph([]) if graph.edges: self.edges_text.setText(str(list(graph.edges))) else: self.edges_text.setText("") keep_dof_checked = self.keep_dof.isChecked() self.keep_dof.setChecked(False) self.nl_input.setValue(len(graph.vertices)) self.nj_input.setValue(len(graph.edges)) self.keep_dof.setChecked(keep_dof_checked) # Show attributes QMessageBox.information( self, "Generalization", f"Link assortment:\n{link_assortment(graph)}\n" f"Contracted link assortment:\n{contracted_link_assortment(graph)}" if graph.edges else "Is a empty graph." ) def __adjust_structure_data(self) -> None: """Update NJ and NL values. If user don't want to keep the DOF: Change the DOF then exit. """ if not self.keep_dof.isChecked(): self.dof.setValue( 3 * (self.nl_input.value() - 1) - 2 * self.nj_input.value() ) return # N2: Get the user's adjusted value. # NL_func: Get the another value of parameters (N1) by degrees of freedom formula. # is_above: Is value increase or decrease? if self.sender() is self.nj_input: n2 = self.nj_input.value() def nl_func() -> float: return (self.dof.value() + 2 * n2) / 3 + 1 is_above = n2 > self.nj_input_old_value else: n2 = self.nl_input.value() def nl_func() -> float: return (3 * (n2 - 1) - self.dof.value()) / 2 is_above = n2 > self.nl_input_old_value n1 = nl_func() while not n1.is_integer(): n2 += 1 if is_above else -1 n1 = nl_func() if n1 == 0 or n2 == 0: break n1 = int(n1) n2 = int(n2) # Return the result values # + Value of widgets. # + Setting old value record. if self.sender() is self.nl_input: self.nj_input.setValue(n1) self.nl_input.setValue(n2) self.nj_input_old_value = n1 self.nl_input_old_value = n2 else: self.nj_input.setValue(n2) self.nl_input.setValue(n1) self.nj_input_old_value = n2 self.nl_input_old_value = n1 @Slot(name='on_number_synthesis_button_clicked') def __number_synthesis(self) -> None: """Synthesis of link assortment.""" self.__clear_assortment() nl = self.nl_input.value() nj = self.nj_input.value() dlg = SynthesisProgressDialog( "Link assortment", f"Number of links: {nl}\n" f"Number of joints: {nj}", 1, self ) @Slot(dict) def update_result(assortment: Dict[Assortment, List[Assortment]]) -> None: """Update results.""" self.assortment.update(assortment) for la, cla_list in assortment.items(): la_item = QTreeWidgetItem([", ".join( f"NL{i + 2} = {a}" for i, a in enumerate(la) ), "N/A"]) for cla in cla_list: la_item.addChild(QTreeWidgetItem([", ".join( f"NC{i + 1} = {a}" for i, a in enumerate(cla) ), "N/A"])) self.link_assortment_list.addTopLevelItem(la_item) first_item = self.link_assortment_list.topLevelItem(0) self.link_assortment_list.setCurrentItem(first_item) dlg.deleteLater() work = LinkThread(nl, nj, dlg) work.progress_update.connect(dlg.setValue) work.size_update.connect(dlg.setMaximum) work.result.connect(update_result) dlg.show() work.start() def __set_time_count(self, t: float, count: int) -> None: """Set time and count digit to label.""" self.time_label.setText(f"{t:.04f} s ({count})") def __set_paint_time(self, t: float) -> None: """Set painting time of atlas.""" self.paint_time_label.setText(f"{t:.04f}s") @Slot(name='on_structure_synthesis_button_clicked') def __structure_synthesis(self) -> None: """Structural synthesis - find by contracted links.""" self.__clear_structure_list() item = self.link_assortment_list.currentItem() if item is None: self.__number_synthesis() item = self.link_assortment_list.currentItem() root = item.parent() if root is None: # Find by link assortment try: # Test assortment_eval(item.text(0)) except ValueError: return jobs = [item.child(i) for i in range(item.childCount())] else: # Find by contracted link assortment jobs = [item] self.__structural_combine(jobs) @Slot(name='on_structure_synthesis_all_button_clicked') def __structure_synthesis_all(self) -> None: """Structural synthesis - find all.""" self.__clear_structure_list() jobs = [] for i in range(self.link_assortment_list.topLevelItemCount()): root = self.link_assortment_list.topLevelItem(i) for j in range(root.childCount()): jobs.append(root.child(j)) self.__structural_combine(jobs) def __structural_combine(self, jobs: Sequence[QTreeWidgetItem]) -> None: """Structural combine by iterator.""" t0 = process_time() dlg = SynthesisProgressDialog( "Structural Synthesis", f"Number of cases: {len(jobs)}", len(jobs), self ) @Slot(QTreeWidgetItem, int) def update_count(item: QTreeWidgetItem, count: int) -> None: """Update the number of graphs.""" item.setText(1, f"{count}") @Slot(list) def update_result(answer: List[Graph]) -> None: """Update the result of atlas.""" self.answer = answer dlg.deleteLater() for i in range(self.link_assortment_list.topLevelItemCount()): root = self.link_assortment_list.topLevelItem(i) count = 0 for j in range(root.childCount()): item = root.child(j) try: count += int(item.text(1)) except ValueError: pass root.setText(1, f"{count}") self.__set_time_count(process_time() - t0, len(self.answer)) self.__reload_atlas() work = GraphThread(jobs, self.graph_degenerate.currentIndex(), dlg) work.count_update.connect(update_count) work.progress_update.connect(dlg.setValue) work.result.connect(update_result) dlg.show() work.start() @Slot(name='on_reload_atlas_clicked') @Slot(bool, name='on_graph_link_as_node_toggled') @Slot(bool, name='on_graph_show_label_toggled') @Slot(int, name='on_graph_engine_currentIndexChanged') def __reload_atlas(self, *_) -> None: """Reload the atlas.""" scroll_bar: QScrollBar = self.structure_list.verticalScrollBar() scroll_pos = scroll_bar.sliderPosition() index = self.structure_list.currentRow() self.structure_list.clear() if not self.answer: return dlg = SynthesisProgressDialog( "Structural Synthesis", f"Drawing atlas ({len(self.answer)}) ...", len(self.answer), self ) dlg.show() t0 = process_time() for i, G in enumerate(self.answer): QCoreApplication.processEvents() if dlg.wasCanceled(): return if self.__draw_atlas(i, G): dlg.setValue(i + 1) else: break self.__set_paint_time(process_time() - t0) dlg.setValue(dlg.maximum()) dlg.deleteLater() scroll_bar.setSliderPosition(scroll_pos) self.structure_list.setCurrentRow(index) def __draw_atlas(self, i: int, g: Graph) -> bool: """Draw atlas and return True if done.""" item = QListWidgetItem(f"No. {i + 1}") item.setIcon(graph2icon( g, self.structure_list.iconSize().width(), self.graph_link_as_node.isChecked(), self.graph_show_label.isChecked(), self.prefer.monochrome_option, engine=self.graph_engine.currentText() )) item.setToolTip( f"Edge Set: {list(g.edges)}\n" f"Link assortment: {link_assortment(g)}\n" f"Contracted Link assortment: {contracted_link_assortment(g)}\n" f"Degree code: {g.degree_code()}" ) self.structure_list.addItem(item) return True def __atlas_image(self, row: Optional[int] = None) -> QImage: """Capture a result item icon to image.""" if row is None: item = self.structure_list.currentItem() else: item = self.structure_list.item(row) return item.icon().pixmap(self.structure_list.iconSize()).toImage() @Slot(QPoint) def __structure_list_context_menu(self, point) -> None: """Context menu for the type synthesis results.""" index = self.structure_list.currentIndex().row() self.to_collection.setEnabled(index > -1) self.copy_edges.setEnabled(index > -1) self.copy_image.setEnabled(index > -1) action = self.pop_menu_topo.exec_(self.structure_list.mapToGlobal(point)) if not action: return clipboard = QApplication.clipboard() if action == self.to_collection: self.add_collection(self.answer[index].edges) elif action == self.copy_edges: clipboard.setText(str(self.answer[index].edges)) elif action == self.copy_image: # Turn the transparent background to white image1 = self.__atlas_image() image2 = QImage(image1.size(), image1.format()) image2.fill(Qt.white) painter = QPainter(image2) painter.drawImage(QPointF(0, 0), image1) painter.end() clipboard.setPixmap(QPixmap.fromImage(image2)) @Slot(name='on_expr_copy_clicked') def __copy_expr(self) -> None: """Copy expression button.""" string = self.edges_text.text() if string: QApplication.clipboard().setText(string) self.edges_text.selectAll() @Slot(name='on_expr_add_collection_clicked') def __add_collection(self) -> None: """Add this expression to collections widget.""" string = self.edges_text.text() if string: self.add_collection(eval(string)) @Slot(name='on_save_atlas_clicked') def __save_atlas(self) -> None: """Saving all the atlas to image file. We should turn transparent background to white first. Then using QImage class to merge into one image. """ count = self.structure_list.count() if count < 1: return lateral = self.__save_atlas_ask() if not lateral: return file_name = self.output_to("atlas image", qt_image_format) if not file_name: return width = self.structure_list.iconSize().width() image_main = QImage(QSize( lateral * width if count > lateral else count * width, ((count // lateral) + bool(count % lateral)) * width ), self.__atlas_image(0).format()) image_main.fill(Qt.transparent) painter = QPainter(image_main) for row in range(count): image = self.__atlas_image(row) painter.drawImage(QPointF(row % lateral, row // lateral) * width, image) painter.end() pixmap = QPixmap.fromImage(image_main) pixmap.save(file_name) self.save_reply_box("Atlas", file_name) def __save_atlas_ask(self) -> int: """Ask when saving the atlas.""" lateral, ok = QInputDialog.getInt( self, "Atlas", "The number of lateral:", 5, 1 ) if not ok: return 0 return lateral @Slot(name='on_save_edges_clicked') def __save_edges(self) -> None: """Saving all the atlas to text file.""" file_name = "" count = self.structure_list.count() if count < 1: return if not file_name: file_name = self.output_to("atlas edges expression", ["Text file (*.txt)"]) if not file_name: return with open(file_name, 'w+', encoding='utf-8') as f: f.write('\n'.join(str(G.edges) for G in self.answer)) self.save_reply_box("edges expression", file_name) @Slot(name='on_edges2atlas_button_clicked') def __edges2atlas(self) -> None: """Turn the text files into a atlas image. This operation will load all edges to list widget first. """ file_names = self.input_from_multiple( "edges data", ["Text file (*.txt)"] ) if not file_names: return read_data = [] for file_name in file_names: with open(file_name, 'r', encoding='utf-8') as f: for line in f: read_data.append(line) answer = [] for edges in read_data: try: g = Graph(eval(edges)) except (SyntaxError, TypeError): QMessageBox.warning( self, "Wrong format", "Please check text format." ) else: answer.append(g) if not answer: QMessageBox.information( self, "No data", "The graph data is empty." ) return self.answer = answer self.__set_time_count(0, len(answer)) self.__reload_atlas() self.__save_atlas()
class MainWindow(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.recording_enabled = False self.serial = serial.Serial() self.rootnode = JsonNode('') self._connected = False self._dirty = False self._filename = None # settings self.settings = QSettingsManager() set_default_settings(self.settings) # Controller Settings self.settingsDialog = None # object explorer self.objectexplorer = ObjectExplorer(self.rootnode, self) self.objectexplorer.nodevalue_changed.connect(self.send_serialdata) self.objectexplorer.nodeproperty_changed.connect(self.set_dirty) self.objectexplorerDockWidget = QDockWidget(self.tr("object explorer"), self) self.objectexplorerDockWidget.setObjectName( "objectexplorer_dockwidget") self.objectexplorerDockWidget.setWidget(self.objectexplorer) # plot widget self.plot = PlotWidget(self.rootnode, self.settings, self) # plot settings self.plotsettings = PlotSettingsWidget(self.settings, self.plot, self) self.plotsettingsDockWidget = QDockWidget(self.tr("plot settings"), self) self.plotsettingsDockWidget.setObjectName("plotsettings_dockwidget") self.plotsettingsDockWidget.setWidget(self.plotsettings) # log widget self.loggingWidget = LoggingWidget(self) self.loggingDockWidget = QDockWidget(self.tr("logger"), self) self.loggingDockWidget.setObjectName("logging_dockwidget") self.loggingDockWidget.setWidget(self.loggingWidget) # record widget self.recordWidget = RecordWidget(self.rootnode, self) self.recordDockWidget = QDockWidget(self.tr("data recording"), self) self.recordDockWidget.setObjectName("record_dockwidget") self.recordDockWidget.setWidget(self.recordWidget) # actions and menus self._init_actions() self._init_menus() # statusbar statusbar = self.statusBar() statusbar.setVisible(True) self.connectionstateLabel = QLabel(self.tr("Not connected")) statusbar.addPermanentWidget(self.connectionstateLabel) statusbar.showMessage(self.tr("Ready")) # layout self.setCentralWidget(self.plot) self.addDockWidget(Qt.LeftDockWidgetArea, self.objectexplorerDockWidget) self.addDockWidget(Qt.LeftDockWidgetArea, self.plotsettingsDockWidget) self.addDockWidget(Qt.BottomDockWidgetArea, self.loggingDockWidget) self.addDockWidget(Qt.BottomDockWidgetArea, self.recordDockWidget) self.load_settings() def _init_actions(self): # Serial Dialog self.serialdlgAction = QAction(self.tr("Serial Settings..."), self) self.serialdlgAction.setShortcut("F6") self.serialdlgAction.setIcon(QIcon(pixmap("configure.png"))) self.serialdlgAction.triggered.connect(self.show_serialdlg) # Connect self.connectAction = QAction(self.tr("Connect"), self) self.connectAction.setShortcut("F5") self.connectAction.setIcon(QIcon(pixmap("network-connect-3.png"))) self.connectAction.triggered.connect(self.toggle_connect) # Quit self.quitAction = QAction(self.tr("Quit"), self) self.quitAction.setShortcut("Alt+F4") self.quitAction.setIcon(QIcon(pixmap("window-close-3.png"))) self.quitAction.triggered.connect(self.close) # Save Config as self.saveasAction = QAction(self.tr("Save as..."), self) self.saveasAction.setShortcut("Ctrl+Shift+S") self.saveasAction.setIcon(QIcon(pixmap("document-save-as-5.png"))) self.saveasAction.triggered.connect(self.show_savecfg_dlg) # Save file self.saveAction = QAction(self.tr("Save"), self) self.saveAction.setShortcut("Ctrl+S") self.saveAction.setIcon(QIcon(pixmap("document-save-5.png"))) self.saveAction.triggered.connect(self.save_file) # Load file self.loadAction = QAction(self.tr("Open..."), self) self.loadAction.setShortcut("Ctrl+O") self.loadAction.setIcon(QIcon(pixmap("document-open-7.png"))) self.loadAction.triggered.connect(self.show_opencfg_dlg) # New self.newAction = QAction(self.tr("New"), self) self.newAction.setShortcut("Ctrl+N") self.newAction.setIcon(QIcon(pixmap("document-new-6.png"))) self.newAction.triggered.connect(self.new) # start recording self.startrecordingAction = QAction(self.tr("Start recording"), self) self.startrecordingAction.setShortcut("F9") self.startrecordingAction.setIcon(QIcon(pixmap("media-record-6.png"))) self.startrecordingAction.triggered.connect(self.start_recording) # stop recording self.stoprecordingAction = QAction(self.tr("Stop recording"), self) self.stoprecordingAction.setShortcut("F10") self.stoprecordingAction.setIcon(QIcon(pixmap("media-playback-stop-8.png"))) self.stoprecordingAction.setEnabled(False) self.stoprecordingAction.triggered.connect(self.stop_recording) # clear record self.clearrecordAction = QAction(self.tr("Clear"), self) self.clearrecordAction.setIcon(QIcon(pixmap("editclear.png"))) self.clearrecordAction.triggered.connect(self.clear_record) # export record self.exportcsvAction = QAction(self.tr("Export to csv..."), self) self.exportcsvAction.setIcon(QIcon(pixmap("text_csv.png"))) self.exportcsvAction.triggered.connect(self.export_csv) # show record settings self.recordsettingsAction = QAction(self.tr("Settings..."), self) self.recordsettingsAction.setIcon(QIcon(pixmap("configure.png"))) self.recordsettingsAction.triggered.connect(self.show_recordsettings) # Info self.infoAction = QAction(self.tr("Info"), self) self.infoAction.setShortcut("F1") self.infoAction.triggered.connect(self.show_info) def _init_menus(self): # file menu self.fileMenu = self.menuBar().addMenu(self.tr("File")) self.fileMenu.addAction(self.newAction) self.fileMenu.addAction(self.loadAction) self.fileMenu.addAction(self.saveAction) self.fileMenu.addAction(self.saveasAction) self.fileMenu.addSeparator() self.fileMenu.addAction(self.connectAction) self.fileMenu.addAction(self.serialdlgAction) self.fileMenu.addSeparator() self.fileMenu.addAction(self.quitAction) # view menu self.viewMenu = self.menuBar().addMenu(self.tr("View")) self.viewMenu.addAction( self.objectexplorerDockWidget.toggleViewAction()) self.viewMenu.addAction(self.plotsettingsDockWidget.toggleViewAction()) self.viewMenu.addAction(self.loggingDockWidget.toggleViewAction()) self.viewMenu.addAction(self.recordDockWidget.toggleViewAction()) # record menu self.recordMenu = self.menuBar().addMenu(self.tr("Record")) self.recordMenu.addAction(self.startrecordingAction) self.recordMenu.addAction(self.stoprecordingAction) self.recordMenu.addAction(self.exportcsvAction) self.recordMenu.addSeparator() self.recordMenu.addAction(self.clearrecordAction) self.recordMenu.addSeparator() self.recordMenu.addAction(self.recordsettingsAction) # info menu self.menuBar().addAction(self.infoAction) def show_info(self): QMessageBox.about( self, QApplication.applicationName(), "%s %s\n" "Copyright (c) by %s" % ( QCoreApplication.applicationName(), QCoreApplication.applicationVersion(), QCoreApplication.organizationName(), ) ) def load_file(self, filename): old_filename = self.filename if self.filename != filename else None self.filename = filename try: with open(filename, 'rb') as f: try: self.objectexplorer.model().beginResetModel() self.rootnode.load(bytearray_to_utf8(f.read())) self.objectexplorer.model().endResetModel() except ValueError as e: critical(self, "File '%s' is not a valid config file." % filename) logger.error(str(e)) if old_filename is not None: self.load_file(old_filename) else: self.filename = None except FileNotFoundError as e: logger.error(str(e)) self.filename = None self.objectexplorer.refresh() def load_settings(self): settings = QSettings() # window geometry try: self.restoreGeometry(settings.value(GEOMETRY_SETTING)) except: logger.debug("error restoring window geometry") # window state try: self.restoreState(settings.value(WINDOWSTATE_SETTING)) except: logger.debug("error restoring window state") # filename self.filename = settings.value(FILENAME_SETTING) if self.filename is not None: self.load_file(self.filename) def save_settings(self): settings = QSettings() settings.setValue(WINDOWSTATE_SETTING, self.saveState()) settings.setValue(GEOMETRY_SETTING, self.saveGeometry()) settings.setValue(FILENAME_SETTING, self.filename) def closeEvent(self, event): if self.dirty: res = QMessageBox.question( self, QCoreApplication.applicationName(), self.tr("Save changes to file '%s'?" % self.filename if self.filename is not None else "unknown"), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel ) if res == QMessageBox.Cancel: event.ignore() return elif res == QMessageBox.Yes: self.save_file() self.save_settings() try: self.worker.quit() except AttributeError: pass try: self.serial.close() except (SerialException, AttributeError): pass def new(self): self.objectexplorer.model().beginResetModel() self.rootnode.clear() self.objectexplorer.model().endResetModel() def send_reset(self): jsonstring = json.dumps({"resetpid": 1}) self.serial.write(bytearray(jsonstring, 'utf-8')) def receive_serialdata(self, time, data): self.loggingWidget.log_input(data) try: self.rootnode.from_json(data) except ValueError as e: logger.error(str(e)) # refresh widgets self.objectexplorer.refresh() self.plot.refresh(time) if self.recording_enabled: self.recordWidget.add_data(time, self.rootnode) def send_serialdata(self, node): if isinstance(node, JsonItem): if self.serial.isOpen(): s = node.to_json() self.serial.write(utf8_to_bytearray(s + '\n')) self.loggingWidget.log_output(s.strip()) def show_serialdlg(self): dlg = SerialDialog(self.settings, self) dlg.exec_() def toggle_connect(self): if self.serial.isOpen(): self.disconnect() else: self.connect() def connect(self): # Load port setting port = self.settings.get(PORT_SETTING) baudrate = self.settings.get(BAUDRATE_SETTING) # If no port has been selected before show serial settings dialog if port is None: if self.show_serialdlg() == QDialog.Rejected: return port = self.settings.get(PORT_SETTING) baudrate = self.settings.get(BAUDRATE_SETTING) # Serial connection try: self.serial.port = port self.serial.baudrate = baudrate self.serial.open() except ValueError: QMessageBox.critical( self, QCoreApplication.applicationName(), self.tr("Serial parameters e.g. baudrate, databits are out " "of range.") ) except SerialException: QMessageBox.critical( self, QCoreApplication.applicationName(), self.tr("The device '%s' can not be found or can not be " "configured." % port) ) else: self.worker = SerialWorker(self.serial, self) self.worker.data_received.connect(self.receive_serialdata) self.worker.start() self.connectAction.setText(self.tr("Disconnect")) self.connectAction.setIcon(QIcon(pixmap("network-disconnect-3.png"))) self.serialdlgAction.setEnabled(False) self.connectionstateLabel.setText( self.tr("Connected to %s") % port) self._connected = True self.objectexplorer.refresh() def disconnect(self): self.worker.quit() self.serial.close() self.connectAction.setText(self.tr("Connect")) self.connectAction.setIcon(QIcon(pixmap("network-connect-3.png"))) self.serialdlgAction.setEnabled(True) self.connectionstateLabel.setText(self.tr("Not connected")) self._connected = False self.objectexplorer.refresh() def show_savecfg_dlg(self): filename, _ = QFileDialog.getSaveFileName( self, self.tr("Save configuration file..."), directory=os.path.expanduser("~"), filter="Json file (*.json)" ) if filename: self.filename = filename self.save_file() def save_file(self): if self.filename is not None: config_string = self.rootnode.dump() with open(self.filename, 'w') as f: f.write(config_string) self.dirty = False else: self.show_savecfg_dlg() def show_opencfg_dlg(self): # show file dialog filename, _ = QFileDialog.getOpenFileName( self, self.tr("Open configuration file..."), directory=os.path.expanduser("~"), filter=self.tr("Json file (*.json);;All files (*.*)") ) # load config file if filename: self.load_file(filename) def refresh_window_title(self): s = "%s %s" % (QCoreApplication.applicationName(), QCoreApplication.applicationVersion()) if self.filename is not None: s += " - " + self.filename if self.dirty: s += "*" self.setWindowTitle(s) def start_recording(self): self.recording_enabled = True self.startrecordingAction.setEnabled(False) self.stoprecordingAction.setEnabled(True) def stop_recording(self): self.recording_enabled = False self.startrecordingAction.setEnabled(True) self.stoprecordingAction.setEnabled(False) def export_csv(self): filename, _ = QFileDialog.getSaveFileName( self, QCoreApplication.applicationName(), filter="CSV files(*.csv);;All files (*.*)" ) if filename == "": return # get current dataframe and export to csv df = self.recordWidget.dataframe decimal = self.settings.get(DECIMAL_SETTING) df = df.applymap(lambda x: str(x).replace(".", decimal)) df.to_csv( filename, index_label="time", sep=self.settings.get(SEPARATOR_SETTING) ) def clear_record(self): self.recordWidget.clear() def show_recordsettings(self): dlg = CSVSettingsDialog(self) dlg.exec_() # filename property @property def filename(self): return self._filename @filename.setter def filename(self, value=""): self._filename = value self.refresh_window_title() # dirty property @property def dirty(self): return self._dirty @dirty.setter def dirty(self, value): self._dirty = value self.refresh_window_title() def set_dirty(self): self.dirty = True # connected property @property def connected(self): return self._connected