def initialize_content(self): # Validators self._summary.detector_offset_edit.setValidator(QDoubleValidator(self._summary.detector_offset_edit)) self._summary.sample_dist_edit.setValidator(QDoubleValidator(self._summary.sample_dist_edit)) self._summary.n_q_bins_edit.setValidator(QIntValidator(self._summary.n_q_bins_edit)) # Event connections self._summary.detector_offset_chk.clicked.connect(self._det_offset_clicked) self._summary.sample_dist_chk.clicked.connect(self._sample_dist_clicked) self._summary.help_button.clicked.connect(self._show_help) self._summary.dark_current_check.clicked.connect(self._dark_clicked) self._summary.dark_browse_button.clicked.connect(self._dark_browse) self._summary.dark_plot_button.clicked.connect(self._dark_plot_clicked) # Output directory g2 = QButtonGroup(self) g2.addButton(self._summary.select_output_dir_radio) g2.addButton(self._summary.use_data_dir_radio) g2.setExclusive(True) self._summary.select_output_dir_radio.clicked.connect(self._output_dir_clicked) self._summary.use_data_dir_radio.clicked.connect(self._output_dir_clicked) self._summary.output_dir_browse_button.clicked.connect(self._output_dir_browse) self._output_dir_clicked() # Lin/log option g3 = QButtonGroup(self) g3.addButton(self._summary.log_binning_radio) g3.addButton(self._summary.lin_binning_radio) g3.setExclusive(True) # Q range self._summary.n_q_bins_edit.setText("100") self._summary.scale_edit.setText("1") self._summary.instr_name_label.hide() self._dark_clicked(self._summary.dark_current_check.isChecked()) # Mask Connections self._summary.mask_browse_button.clicked.connect(self._mask_browse_clicked) self._summary.mask_plot_button.clicked.connect(self._mask_plot_clicked) self._summary.mask_check.clicked.connect(self._mask_checked) # Absolute scale connections and validators self._summary.scale_edit.setValidator(QDoubleValidator(self._summary.scale_edit)) self._summary.scale_beam_radius_edit.setValidator(QDoubleValidator(self._summary.scale_beam_radius_edit)) self._summary.scale_att_trans_edit.setValidator(QDoubleValidator(self._summary.scale_att_trans_edit)) self._summary.scale_data_browse_button.clicked.connect(self._scale_data_browse) self._summary.scale_data_plot_button.clicked.connect(self._scale_data_plot_clicked) self._summary.beamstop_chk.clicked.connect(self._beamstop_clicked) self._summary.scale_chk.clicked.connect(self._scale_clicked) self._scale_clicked(self._summary.scale_chk.isChecked()) # TOF cut validator self._summary.low_tof_edit.setValidator(QDoubleValidator(self._summary.low_tof_edit)) self._summary.high_tof_edit.setValidator(QDoubleValidator(self._summary.high_tof_edit)) # TOF connections self._summary.tof_cut_chk.clicked.connect(self._tof_clicked) # Monitor normalization self._summary.beam_monitor_chk.clicked.connect(self._beam_monitor_clicked) self._summary.beam_monitor_browse_button.clicked.connect(self._beam_monitor_reference_browse) # Resolution validator self._summary.sample_apert_edit.setValidator(QDoubleValidator(self._summary.sample_apert_edit)) self._summary.resolution_chk.clicked.connect(self._resolution_clicked) # Since EQSANS does not currently use the absolute scale calculation, expose it in debug mode only for now if not self._settings.debug: self._summary.config_options_layout.deleteLater() self._summary.abs_scale_options_layout.deleteLater() self._summary.abs_scale_direct_beam_layout.deleteLater() self._summary.monitor_layout.deleteLater() self._summary.direct_beam_label.hide() self._summary.att_trans_label.hide() self._summary.beamstop_chk.hide() self._summary.scale_data_edit.hide() self._summary.scale_data_plot_button.hide() self._summary.scale_data_browse_button.hide() self._summary.scale_att_trans_edit.hide() self._summary.scale_beam_radius_edit.hide() self._summary.scale_chk.hide() self._summary.beam_monitor_chk.hide() self._summary.tof_correction_chk.hide() self._summary.beam_monitor_edit.hide() self._summary.beam_monitor_browse_button.hide() # Same thing for sample-detector distance and offset: not yet hooked in self._summary.geometry_options_groupbox.hide() # Hide expert options #self._summary.config_mask_chk.hide() self._summary.tof_cut_chk.hide() self._summary.low_tof_edit.hide() self._summary.high_tof_edit.hide() self._summary.low_tof_label.hide() self._summary.high_tof_label.hide() if not self._settings.advanced: self._summary.att_scale_factor_label.hide() self._summary.scale_edit.hide() self._summary.mask_groupbox.hide() self._summary.solid_angle_chk.hide() self._summary.resolution_chk.hide() self._summary.sample_apert_edit.hide() self._summary.sample_apert_label.hide() # We need the EQSANS data proxy for a quick load of a file for masking purposes, but # we don't want to show the plot button. Turn this off for the moment. if True or not self._has_instrument_view: self._summary.dark_plot_button.hide() self._summary.scale_data_plot_button.hide()
def initialize_content(self): """ Declare the validators and event connections for the widgets loaded through the .ui file. """ # Sample data # Validators self._content.transmission_edit.setValidator( QDoubleValidator(self._content.transmission_edit)) self._content.dtransmission_edit.setValidator( QDoubleValidator(self._content.dtransmission_edit)) self._content.beam_radius_edit.setValidator( QDoubleValidator(self._content.beam_radius_edit)) self._content.sample_thickness_edit.setValidator( QDoubleValidator(self._content.sample_thickness_edit)) # Connections self._content.data_file_browse_button.clicked.connect( self._data_file_browse) self._content.calculate_radio.clicked.connect(self._calculate_clicked) self._content.fix_trans_radio.clicked.connect(self._calculate_clicked) self._content.empty_button.clicked.connect(self._empty_browse) self._content.sample_button.clicked.connect(self._sample_browse) self._content.data_file_plot_button.clicked.connect( self._data_file_plot) self._content.empty_plot_button.clicked.connect(self._empty_plot) self._content.sample_plot_button.clicked.connect(self._sample_plot) # Calculate/Fix radio button g1 = QButtonGroup(self) g1.addButton(self._content.calculate_radio) g1.addButton(self._content.fix_trans_radio) g1.setExclusive(True) if not self._settings.debug: self._content.fix_transmission_layout.deleteLater() self._content.calculate_radio.hide() self._content.fix_trans_radio.hide() self._content.plus_minus_label.hide() self._content.transmission_edit.hide() self._content.dtransmission_edit.hide() if not self._has_instrument_view: self._content.data_file_plot_button.hide() self._content.empty_plot_button.hide() self._content.sample_plot_button.hide() # Background ########## # Validators self._content.bck_transmission_edit.setValidator( QDoubleValidator(self._content.bck_transmission_edit)) self._content.bck_dtransmission_edit.setValidator( QDoubleValidator(self._content.bck_dtransmission_edit)) self._content.bck_beam_radius_edit.setValidator( QDoubleValidator(self._content.beam_radius_edit)) #self._content.bck_thickness_edit.setValidator(QDoubleValidator(self._content.bck_thickness_edit)) # Connections self._content.background_chk.clicked.connect(self._background_clicked) self._content.background_browse.clicked.connect( self._background_browse) self._content.bck_calculate_radio.clicked.connect( self._bck_calculate_clicked) self._content.bck_fix_trans_radio.clicked.connect( self._bck_calculate_clicked) self._content.bck_empty_button.clicked.connect(self._bck_empty_browse) self._content.bck_sample_button.clicked.connect( self._bck_sample_browse) self._content.background_plot_button.clicked.connect( self._background_plot_clicked) self._content.bck_empty_plot_button.clicked.connect( self._bck_empty_plot) self._content.bck_sample_plot_button.clicked.connect( self._bck_sample_plot) # Calculate/Fix radio button g2 = QButtonGroup(self) g2.addButton(self._content.bck_calculate_radio) g2.addButton(self._content.bck_fix_trans_radio) g2.setExclusive(True) if not self._settings.debug: self._content.bck_fix_transmission_layout.deleteLater() self._content.bck_calculate_radio.hide() self._content.bck_fix_trans_radio.hide() self._content.bck_plus_minus_label.hide() self._content.bck_transmission_edit.hide() self._content.bck_dtransmission_edit.hide() if not self._settings.advanced: self._content.theta_dep_chk.hide() self._content.bck_theta_dep_chk.hide() self._content.sample_thickness_label.hide() self._content.sample_thickness_edit.hide() #self._content.bck_thickness_label.hide() #self._content.bck_thickness_edit.hide() if not self._has_instrument_view: self._content.background_plot_button.hide() self._content.bck_empty_plot_button.hide() self._content.bck_sample_plot_button.hide()
class PyDMEnumButton(QWidget, PyDMWritableWidget, WidgetType): """ A QWidget that renders buttons for every option of Enum Items. For now three types of buttons can be rendered: - Push Button - Radio Button Parameters ---------- parent : QWidget The parent widget for the Label init_channel : str, optional The channel to be used by the widget. Signals ------- send_value_signal : int, float, str, bool or np.ndarray Emitted when the user changes the value. """ Q_ENUMS(WidgetType) WidgetType = WidgetType def __init__(self, parent=None, init_channel=None): QWidget.__init__(self, parent) PyDMWritableWidget.__init__(self, init_channel=init_channel) self._has_enums = False self._checkable = True self.setLayout(QGridLayout(self)) self._btn_group = QButtonGroup() self._btn_group.setExclusive(True) self._btn_group.buttonClicked[int].connect(self.handle_button_clicked) self._widget_type = WidgetType.PushButton self._orientation = Qt.Vertical self._widgets = [] self.rebuild_widgets() def minimumSizeHint(self): """ This property holds the recommended minimum size for the widget. Returns ------- QSize """ # This is totally arbitrary, I just want *some* visible nonzero size return QSize(50, 100) @Property("QStringList") def items(self): """ Items to be displayed in the button group. This property can be overridden by the items coming from the control system. Because C++ QStringList expects a list type, we need to make sure that None is never returned. Returns ------- List[str] """ return self.enum_strings or [] @items.setter def items(self, value): self.enum_strings_changed(value) @Property(WidgetType) def widgetType(self): """ The widget type to be used when composing the group. Returns ------- WidgetType """ return self._widget_type @widgetType.setter def widgetType(self, new_type): """ The widget type to be used when composing the group. Parameters ---------- new_type : WidgetType """ if new_type != self._widget_type: self._widget_type = new_type self.rebuild_widgets() @Property(Qt.Orientation) def orientation(self): """ Whether to lay out the bit indicators vertically or horizontally. Returns ------- int """ return self._orientation @orientation.setter def orientation(self, new_orientation): """ Whether to lay out the bit indicators vertically or horizontally. Parameters ------- new_orientation : Qt.Orientation, int """ if new_orientation != self._orientation: self._orientation = new_orientation self.rebuild_layout() @Property(bool) def checkable(self): """ Whether or not the button should be checkable. Returns ------- bool """ return self._checkable @checkable.setter def checkable(self, value): if value != self._checkable: self._checkable = value for widget in self._widgets: widget.setCheckable(value) @Slot(int) def handle_button_clicked(self, id): """ Handles the event of a button being clicked. Parameters ---------- id : int The clicked button id. """ self.send_value_signal.emit(id) def clear(self): """ Remove all inner widgets from the layout """ for col in range(0, self.layout().columnCount()): for row in range(0, self.layout().rowCount()): item = self.layout().itemAtPosition(row, col) if item is not None: w = item.widget() if w is not None: self.layout().removeWidget(w) def rebuild_widgets(self): """ Rebuild the list of widgets based on a new enum or generates a default list of fake strings so we can see something at Designer. """ def generate_widgets(items): while len(self._widgets) != 0: w = self._widgets.pop(0) self._btn_group.removeButton(w) w.deleteLater() for idx, entry in enumerate(items): w = class_for_type[self._widget_type](parent=self) w.setCheckable(self.checkable) w.setText(entry) self._widgets.append(w) self._btn_group.addButton(w, idx) self.clear() if self._has_enums: generate_widgets(self.enum_strings) else: generate_widgets(["Item 1", "Item 2", "Item ..."]) self.rebuild_layout() def rebuild_layout(self): """ Method to reorganize the top-level widget and its contents according to the layout property values. """ self.clear() if self.orientation == Qt.Vertical: for i, widget in enumerate(self._widgets): self.layout().addWidget(widget, i, 0) elif self.orientation == Qt.Horizontal: for i, widget in enumerate(self._widgets): self.layout().addWidget(widget, 0, i) def check_enable_state(self): """ Checks whether or not the widget should be disable. This method also disables the widget and add a Tool Tip with the reason why it is disabled. """ status = self._write_access and self._connected and self._has_enums tooltip = "" if not self._connected: tooltip += "Channel is disconnected." elif not self._write_access: if data_plugins.is_read_only(): tooltip += "Running PyDM on Read-Only mode." else: tooltip += "Access denied by Channel Access Security." elif not self._has_enums: tooltip += "Enums not available." self.setToolTip(tooltip) self.setEnabled(status) def value_changed(self, new_val): """ Callback invoked when the Channel value is changed. Parameters ---------- new_val : int The new value from the channel. """ if new_val is not None and new_val != self.value: super(PyDMEnumButton, self).value_changed(new_val) btn = self._btn_group.button(new_val) if btn: btn.setChecked(True) def enum_strings_changed(self, new_enum_strings): """ Callback invoked when the Channel has new enum values. This callback also triggers a value_changed call so the new enum values to be broadcasted. Parameters ---------- new_enum_strings : tuple The new list of values """ if new_enum_strings is not None \ and new_enum_strings != self.enum_strings: super(PyDMEnumButton, self).enum_strings_changed(new_enum_strings) self._has_enums = True self.check_enable_state() self.rebuild_widgets() def paintEvent(self, _): """ Paint events are sent to widgets that need to update themselves, for instance when part of a widget is exposed because a covering widget was moved. At PyDMDrawing this method handles the alarm painting with parameters from the stylesheet, configures the brush, pen and calls ```draw_item``` so the specifics can be performed for each of the drawing classes. Parameters ---------- event : QPaintEvent """ painter = QPainter(self) opt = QStyleOption() opt.initFrom(self) self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) painter.setRenderHint(QPainter.Antialiasing)
def initialize_content(self): """ Declare the validators and event connections for the widgets loaded through the .ui file. """ # Sample data # Validators self._content.transmission_edit.setValidator(QDoubleValidator(self._content.transmission_edit)) self._content.dtransmission_edit.setValidator(QDoubleValidator(self._content.dtransmission_edit)) self._content.beam_radius_edit.setValidator(QDoubleValidator(self._content.beam_radius_edit)) self._content.sample_thickness_edit.setValidator(QDoubleValidator(self._content.sample_thickness_edit)) # Connections self._content.data_file_browse_button.clicked.connect(self._data_file_browse) self._content.calculate_radio.clicked.connect(self._calculate_clicked) self._content.fix_trans_radio.clicked.connect(self._calculate_clicked) self._content.empty_button.clicked.connect(self._empty_browse) self._content.sample_button.clicked.connect(self._sample_browse) self._content.data_file_plot_button.clicked.connect(self._data_file_plot) self._content.empty_plot_button.clicked.connect(self._empty_plot) self._content.sample_plot_button.clicked.connect(self._sample_plot) # Calculate/Fix radio button g1 = QButtonGroup(self) g1.addButton(self._content.calculate_radio) g1.addButton(self._content.fix_trans_radio) g1.setExclusive(True) if not self._settings.debug: self._content.fix_transmission_layout.deleteLater() self._content.calculate_radio.hide() self._content.fix_trans_radio.hide() self._content.plus_minus_label.hide() self._content.transmission_edit.hide() self._content.dtransmission_edit.hide() if not self._has_instrument_view: self._content.data_file_plot_button.hide() self._content.empty_plot_button.hide() self._content.sample_plot_button.hide() # Background ########## # Validators self._content.bck_transmission_edit.setValidator(QDoubleValidator(self._content.bck_transmission_edit)) self._content.bck_dtransmission_edit.setValidator(QDoubleValidator(self._content.bck_dtransmission_edit)) self._content.bck_beam_radius_edit.setValidator(QDoubleValidator(self._content.beam_radius_edit)) #self._content.bck_thickness_edit.setValidator(QDoubleValidator(self._content.bck_thickness_edit)) # Connections self._content.background_chk.clicked.connect(self._background_clicked) self._content.background_browse.clicked.connect(self._background_browse) self._content.bck_calculate_radio.clicked.connect(self._bck_calculate_clicked) self._content.bck_fix_trans_radio.clicked.connect(self._bck_calculate_clicked) self._content.bck_empty_button.clicked.connect(self._bck_empty_browse) self._content.bck_sample_button.clicked.connect(self._bck_sample_browse) self._content.background_plot_button.clicked.connect(self._background_plot_clicked) self._content.bck_empty_plot_button.clicked.connect(self._bck_empty_plot) self._content.bck_sample_plot_button.clicked.connect(self._bck_sample_plot) # Calculate/Fix radio button g2 = QButtonGroup(self) g2.addButton(self._content.bck_calculate_radio) g2.addButton(self._content.bck_fix_trans_radio) g2.setExclusive(True) if not self._settings.debug: self._content.bck_fix_transmission_layout.deleteLater() self._content.bck_calculate_radio.hide() self._content.bck_fix_trans_radio.hide() self._content.bck_plus_minus_label.hide() self._content.bck_transmission_edit.hide() self._content.bck_dtransmission_edit.hide() if not self._settings.advanced: self._content.theta_dep_chk.hide() self._content.bck_theta_dep_chk.hide() self._content.sample_thickness_label.hide() self._content.sample_thickness_edit.hide() #self._content.bck_thickness_label.hide() #self._content.bck_thickness_edit.hide() if not self._has_instrument_view: self._content.background_plot_button.hide() self._content.bck_empty_plot_button.hide() self._content.bck_sample_plot_button.hide()
class BaseWindow(SiriusMainWindow): """Base class.""" def __init__(self, parent=None, prefix=_VACA_PREFIX): """Init.""" super().__init__(parent) self.prefix = prefix self._curr_dir = _os.path.abspath(_os.path.dirname(__file__)) def _setupUi(self): # menubar self.menubar = QMenuBar(self) self.menubar.setNativeMenuBar(False) self.setMenuBar(self.menubar) self.menu = self.menubar.addMenu("Open...") self._setupMenu() # auxiliar diagnostics widget self.auxdig_wid = None self._setupDiagWidget() # lattice widget self.lattice_wid = QSvgWidget( _os.path.join(self._curr_dir, self.SVG_FILE)) # screens view widget (create only one ScrnView) self._scrns_wids_dict = dict() self._currScrn = 0 scrn_wid = SiriusScrnView(parent=self, prefix=self.prefix, device=self._scrns[self._currScrn]) scrn_wid.setVisible(True) self._scrns_wids_dict[self._currScrn] = scrn_wid self.scrns_wid = QWidget() lay_scrns = QGridLayout(self.scrns_wid) lay_scrns.addWidget(scrn_wid) # correction widget self.corr_wid = QGroupBox('Screens and Correctors Panel') self._scrns_sel_bg = QButtonGroup(parent=self.corr_wid) self._scrns_sel_bg.setExclusive(True) self._setupScrnsCorrsWidget() vlay1 = QVBoxLayout() if self.auxdig_wid: vlay1.addWidget(self.auxdig_wid) vlay1.addWidget(self.scrns_wid) vlay2 = QVBoxLayout() vlay2.addWidget(self.lattice_wid) vlay2.addWidget(self.corr_wid) cw = QWidget() lay = QHBoxLayout(cw) lay.addLayout(vlay1) lay.addLayout(vlay2) self.setCentralWidget(cw) def _setupMenu(self): raise NotImplementedError def _setupScrnsCorrsWidget(self): raise NotImplementedError def _setupDiagWidget(self): raise NotImplementedError @Slot() def _setScrnWidget(self): scrn_obj = self._scrns_wids_dict[self._currScrn] scrn_obj.setVisible(False) sender = self.sender() self._currScrn = self._scrns_sel_bg.id(sender) if self._currScrn not in self._scrns_wids_dict.keys(): scrn_obj = SiriusScrnView(parent=self, prefix=self.prefix, device=self._scrns[self._currScrn]) self.scrns_wid.layout().addWidget(scrn_obj, 2, 0) self._scrns_wids_dict[self._currScrn] = scrn_obj else: scrn_obj = self._scrns_wids_dict[self._currScrn] self._scrns_wids_dict[self._currScrn].setVisible(True) def _create_headerline(self, labels): """Create and return a headerline.""" hl = QWidget() hl.setLayout(QHBoxLayout()) hl.layout().setContentsMargins(0, 9, 0, 0) glay = None for text, width in labels: if not width: if glay: hl.layout().addLayout(glay) hl.layout().addStretch() glay = QGridLayout() glay.setAlignment(Qt.AlignCenter) glay.setContentsMargins(0, 0, 0, 0) c = 0 else: label = QLabel(text, self) label.setStyleSheet(""" min-width:valueem; min-height:1.29em; max-height:1.29em; font-weight:bold; qproperty-alignment: AlignCenter; """.replace('value', str(width))) glay.addWidget(label, 0, c) c += 1 return hl def _create_scrn_summwidget(self, scrn_device, scrn_idx): """Create and return a screen detail widget.""" cb_scrn = QCheckBox(scrn_device.get_nickname(dev=True), self) self._scrns_sel_bg.addButton(cb_scrn) self._scrns_sel_bg.setId(cb_scrn, scrn_idx) if scrn_idx == self._currScrn: cb_scrn.setChecked(True) cb_scrn.clicked.connect(self._setScrnWidget) cb_scrn.setStyleSheet(""" min-width:6.5em; max-width:6.5em; font-weight:bold;""") led_camenbl = SiriusLedState( self, scrn_device.substitute(prefix=self.prefix, propty='CamEnbl-Sts')) led_camenbl.setStyleSheet("min-width:3.2em; max-width:3.2em;") cb_scrntype = PyDMEnumComboBox( self, scrn_device.substitute(prefix=self.prefix, propty='ScrnType-Sel')) cb_scrntype.setSizePolicy(QSzPlcy.Minimum, QSzPlcy.Fixed) cb_scrntype.setStyleSheet("min-width:4.5em;max-width:4.5em;") lb_scrntype = PyDMLabel( self, scrn_device.substitute(prefix=self.prefix, propty='ScrnType-Sts')) lb_scrntype.setStyleSheet("min-width:4.5em; max-width:4.5em;") lb_scrntype.setAlignment(Qt.AlignCenter) led_scrntype = PyDMLed(self, scrn_device.substitute(prefix=self.prefix, propty='ScrnType-Sts'), color_list=[ PyDMLed.LightGreen, PyDMLed.Red, PyDMLed.Red, PyDMLed.Yellow ]) led_scrntype.shape = 2 led_scrntype.setStyleSheet("""min-width:4.5em; max-width:4.5em;""") wid = QWidget() lay = QGridLayout(wid) lay.setAlignment(Qt.AlignCenter) lay.addWidget(cb_scrn, 1, 1) lay.addWidget(led_camenbl, 1, 2) lay.addWidget(cb_scrntype, 1, 3) lay.addWidget(lb_scrntype, 1, 4) lay.addWidget(led_scrntype, 2, 4) return wid def _create_corr_summwidget(self, corr): """Create and return a corrector detail widget.""" wid = QWidget() wid.setSizePolicy(QSzPlcy.Preferred, QSzPlcy.Maximum) lay = QGridLayout(wid) lay.setContentsMargins(0, 0, 0, 0) lay.setAlignment(Qt.AlignCenter) propty_sp = 'Current-SP' if corr.sec == 'LI' else 'Kick-SP' propty_mon = propty_sp.replace('SP', 'Mon') led = SiriusLedState( self, corr.substitute(prefix=self.prefix, propty='PwrState-Sts')) led.setStyleSheet("max-width:1.29em;") lay.addWidget(led, 1, 1) nickname = corr.get_nickname(sec=corr.sec == 'LI', dev=True) pb = QPushButton(nickname, self) if corr.dis == 'PU': util.connect_window(pb, PUDetailWindow, parent=self, devname=corr) else: util.connect_window(pb, PSDetailWindow, parent=self, psname=corr) pb.setStyleSheet(""" min-width:6em; max-width:6em; min-height:1.29em;""") lay.addWidget(pb, 1, 2) sp_kick = PyDMSpinboxScrollbar( self, corr.substitute(prefix=self.prefix, propty=propty_sp)) sp_kick.setStyleSheet("QDoubleSpinBox{min-width:4em; max-width:4em; }" "QScrollBar{max-width:4em;}") sp_kick.spinbox.precisionFromPV = False sp_kick.spinbox.precision = 1 sp_kick.scrollbar.limitsFromPV = True lay.addWidget(sp_kick, 1, 3, 2, 1) lb_kick = PyDMLabel( self, corr.substitute(prefix=self.prefix, propty=propty_mon)) lb_kick.setStyleSheet(""" min-width:5em; max-width:5em; min-height:1.29em;""") lb_kick.showUnits = True lb_kick.precisionFromPV = False lb_kick.precision = 1 lb_kick.setAlignment(Qt.AlignCenter) lay.addWidget(lb_kick, 1, 4) return wid
class PyDMEnumButton(QWidget, PyDMWritableWidget, WidgetType): """ A QWidget that renders buttons for every option of Enum Items. For now three types of buttons can be rendered: - Push Button - Radio Button Parameters ---------- parent : QWidget The parent widget for the Label init_channel : str, optional The channel to be used by the widget. Signals ------- send_value_signal : int, float, str, bool or np.ndarray Emitted when the user changes the value. """ Q_ENUMS(WidgetType) WidgetType = WidgetType def __init__(self, parent=None, init_channel=None): QWidget.__init__(self, parent) PyDMWritableWidget.__init__(self, init_channel=init_channel) self._has_enums = False self.setLayout(QGridLayout(self)) self._btn_group = QButtonGroup() self._btn_group.setExclusive(True) self._btn_group.buttonClicked[int].connect(self.handle_button_clicked) self._widget_type = WidgetType.PushButton self._orientation = Qt.Vertical self._widgets = [] self.rebuild_widgets() def minimumSizeHint(self): """ This property holds the recommended minimum size for the widget. Returns ------- QSize """ # This is totally arbitrary, I just want *some* visible nonzero size return QSize(50, 100) @Property(WidgetType) def widgetType(self): """ The widget type to be used when composing the group. Returns ------- WidgetType """ return self._widget_type @widgetType.setter def widgetType(self, new_type): """ The widget type to be used when composing the group. Parameters ---------- new_type : WidgetType """ if new_type != self._widget_type: self._widget_type = new_type self.rebuild_widgets() @Property(Qt.Orientation) def orientation(self): """ Whether to lay out the bit indicators vertically or horizontally. Returns ------- int """ return self._orientation @orientation.setter def orientation(self, new_orientation): """ Whether to lay out the bit indicators vertically or horizontally. Parameters ------- new_orientation : Qt.Orientation, int """ if new_orientation != self._orientation: self._orientation = new_orientation self.rebuild_layout() @Slot(int) def handle_button_clicked(self, id): """ Handles the event of a button being clicked. Parameters ---------- id : int The clicked button id. """ self.send_value_signal.emit(id) def clear(self): """ Remove all inner widgets from the layout """ for col in range(0, self.layout().columnCount()): for row in range(0, self.layout().rowCount()): item = self.layout().itemAtPosition(row, col) if item is not None: w = item.widget() if w is not None: self.layout().removeWidget(w) def rebuild_widgets(self): """ Rebuild the list of widgets based on a new enum or generates a default list of fake strings so we can see something at Designer. """ def generate_widgets(items): while len(self._widgets) != 0: w = self._widgets.pop(0) self._btn_group.removeButton(w) w.deleteLater() for idx, entry in enumerate(items): w = class_for_type[self._widget_type](parent=self) w.setCheckable(True) w.setText(entry) self._widgets.append(w) self._btn_group.addButton(w, idx) self.clear() if self._has_enums: generate_widgets(self.enum_strings) else: generate_widgets(["Item 1", "Item 2", "Item ..."]) self.rebuild_layout() def rebuild_layout(self): """ Method to reorganize the top-level widget and its contents according to the layout property values. """ self.clear() if self.orientation == Qt.Vertical: for i, widget in enumerate(self._widgets): self.layout().addWidget(widget, i, 0) elif self.orientation == Qt.Horizontal: for i, widget in enumerate(self._widgets): self.layout().addWidget(widget, 0, i) def check_enable_state(self): """ Checks whether or not the widget should be disable. This method also disables the widget and add a Tool Tip with the reason why it is disabled. """ status = self._write_access and self._connected and self._has_enums tooltip = "" if not self._connected: tooltip += "Channel is disconnected." elif not self._write_access: if data_plugins.is_read_only(): tooltip += "Running PyDM on Read-Only mode." else: tooltip += "Access denied by Channel Access Security." elif not self._has_enums: tooltip += "Enums not available." self.setToolTip(tooltip) self.setEnabled(status) def value_changed(self, new_val): """ Callback invoked when the Channel value is changed. Parameters ---------- new_val : int The new value from the channel. """ if new_val is not None and new_val != self.value: super(PyDMEnumButton, self).value_changed(new_val) btn = self._btn_group.button(new_val) if btn: btn.setChecked(True) def enum_strings_changed(self, new_enum_strings): """ Callback invoked when the Channel has new enum values. This callback also triggers a value_changed call so the new enum values to be broadcasted. Parameters ---------- new_enum_strings : tuple The new list of values """ if new_enum_strings is not None \ and new_enum_strings != self.enum_strings: super(PyDMEnumButton, self).enum_strings_changed(new_enum_strings) self._has_enums = True self.check_enable_state() self.rebuild_widgets()
class ConsoleRestartDialog(QDialog): """ Dialog to apply preferences that need a restart of the console kernel. """ # Constants for actions when Preferences require a kernel restart NO_RESTART = 1 RESTART_CURRENT = 2 RESTART_ALL = 3 def __init__(self, parent): super(ConsoleRestartDialog, self).__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint) self._parent = parent self._action = self.NO_RESTART self._action_string = { self.NO_RESTART: _("Keep Existing Kernels"), self.RESTART_CURRENT: _("Restart Current Kernel"), self.RESTART_ALL: _("Restart All Kernels") } # Dialog widgets # Text self._text_label = QLabel( _("By default, some IPython console preferences will be " "applied to new consoles only. To apply preferences to " "existing consoles, select from the options below.<br><br>" "Please note: applying changes to running consoles will force" " a kernel restart and all current work will be lost."), self) self._text_label.setWordWrap(True) self._text_label.setFixedWidth(450) # Checkboxes self._restart_current = QCheckBox( _("Apply to current console and restart kernel"), self) self._restart_all = QCheckBox( _("Apply to all existing consoles and restart all kernels"), self) self._checkbox_group = QButtonGroup(self) self._checkbox_group.setExclusive(False) self._checkbox_group.addButton(self._restart_current, id=self.RESTART_CURRENT) self._checkbox_group.addButton(self._restart_all, id=self.RESTART_ALL) self._action_button = QPushButton(self._action_string[self.NO_RESTART], parent=self) # Dialog Layout layout = QVBoxLayout(self) layout.addWidget(self._text_label) layout.addSpacing(5) layout.addWidget(self._restart_current) layout.addWidget(self._restart_all) layout.addSpacing(10) layout.addWidget(self._action_button, 0, Qt.AlignRight) layout.setContentsMargins(20, 20, 20, 20) self.setLayout(layout) # Signals self._checkbox_group.buttonToggled.connect( self.update_action_button_text) self._action_button.clicked.connect(self.accept) def update_action_button_text(self, checkbox, is_checked): """ Update action button text. Takes into account the given checkbox to update the text. """ checkbox_id = self._checkbox_group.id(checkbox) if is_checked: text = self._action_string[checkbox_id] self._checkbox_group.buttonToggled.disconnect( self.update_action_button_text) self._restart_current.setChecked(False) self._restart_all.setChecked(False) checkbox.setChecked(True) self._checkbox_group.buttonToggled.connect( self.update_action_button_text) else: text = self._action_string[self.NO_RESTART] self._action_button.setText(text) def get_action_value(self): """ Return tuple indicating True or False for the available actions. """ restart_current = self._restart_current.isChecked() restart_all = self._restart_all.isChecked() no_restart = not any([restart_all, restart_current]) return restart_all, restart_current, no_restart
class QtSearchInput(QWidget): """ Qt view for SearchInput Parameters ---------- model: SearchInput """ def __init__(self, model, *args, **kwargs): self.model = model super().__init__(*args, **kwargs) self.setLayout(QFormLayout()) # Radiobuttons to quickly select default time period self.all_widget = QRadioButton("All") self.year_widget = QRadioButton("1 Year") self.month_widget = QRadioButton("30 Days") self.week_widget = QRadioButton("1 Week") self.today_widget = QRadioButton("24h") self.hour_widget = QRadioButton("1 Hour") self.radio_button_group = QButtonGroup() self.radio_button_group.addButton(self.all_widget) self.radio_button_group.addButton(self.year_widget) self.radio_button_group.addButton(self.month_widget) self.radio_button_group.addButton(self.week_widget) self.radio_button_group.addButton(self.today_widget) self.radio_button_group.addButton(self.hour_widget) default_period_layout = QGridLayout() default_period_layout.setHorizontalSpacing(85) default_period_layout.setVerticalSpacing(10) default_period_layout.addWidget(self.all_widget, 0, 0, 1, 2) default_period_layout.addWidget(self.year_widget, 1, 0, 1, 2) default_period_layout.addWidget(self.month_widget, 2, 0, 1, 2) default_period_layout.addWidget(self.week_widget, 0, 1, 1, 2) default_period_layout.addWidget(self.today_widget, 1, 1, 1, 2) default_period_layout.addWidget(self.hour_widget, 2, 1, 1, 2) self.layout().addRow("When:", default_period_layout) # TODO: rethink if restriction to acceptable timedelta values is required # from ..models.search.search_input import SearchInput # self.allowed = {timedelta(days=-1), timedelta(days=-30), timedelta(minutes=-60), timedelta(days=-7), # timedelta(days=-365)} # def time_validator(since=None, until=None): # """ # Enforce that since and until are values that a UI can represent. # This is an example similar to what will be used in the Qt UI. # """ # now = timedelta() # if isinstance(since, timedelta): # if not (until is None or until == now): # raise ValueError( # "This UI cannot express since=timedelta(...) unless until " # "is timedelta() or None." # ) # for item in allowed: # if since == item: # break # else: # # No matches # raise ValueError( # "This UI can only express since as a timedelta if it is " # f"one of {allowed}. The value {since} is not allowed" # ) # s = SearchInput() # s.time_validator = time_validator # "Since: <datetime picker>" self.since_widget = QDateTimeEdit() self.since_widget.setCalendarPopup(True) self.since_widget.setDisplayFormat("yyyy-MM-dd HH:mm") self.layout().addRow("Since:", self.since_widget) # "Until: <datetime picker>" self.until_widget = QDateTimeEdit() self.until_widget.setCalendarPopup(True) self.until_widget.setDisplayFormat("yyyy-MM-dd HH:mm") self.layout().addRow("Until:", self.until_widget) # Refresh Button self.refresh_button = QPushButton("Refresh") self.layout().addWidget(self.refresh_button) # Changes to the GUI update the model. self.since_widget.dateTimeChanged.connect(self.on_since_view_changed) self.until_widget.dateTimeChanged.connect(self.on_until_view_changed) self.refresh_button.clicked.connect(self.model.request_reload) self.model.events.reload.connect(self.on_reload) self.model.events.query.connect(self.on_reload) # Changes to the model update the GUI. self.model.events.since.connect(self.on_since_model_changed) self.model.events.until.connect(self.on_until_model_changed) # connect QRadioButtons and change date dropdowns (since/until widgets) accordingly self.hour_widget.toggled.connect(self.on_toggle_hour) self.today_widget.toggled.connect(self.on_toggle_24h) self.week_widget.toggled.connect(self.on_toggle_week) self.month_widget.toggled.connect(self.on_toggle_month) self.year_widget.toggled.connect(self.on_toggle_year) self.all_widget.toggled.connect(self.on_toggle_all) self.all_widget.setChecked(True) def on_reload(self, event): now = datetime.now(LOCAL_TIMEZONE) if isinstance(self.model.since, timedelta): with _blocked(self.since_widget): self.since_widget.setDateTime(as_qdatetime(now + self.model.since)) if isinstance(self.model.until, timedelta): with _blocked(self.until_widget): self.until_widget.setDateTime(as_qdatetime(now + self.model.until)) def on_since_view_changed(self, qdatetime): # When GUI is updated self.model.since = QDateTime.toPython(qdatetime) def on_since_model_changed(self, event): # When model is updated (e.g. from console or by clicking a QRadioButton) now = datetime.now(LOCAL_TIMEZONE) if isinstance(event.date, timedelta): qdatetime = as_qdatetime(now + event.date) if event.date == timedelta(minutes=-60): self.hour_widget.setChecked(True) elif event.date == timedelta(days=-1): self.today_widget.setChecked(True) elif event.date == timedelta(days=-7): self.week_widget.setChecked(True) elif event.date == timedelta(days=-30): self.month_widget.setChecked(True) elif event.date == timedelta(days=-365): self.year_widget.setChecked(True) else: # No checkbox associated with this custom timedelta pass else: # Must be a datetime if event.date == ADA_LOVELACE_BIRTHDAY: self.all_widget.setChecked(True) else: self.uncheck_radiobuttons() qdatetime = as_qdatetime(event.date) with _blocked(self.since_widget): self.since_widget.setDateTime(qdatetime) with _blocked(self.until_widget): self.until_widget.setDateTime(as_qdatetime(now)) def on_until_view_changed(self, qdatetime): # When GUI is updated self.model.until = QDateTime.toPython(qdatetime) def on_until_model_changed(self, event): # When model is updated (e.g. from console or by clicking a QRadioButton) if not isinstance(event.date, timedelta): qdatetime = as_qdatetime(event.date) self.uncheck_radiobuttons() with _blocked(self.until_widget): self.until_widget.setDateTime(qdatetime) def on_toggle_24h(self): if self.today_widget.isChecked(): self.model.since = timedelta(days=-1) self.model.until = timedelta() def on_toggle_hour(self): if self.hour_widget.isChecked(): self.model.since = timedelta(minutes=-60) self.model.until = timedelta() def on_toggle_week(self): if self.week_widget.isChecked(): self.model.since = timedelta(days=-7) self.model.until = timedelta() def on_toggle_month(self): if self.month_widget.isChecked(): self.model.since = timedelta(days=-30) self.model.until = timedelta() def on_toggle_year(self): if self.year_widget.isChecked(): self.model.since = timedelta(days=-365) self.model.until = timedelta() def on_toggle_all(self): # Search for all catalogs since Ada Lovelace's Birthday. if self.all_widget.isChecked(): self.model.since = ADA_LOVELACE_BIRTHDAY self.model.until = timedelta() def uncheck_radiobuttons(self): self.radio_button_group.setExclusive(False) self.all_widget.setChecked(False) self.year_widget.setChecked(False) self.month_widget.setChecked(False) self.week_widget.setChecked(False) self.today_widget.setChecked(False) self.hour_widget.setChecked(False) self.radio_button_group.setExclusive(True)
def initialize_content(self): # Validators self._summary.detector_offset_edit.setValidator(QDoubleValidator(self._summary.detector_offset_edit)) self._summary.sample_dist_edit.setValidator(QDoubleValidator(self._summary.sample_dist_edit)) self._summary.n_q_bins_edit.setValidator(QIntValidator(self._summary.n_q_bins_edit)) # Event connections self._summary.detector_offset_chk.clicked.connect(self._det_offset_clicked) self._summary.sample_dist_chk.clicked.connect(self._sample_dist_clicked) self._summary.help_button.clicked.connect(self._show_help) self._summary.dark_current_check.clicked.connect(self._dark_clicked) self._summary.dark_browse_button.clicked.connect(self._dark_browse) self._summary.dark_plot_button.clicked.connect(self._dark_plot_clicked) # Output directory g2 = QButtonGroup(self) g2.addButton(self._summary.select_output_dir_radio) g2.addButton(self._summary.use_data_dir_radio) g2.setExclusive(True) self._summary.select_output_dir_radio.clicked.connect(self._output_dir_clicked) self._summary.use_data_dir_radio.clicked.connect(self._output_dir_clicked) self._summary.output_dir_browse_button.clicked.connect(self._output_dir_browse) self._output_dir_clicked() # Lin/log option g3 = QButtonGroup(self) g3.addButton(self._summary.log_binning_radio) g3.addButton(self._summary.lin_binning_radio) g3.setExclusive(True) # Q range self._summary.n_q_bins_edit.setText("100") self._summary.scale_edit.setText("1") self._summary.instr_name_label.hide() self._dark_clicked(self._summary.dark_current_check.isChecked()) # Mask Connections self._summary.mask_browse_button.clicked.connect(self._mask_browse_clicked) self._summary.mask_plot_button.clicked.connect(self._mask_plot_clicked) self._summary.mask_check.clicked.connect(self._mask_checked) # Absolute scale connections and validators self._summary.scale_edit.setValidator(QDoubleValidator(self._summary.scale_edit)) self._summary.scale_beam_radius_edit.setValidator(QDoubleValidator(self._summary.scale_beam_radius_edit)) self._summary.scale_att_trans_edit.setValidator(QDoubleValidator(self._summary.scale_att_trans_edit)) self._summary.scale_data_browse_button.clicked.connect(self._scale_data_browse) self._summary.scale_data_plot_button.clicked.connect(self._scale_data_plot_clicked) self._summary.beamstop_chk.clicked.connect(self._beamstop_clicked) self._summary.scale_chk.clicked.connect(self._scale_clicked) self._scale_clicked(self._summary.scale_chk.isChecked()) # TOF cut validator self._summary.low_tof_edit.setValidator(QDoubleValidator(self._summary.low_tof_edit)) self._summary.high_tof_edit.setValidator(QDoubleValidator(self._summary.high_tof_edit)) # TOF connections self._summary.tof_cut_chk.clicked.connect(self._tof_clicked) # Monitor normalization self._summary.beam_monitor_chk.clicked.connect(self._beam_monitor_clicked) self._summary.beam_monitor_browse_button.clicked.connect(self._beam_monitor_reference_browse) # Resolution validator self._summary.sample_apert_edit.setValidator(QDoubleValidator(self._summary.sample_apert_edit)) self._summary.resolution_chk.clicked.connect(self._resolution_clicked) # Since EQSANS does not currently use the absolute scale calculation, expose it in debug mode only for now if not self._settings.debug: self._summary.config_options_layout.deleteLater() self._summary.abs_scale_options_layout.deleteLater() self._summary.abs_scale_direct_beam_layout.deleteLater() self._summary.monitor_layout.deleteLater() self._summary.direct_beam_label.hide() self._summary.att_trans_label.hide() self._summary.beamstop_chk.hide() self._summary.scale_data_edit.hide() self._summary.scale_data_plot_button.hide() self._summary.scale_data_browse_button.hide() self._summary.scale_att_trans_edit.hide() self._summary.scale_beam_radius_edit.hide() self._summary.scale_chk.hide() self._summary.beam_monitor_chk.hide() self._summary.tof_correction_chk.hide() self._summary.beam_monitor_edit.hide() self._summary.beam_monitor_browse_button.hide() # Same thing for sample-detector distance and offset: not yet hooked in self._summary.geometry_options_groupbox.hide() # Hide expert options #self._summary.config_mask_chk.hide() self._summary.tof_cut_chk.hide() self._summary.low_tof_edit.hide() self._summary.high_tof_edit.hide() self._summary.low_tof_label.hide() self._summary.high_tof_label.hide() if not self._settings.advanced: self._summary.att_scale_factor_label.hide() self._summary.scale_edit.hide() self._summary.mask_groupbox.hide() self._summary.solid_angle_chk.hide() self._summary.resolution_chk.hide() self._summary.sample_apert_edit.hide() self._summary.sample_apert_label.hide() # We need the EQSANS data proxy for a quick load of a file for masking purposes, but # we don't want to show the plot button. Turn this off for the moment. if True or not self._has_instrument_view: self._summary.dark_plot_button.hide() self._summary.scale_data_plot_button.hide()
def initialize_content(self): """ Initialize the content of the frame """ # Validators self._content.low_scale_edit.setValidator(QDoubleValidator(self._content.low_scale_edit)) self._content.medium_scale_edit.setValidator(QDoubleValidator(self._content.medium_scale_edit)) self._content.high_scale_edit.setValidator(QDoubleValidator(self._content.high_scale_edit)) self._content.low_min_edit.setValidator(QDoubleValidator(self._content.low_min_edit)) self._content.low_max_edit.setValidator(QDoubleValidator(self._content.low_max_edit)) self._content.medium_min_edit.setValidator(QDoubleValidator(self._content.medium_min_edit)) self._content.medium_max_edit.setValidator(QDoubleValidator(self._content.medium_max_edit)) # Browse buttons self._content.low_q_browse_button.clicked.connect(self._low_q_browse) self._content.medium_q_browse_button.clicked.connect(self._medium_q_browse) self._content.high_q_browse_button.clicked.connect(self._high_q_browse) self._content.low_q_combo.activated.connect(self._update_low_q) self._content.medium_q_combo.activated.connect(self._update_medium_q) self._content.high_q_combo.activated.connect(self._update_high_q) # Radio buttons self._content.low_radio.clicked.connect(self._low_q_selected) self._content.medium_radio.clicked.connect(self._medium_q_selected) self._content.high_radio.clicked.connect(self._high_q_selected) # Selection buttons self._content.low_range_button.clicked.connect(self._low_range) self._content.medium_range_button.clicked.connect(self._medium_range) # Scale factors self._content.low_scale_edit.returnPressed.connect(self._update_low_scale) self._content.medium_scale_edit.returnPressed.connect(self._update_medium_scale) self._content.high_scale_edit.returnPressed.connect(self._update_high_scale) # Apply and save buttons self._content.apply_button.clicked.connect(self._apply) self._content.save_result_button.clicked.connect(self._save_result) # Create button group for data set selection g = QButtonGroup(self) g.addButton(self._content.low_radio) g.addButton(self._content.medium_radio) g.addButton(self._content.high_radio) g.setExclusive(True) self._content.low_radio.setChecked(True) self._content.low_q_combo.insertItem(0, "") self.populate_combobox(self._content.low_q_combo) self._content.low_q_combo.setEditable(True) self._content.medium_q_combo.insertItem(0, "") self.populate_combobox(self._content.medium_q_combo) self._content.medium_q_combo.setEditable(True) self._content.high_q_combo.insertItem(0, "") self.populate_combobox(self._content.high_q_combo) self._content.high_q_combo.setEditable(True) # pylint: disable = no-self-argument class ShowEventFilter(QObject): def eventFilter(obj_self, filteredObj, event): if event.type() == QEvent.HoverEnter: self.populate_combobox(filteredObj) filteredObj.update() elif event.type() == QEvent.KeyPress: if filteredObj == self._content.low_q_combo: self._low_q_modified = True elif filteredObj == self._content.medium_q_combo: self._medium_q_modified = True elif filteredObj == self._content.high_q_combo: self._high_q_modified = True if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter: filteredObj.setItemText(0, filteredObj.lineEdit().text()) if filteredObj == self._content.low_q_combo: self._update_low_q() elif filteredObj == self._content.medium_q_combo: self._update_medium_q() elif filteredObj == self._content.high_q_combo: self._update_high_q() return True return QObject.eventFilter(obj_self, filteredObj, event) eventFilter = ShowEventFilter(self) self._content.low_q_combo.installEventFilter(eventFilter) self._content.medium_q_combo.installEventFilter(eventFilter) self._content.high_q_combo.installEventFilter(eventFilter)
class PeriodicTableWidget(QWidget): selectionChanged = Signal() def __init__(self, parent=None): QWidget.__init__(self, parent) # Widgets, layouts and signals self._group = QButtonGroup() layout = QGridLayout() layout.setSpacing(0) ## Element for z, position in _ELEMENT_POSITIONS.items(): widget = ElementPushButton(z) widget.setCheckable(True) layout.addWidget(widget, *position) self._group.addButton(widget, z) ## Labels layout.addWidget(QLabel(''), 7, 0) # Dummy layout.addWidget(QLabel('*'), 5, 2, Qt.AlignRight) layout.addWidget(QLabel('*'), 8, 2, Qt.AlignRight) layout.addWidget(QLabel('**'), 6, 2, Qt.AlignRight) layout.addWidget(QLabel('**'), 9, 2, Qt.AlignRight) for row in [0, 1, 2, 3, 4, 5, 6, 8, 9]: layout.setRowStretch(row, 1) self.setLayout(layout) # Signals self._group.buttonClicked.connect(self.selectionChanged) # Default self.setColorFunction(_category_color_function) def setColorFunction(self, func): if not callable(func): raise ValueError('Not a function') self._color_function = func # Redraw for widget in self._group.buttons(): z = self._group.id(widget) bcolor = func(z) fcolor = 'white' if _calculate_brightness(bcolor) < 128 else 'black' sheet = 'background-color: %s; color: %s' % (bcolor.name(), fcolor) widget.setStyleSheet(sheet) def colorFunction(self): return self._color_function def setMultipleSelection(self, multiple): self._group.setExclusive(not multiple) def isMultipleSelection(self): return not self._group.exclusive() def setSelection(self, selection): def _uncheckedAll(): for widget in self._group.buttons(): widget.setChecked(False) if selection is None: _uncheckedAll() self.selectionChanged.emit() return if isinstance(selection, (int, six.string_types)): selection = [selection] if not self.isMultipleSelection() and len(selection) > 1: raise ValueError('Multiple selection mode is off. Cannot select more than one element') _uncheckedAll() for z in selection: if isinstance(z, six.string_types): z = get_atomic_number(z) self._group.button(z).setChecked(True) self.selectionChanged.emit() # def selection(self): selection = set() for widget in self._group.buttons(): if widget.isChecked(): selection.add(self._group.id(widget)) if self.isMultipleSelection(): return frozenset(selection) else: if len(selection) > 0: return list(selection)[0] else: return None def selectionSymbol(self): selection = self.selection() if self.isMultipleSelection(): return frozenset(map(get_symbol, selection)) else: if selection is None: return None else: return get_symbol(selection)
class WndLoadQuantitativeCalibration(SecondaryWindow): signal_quantitative_calibration_changed = Signal() def __init__(self, *, gpc, gui_vars): super().__init__() # Global processing classes self.gpc = gpc # Global GUI variables (used for control of GUI state) self.gui_vars = gui_vars self.initialize() def initialize(self): self.table_header_display_names = False self.setWindowTitle("PyXRF: Load Quantitative Calibration") self.setMinimumWidth(750) self.setMinimumHeight(400) self.resize(750, 600) self.pb_load_calib = QPushButton("Load Calibration ...") self.pb_load_calib.clicked.connect(self.pb_load_calib_clicked) self._changes_exist = False self._auto_update = True self.cb_auto_update = QCheckBox("Auto") self.cb_auto_update.setCheckState( Qt.Checked if self._auto_update else Qt.Unchecked) self.cb_auto_update.stateChanged.connect( self.cb_auto_update_state_changed) self.pb_update_plots = QPushButton("Update Plots") self.pb_update_plots.clicked.connect(self.pb_update_plots_clicked) self.grp_current_scan = QGroupBox( "Parameters of Currently Processed Scan") self._distance_to_sample = 0.0 self.le_distance_to_sample = LineEditExtended() le_dist_validator = QDoubleValidator() le_dist_validator.setBottom(0) self.le_distance_to_sample.setValidator(le_dist_validator) self._set_distance_to_sample() self.le_distance_to_sample.editingFinished.connect( self.le_distance_to_sample_editing_finished) self.le_distance_to_sample.focusOut.connect( self.le_distance_to_sample_focus_out) hbox = QHBoxLayout() hbox.addWidget(QLabel("Distance-to-sample:")) hbox.addWidget(self.le_distance_to_sample) hbox.addStretch(1) self.grp_current_scan.setLayout(hbox) self.eline_rb_exclusive = [ ] # Holds the list of groups of exclusive radio buttons self._setup_tab_widget() vbox = QVBoxLayout() hbox = QHBoxLayout() hbox.addWidget(self.pb_load_calib) hbox.addStretch(1) hbox.addWidget(self.cb_auto_update) hbox.addWidget(self.pb_update_plots) vbox.addLayout(hbox) vbox.addWidget(self.tab_widget) vbox.addWidget(self.grp_current_scan) self.setLayout(vbox) # Display data self.update_all_data() self._set_tooltips() def _setup_tab_widget(self): self.tab_widget = QTabWidget() self.loaded_standards = QWidget() # self.display_loaded_standards() self.scroll = QScrollArea() self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll.setWidget(self.loaded_standards) self.tab_widget.addTab(self.scroll, "Loaded Standards") self.combo_set_table_header = QComboBox() self.combo_set_table_header.addItems( ["Standard Serial #", "Standard Name"]) self.combo_set_table_header.currentIndexChanged.connect( self.combo_set_table_header_index_changed) vbox = QVBoxLayout() vbox.addSpacing(5) hbox = QHBoxLayout() hbox.addWidget(QLabel("Display in table header:")) hbox.addWidget(self.combo_set_table_header) hbox.addStretch(1) vbox.addLayout(hbox) self.table = QTableWidget() self.table.verticalHeader().hide() self.table.setSelectionMode(QTableWidget.NoSelection) self.table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) self.table.horizontalHeader().setMinimumSectionSize(150) vbox.addWidget(self.table) self.table.setStyleSheet("QTableWidget::item{color: black;}") frame = QFrame() vbox.setContentsMargins(0, 0, 0, 0) frame.setLayout(vbox) self.tab_widget.addTab(frame, "Selected Emission Lines") def display_loaded_standards(self): calib_data = self.gpc.get_quant_calibration_data() calib_settings = self.gpc.get_quant_calibration_settings() # Create the new widget (this deletes the old widget) self.loaded_standards = QWidget() self.loaded_standards.setMinimumWidth(700) # Also delete references to all components self.frames_calib_data = [] self.pbs_view = [] self.pbs_remove = [] # All 'View' buttons are added to the group in order to be connected to the same slot self.group_view = QButtonGroup() self.group_view.setExclusive(False) self.group_view.buttonClicked.connect(self.pb_view_clicked) # The same for the 'Remove' buttons self.group_remove = QButtonGroup() self.group_remove.setExclusive(False) self.group_remove.buttonClicked.connect(self.pb_remove_clicked) vbox = QVBoxLayout() class _LabelBlack(QLabel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet("color: black") for cdata, csettings in zip(calib_data, calib_settings): frame = QFrame() frame.setFrameStyle(QFrame.StyledPanel) frame.setStyleSheet( get_background_css((200, 255, 200), widget="QFrame")) _vbox = QVBoxLayout() name = cdata["name"] # Standard name (can be arbitrary string # If name is long, then print it in a separate line _name_is_long = len(name) > 30 pb_view = QPushButton("View ...") self.group_view.addButton(pb_view) pb_remove = QPushButton("Remove") self.group_remove.addButton(pb_remove) # Row 1: serial, name serial = cdata["serial"] _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack(f"<b>Standard</b> #{serial}")) if not _name_is_long: _hbox.addWidget(_LabelBlack(f"'{name}'")) _hbox.addStretch(1) _hbox.addWidget(pb_view) _hbox.addWidget(pb_remove) _vbox.addLayout(_hbox) # Optional row if _name_is_long: # Wrap name if it is extemely long name = textwrap.fill(name, width=80) _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack("<b>Name:</b> "), 0, Qt.AlignTop) _hbox.addWidget(_LabelBlack(name), 0, Qt.AlignTop) _hbox.addStretch(1) _vbox.addLayout(_hbox) # Row 2: description description = textwrap.fill(cdata["description"], width=80) _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack("<b>Description:</b>"), 0, Qt.AlignTop) _hbox.addWidget(_LabelBlack(f"{description}"), 0, Qt.AlignTop) _hbox.addStretch(1) _vbox.addLayout(_hbox) # Row 3: incident_energy = cdata["incident_energy"] scaler = cdata["scaler_name"] detector_channel = cdata["detector_channel"] distance_to_sample = cdata["distance_to_sample"] _hbox = QHBoxLayout() _hbox.addWidget( _LabelBlack(f"<b>Incident energy, keV:</b> {incident_energy}")) _hbox.addWidget(_LabelBlack(f" <b>Scaler:</b> {scaler}")) _hbox.addWidget( _LabelBlack(f" <b>Detector channel:</b> {detector_channel}")) _hbox.addWidget( _LabelBlack( f" <b>Distance-to-sample:</b> {distance_to_sample}")) _hbox.addStretch(1) _vbox.addLayout(_hbox) # Row 4: file name fln = textwrap.fill(csettings["file_path"], width=80) _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack("<b>Source file:</b>"), 0, Qt.AlignTop) _hbox.addWidget(_LabelBlack(fln), 0, Qt.AlignTop) _hbox.addStretch(1) _vbox.addLayout(_hbox) frame.setLayout(_vbox) # Now the group box is added to the upper level layout vbox.addWidget(frame) vbox.addSpacing(5) self.frames_calib_data.append(frame) self.pbs_view.append(pb_view) self.pbs_remove.append(pb_remove) # Add the layout to the widget self.loaded_standards.setLayout(vbox) # ... and put the widget inside the scroll area. This will update the # contents of the scroll area. self.scroll.setWidget(self.loaded_standards) def display_table_header(self): calib_data = self.gpc.get_quant_calibration_data() header_by_name = self.table_header_display_names tbl_labels = ["Lines"] for n, cdata in enumerate(calib_data): if header_by_name: txt = cdata["name"] else: txt = cdata["serial"] txt = textwrap.fill(txt, width=20) tbl_labels.append(txt) self.table.setHorizontalHeaderLabels(tbl_labels) def display_standard_selection_table(self): calib_data = self.gpc.get_quant_calibration_data() self._quant_file_paths = self.gpc.get_quant_calibration_file_path_list( ) brightness = 220 table_colors = [(255, brightness, brightness), (brightness, 255, brightness)] # Disconnect all radio button signals before clearing the table for bgroup in self.eline_rb_exclusive: bgroup.buttonToggled.disconnect(self.rb_selection_toggled) # This list will hold radio button groups for horizontal rows # Those are exclusive groups. They are not going to be # used directly, but they must be kept alive in order # for the radiobuttons to work properly. Most of the groups # will contain only 1 radiobutton, which will always remain checked. self.eline_rb_exclusive = [] # The following list will contain the list of radio buttons for each # row. If there is no radiobutton in a position, then the element is # set to None. # N rows: the number of emission lines, N cols: the number of standards self.eline_rb_lists = [] self.table.clear() if not calib_data: self.table.setRowCount(0) self.table.setColumnCount(0) else: # Create the sorted list of available element lines line_set = set() for cdata in calib_data: ks = list(cdata["element_lines"].keys()) line_set.update(list(ks)) self.eline_list = list(line_set) self.eline_list.sort() for n in range(len(self.eline_list)): self.eline_rb_exclusive.append(QButtonGroup()) self.eline_rb_lists.append([None] * len(calib_data)) self.table.setColumnCount(len(calib_data) + 1) self.table.setRowCount(len(self.eline_list)) self.display_table_header() for n, eline in enumerate(self.eline_list): rgb = table_colors[n % 2] item = QTableWidgetItem(eline) item.setTextAlignment(Qt.AlignCenter) item.setFlags(item.flags() & ~Qt.ItemIsEditable) item.setBackground(QBrush(QColor(*rgb))) self.table.setItem(n, 0, item) for ns, cdata in enumerate(calib_data): q_file_path = self._quant_file_paths[ ns] # Used to identify standard if eline in cdata["element_lines"]: rb = QRadioButton() if self.gpc.get_quant_calibration_is_eline_selected( eline, q_file_path): rb.setChecked(True) rb.setStyleSheet("color: black") self.eline_rb_lists[n][ns] = rb # self.eline_rb_by_standard[ns].addButton(rb) self.eline_rb_exclusive[n].addButton(rb) item = QWidget() item_hbox = QHBoxLayout(item) item_hbox.addWidget(rb) item_hbox.setAlignment(Qt.AlignCenter) item_hbox.setContentsMargins(0, 0, 0, 0) item.setStyleSheet(get_background_css(rgb)) # Generate tooltip density = cdata["element_lines"][eline]["density"] fluorescence = cdata["element_lines"][eline][ "fluorescence"] ttip = f"Fluorescence (F): {fluorescence:12g}\nDensity (D): {density:12g}\n" # Avoid very small values of density (probably zero) if abs(density) > 1e-30: ttip += f"F/D: {fluorescence/density:12g}" item.setToolTip(ttip) self.table.setCellWidget(n, ns + 1, item) else: # There is no radio button, but we still need to fill the cell item = QTableWidgetItem("") item.setFlags(item.flags() & ~Qt.ItemIsEditable) item.setBackground(QBrush(QColor(*rgb))) self.table.setItem(n, ns + 1, item) # Now the table is set (specifically radio buttons). # So we can connect the button groups with the event processing function for bgroup in self.eline_rb_exclusive: bgroup.buttonToggled.connect(self.rb_selection_toggled) @Slot() def update_all_data(self): self.display_loaded_standards() self.display_standard_selection_table() self._set_distance_to_sample() def _set_distance_to_sample(self): """Set 'le_distance_to_sample` without updating maps""" distance_to_sample = self.gpc.get_quant_calibration_distance_to_sample( ) if distance_to_sample is None: distance_to_sample = 0.0 self._distance_to_sample = distance_to_sample self._set_le_distance_to_sample(distance_to_sample) def _set_tooltips(self): set_tooltip(self.pb_load_calib, "Load <b>calibration data</b> from JSON file.") set_tooltip( self.cb_auto_update, "Automatically <b>update the plots</b> when changes are made. " "If unchecked, then button <b>Update Plots</b> must be pressed " "to update the plots. Automatic update is often undesirable " "when large maps are displayed and multiple changes to parameters " "are made.", ) set_tooltip( self.pb_update_plots, "<b>Update plots</b> based on currently selected parameters.") set_tooltip( self.le_distance_to_sample, "Distance between <b>the sample and the detector</b>. The ratio between of the distances " "during calibration and measurement is used to scale computed concentrations. " "If distance-to-sample is 0 for calibration or measurement, then no scaling is performed.", ) set_tooltip( self.combo_set_table_header, "Use <b>Serial Number</b> or <b>Name</b> of the calibration standard in the header of the table", ) set_tooltip( self.table, "Use Radio Buttons to select the <b>source of calibration data</b> for each emission line. " "This feature is needed if multiple loaded calibration files have data on the same " "emission line.", ) def update_widget_state(self, condition=None): # Update the state of the menu bar state = not self.gui_vars["gui_state"]["running_computations"] self.setEnabled(state) # Hide the window if required by the program state state_xrf_map_exists = self.gui_vars["gui_state"][ "state_xrf_map_exists"] if not state_xrf_map_exists: self.hide() if condition == "tooltips": self._set_tooltips() def cb_auto_update_state_changed(self, state): self._auto_update = state self.pb_update_plots.setEnabled(not state) # If changes were made, apply the changes while switching to 'auto' mode if state and self._changes_exist: self._update_maps_auto() def pb_update_plots_clicked(self): self._update_maps() def pb_load_calib_clicked(self): current_dir = self.gpc.get_current_working_directory() file_name = QFileDialog.getOpenFileName( self, "Select File with Quantitative Calibration Data", current_dir, "JSON (*.json);; All (*)") file_name = file_name[0] if file_name: try: logger.debug( f"Loading quantitative calibration from file: '{file_name}'" ) self.gpc.load_quantitative_calibration_data(file_name) self.update_all_data() self._update_maps_auto() except Exception: msg = "The selected JSON file has incorrect format. Select a different file." msgbox = QMessageBox(QMessageBox.Critical, "Data Loading Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() def pb_view_clicked(self, button): try: n_standard = self.pbs_view.index(button) calib_settings = self.gpc.get_quant_calibration_settings() file_path = calib_settings[n_standard]["file_path"] calib_preview = self.gpc.get_quant_calibration_text_preview( file_path) dlg = DialogViewCalibStandard(None, file_path=file_path, calib_preview=calib_preview) dlg.exec() except ValueError: logger.error( "'View' button was pressed, but not found in the list of buttons" ) def pb_remove_clicked(self, button): try: n_standard = self.pbs_remove.index(button) calib_settings = self.gpc.get_quant_calibration_settings() file_path = calib_settings[n_standard]["file_path"] self.gpc.quant_calibration_remove_entry(file_path) self.update_all_data() self._update_maps_auto() except ValueError: logger.error( "'Remove' button was pressed, but not found in the list of buttons" ) def rb_selection_toggled(self, button, checked): if checked: # Find the button in 2D list 'self.eline_rb_lists' button_found = False for nr, rb_list in enumerate(self.eline_rb_lists): try: nc = rb_list.index(button) button_found = True break except ValueError: pass if button_found: eline = self.eline_list[nr] n_standard = nc file_path = self._quant_file_paths[n_standard] self.gpc.set_quant_calibration_select_eline(eline, file_path) self._update_maps_auto() else: # This should never happen logger.error( "Selection radio button was pressed, but not found in the list" ) def combo_set_table_header_index_changed(self, index): self.table_header_display_names = bool(index) self.display_table_header() def le_distance_to_sample_editing_finished(self): distance_to_sample = float(self.le_distance_to_sample.text()) if distance_to_sample != self._distance_to_sample: self._distance_to_sample = distance_to_sample self.gpc.set_quant_calibration_distance_to_sample( distance_to_sample) self._update_maps_auto() def le_distance_to_sample_focus_out(self): try: float(self.le_distance_to_sample.text()) except ValueError: # If the text can not be interpreted to float, then replace the text with the old value self._set_le_distance_to_sample(self._distance_to_sample) def _set_le_distance_to_sample(self, distance_to_sample): self.le_distance_to_sample.setText(f"{distance_to_sample:.12g}") def _update_maps_auto(self): """Update maps only if 'auto' update is ON. Used as a 'filter' to prevent extra plot updates.""" self._changes_exist = True if self._auto_update: self._update_maps() def _update_maps(self): """Upload the selections (limit table) and update plot""" self._changes_exist = False self._redraw_maps() # Emit signal only after the maps are redrawn. This should change # ranges in the respective controls for the plots self.signal_quantitative_calibration_changed.emit() def _redraw_maps(self): # We don't emit any signals here, but we don't really need to. logger.debug("Redrawing RGB XRF Maps") self.gpc.compute_map_ranges() self.gpc.redraw_maps() self.gpc.compute_rgb_map_ranges() self.gpc.redraw_rgb_maps()