class FitParam(object): def __init__( self, name, value, min, max, logscale=False, steps=5000, format="%.3f", size_offset=0, unit="", ): self.name = name self.value = value self.min = min self.max = max self.logscale = logscale self.steps = steps self.format = format self.unit = unit self.prefix_label = None self.lineedit = None self.unit_label = None self.slider = None self.button = None self._widgets = [] self._size_offset = size_offset self._refresh_callback = None self.dataset = FitParamDataSet(title=_("Curve fitting parameter")) def copy(self): """Return a copy of this fitparam""" return self.__class__( self.name, self.value, self.min, self.max, self.logscale, self.steps, self.format, self._size_offset, self.unit, ) def create_widgets(self, parent, refresh_callback): self._refresh_callback = refresh_callback self.prefix_label = QLabel() font = self.prefix_label.font() font.setPointSize(font.pointSize() + self._size_offset) self.prefix_label.setFont(font) self.button = QPushButton() self.button.setIcon(get_icon("settings.png")) self.button.setToolTip( _("Edit '%s' fit parameter properties") % self.name) self.button.clicked.connect(lambda: self.edit_param(parent)) self.lineedit = QLineEdit() self.lineedit.editingFinished.connect(self.line_editing_finished) self.unit_label = QLabel(self.unit) self.slider = QSlider() self.slider.setOrientation(Qt.Horizontal) self.slider.setRange(0, self.steps - 1) self.slider.valueChanged.connect(self.slider_value_changed) self.update(refresh=False) self.add_widgets([ self.prefix_label, self.lineedit, self.unit_label, self.slider, self.button, ]) def add_widgets(self, widgets): self._widgets += widgets def get_widgets(self): return self._widgets def set_scale(self, state): self.logscale = state > 0 self.update_slider_value() def set_text(self, fmt=None): style = "<span style='color: #444444'><b>%s</b></span>" self.prefix_label.setText(style % self.name) if self.value is None: value_str = "" else: if fmt is None: fmt = self.format value_str = fmt % self.value self.lineedit.setText(value_str) self.lineedit.setDisabled(self.value == self.min and self.max == self.min) def line_editing_finished(self): try: self.value = float(self.lineedit.text()) except ValueError: self.set_text() self.update_slider_value() self._refresh_callback() def slider_value_changed(self, int_value): if self.logscale: total_delta = np.log10(1 + self.max - self.min) self.value = (self.min + 10**(total_delta * int_value / (self.steps - 1)) - 1) else: total_delta = self.max - self.min self.value = self.min + total_delta * int_value / (self.steps - 1) self.set_text() self._refresh_callback() def update_slider_value(self): if self.value is None or self.min is None or self.max is None: self.slider.setEnabled(False) if self.slider.parent() and self.slider.parent().isVisible(): self.slider.show() elif self.value == self.min and self.max == self.min: self.slider.hide() else: self.slider.setEnabled(True) if self.slider.parent() and self.slider.parent().isVisible(): self.slider.show() if self.logscale: value_delta = max([np.log10(1 + self.value - self.min), 0.0]) total_delta = np.log10(1 + self.max - self.min) else: value_delta = self.value - self.min total_delta = self.max - self.min intval = int(self.steps * value_delta / total_delta) self.slider.blockSignals(True) self.slider.setValue(intval) self.slider.blockSignals(False) def edit_param(self, parent): update_dataset(self.dataset, self) if self.dataset.edit(parent=parent): restore_dataset(self.dataset, self) if self.value > self.max: self.max = self.value if self.value < self.min: self.min = self.value self.update() def update(self, refresh=True): self.unit_label.setText(self.unit) self.slider.setRange(0, self.steps - 1) self.update_slider_value() self.set_text() if refresh: self._refresh_callback()
class EditGeometryProperties(PyDialog): force = True def __init__(self, data, win_parent=None): """ +------------------+ | Edit Actor Props | +------------------+------+ | Name1 | | Name2 | | Name3 | | Name4 | | | | Active_Name main | | Color box | | Line_Width 2 | | Point_Size 2 | | Bar_Scale 2 | | Opacity 0.5 | | Show/Hide | | | | Apply OK Cancel | +-------------------------+ """ PyDialog.__init__(self, data, win_parent) self.set_font_size(data['font_size']) del self.out_data['font_size'] self.setWindowTitle('Edit Geometry Properties') self.allow_update = True #default #self.win_parent = win_parent #self.out_data = data self.keys = sorted(data.keys()) self.keys = data.keys() keys = self.keys #nrows = len(keys) self.active_key = 'main' items = list(keys) header_labels = ['Groups'] table_model = Model(items, header_labels, self) view = SingleChoiceQTableView(self) #Call your custom QTableView here view.setModel(table_model) #if qt_version == 4: #view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table = view #self.opacity_edit.valueChanged.connect(self.on_opacity) #mListWidget, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(itemClicked(QListWidgetItem*))); #self.table.itemClicked.connect(self.table.mouseDoubleClickEvent) actor_obj = data[self.active_key] name = actor_obj.name line_width = actor_obj.line_width point_size = actor_obj.point_size bar_scale = actor_obj.bar_scale opacity = actor_obj.opacity color = actor_obj.color show = actor_obj.is_visible self.representation = actor_obj.representation # table header = self.table.horizontalHeader() header.setStretchLastSection(True) self._default_is_apply = False self.name = QLabel("Name:") self.name_edit = QLineEdit(str(name)) self.name_edit.setDisabled(True) self.color = QLabel("Color:") self.color_edit = QPushButton() #self.color_edit.setFlat(True) color = self.out_data[self.active_key].color qcolor = QtGui.QColor() qcolor.setRgb(*color) #print('color =%s' % str(color)) palette = QtGui.QPalette(self.color_edit.palette()) # make a copy of the palette #palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, \ #qcolor) palette.setColor(QtGui.QPalette.Background, QtGui.QColor('blue')) # ButtonText self.color_edit.setPalette(palette) self.color_edit.setStyleSheet("QPushButton {" "background-color: rgb(%s, %s, %s);" % tuple(color) + #"border:1px solid rgb(255, 170, 255); " "}") self.use_slider = True self.is_opacity_edit_active = False self.is_opacity_edit_slider_active = False self.is_line_width_edit_active = False self.is_line_width_edit_slider_active = False self.is_point_size_edit_active = False self.is_point_size_edit_slider_active = False self.is_bar_scale_edit_active = False self.is_bar_scale_edit_slider_active = False self.opacity = QLabel("Opacity:") self.opacity_edit = QDoubleSpinBox(self) self.opacity_edit.setRange(0.1, 1.0) self.opacity_edit.setDecimals(1) self.opacity_edit.setSingleStep(0.1) self.opacity_edit.setValue(opacity) if self.use_slider: self.opacity_slider_edit = QSlider(QtCore.Qt.Horizontal) self.opacity_slider_edit.setRange(1, 10) self.opacity_slider_edit.setValue(opacity * 10) self.opacity_slider_edit.setTickInterval(1) self.opacity_slider_edit.setTickPosition(QSlider.TicksBelow) self.line_width = QLabel("Line Width:") self.line_width_edit = QSpinBox(self) self.line_width_edit.setRange(1, MAX_LINE_WIDTH) self.line_width_edit.setSingleStep(1) self.line_width_edit.setValue(line_width) if self.use_slider: self.line_width_slider_edit = QSlider(QtCore.Qt.Horizontal) self.line_width_slider_edit.setRange(1, MAX_LINE_WIDTH) self.line_width_slider_edit.setValue(line_width) self.line_width_slider_edit.setTickInterval(1) self.line_width_slider_edit.setTickPosition(QSlider.TicksBelow) if self.representation in ['point', 'surface']: self.line_width.setEnabled(False) self.line_width_edit.setEnabled(False) self.line_width_slider_edit.setEnabled(False) self.point_size = QLabel("Point Size:") self.point_size_edit = QSpinBox(self) self.point_size_edit.setRange(1, MAX_POINT_SIZE) self.point_size_edit.setSingleStep(1) self.point_size_edit.setValue(point_size) self.point_size.setVisible(False) self.point_size_edit.setVisible(False) if self.use_slider: self.point_size_slider_edit = QSlider(QtCore.Qt.Horizontal) self.point_size_slider_edit.setRange(1, MAX_POINT_SIZE) self.point_size_slider_edit.setValue(point_size) self.point_size_slider_edit.setTickInterval(1) self.point_size_slider_edit.setTickPosition(QSlider.TicksBelow) self.point_size_slider_edit.setVisible(False) if self.representation in ['wire', 'surface']: self.point_size.setEnabled(False) self.point_size_edit.setEnabled(False) if self.use_slider: self.point_size_slider_edit.setEnabled(False) self.bar_scale = QLabel("Bar Scale:") self.bar_scale_edit = QDoubleSpinBox(self) #self.bar_scale_edit.setRange(0.01, 1.0) # was 0.1 #self.bar_scale_edit.setRange(0.05, 5.0) self.bar_scale_edit.setDecimals(1) #self.bar_scale_edit.setSingleStep(bar_scale / 10.) self.bar_scale_edit.setSingleStep(0.1) self.bar_scale_edit.setValue(bar_scale) #if self.use_slider: #self.bar_scale_slider_edit = QSlider(QtCore.Qt.Horizontal) #self.bar_scale_slider_edit.setRange(1, 100) # 1/0.05 = 100/5.0 #self.bar_scale_slider_edit.setValue(opacity * 0.05) #self.bar_scale_slider_edit.setTickInterval(10) #self.bar_scale_slider_edit.setTickPosition(QSlider.TicksBelow) if self.representation != 'bar': self.bar_scale.setEnabled(False) self.bar_scale_edit.setEnabled(False) self.bar_scale.setVisible(False) self.bar_scale_edit.setVisible(False) #self.bar_scale_slider_edit.setVisible(False) #self.bar_scale_slider_edit.setEnabled(False) # show/hide self.checkbox_show = QCheckBox("Show") self.checkbox_hide = QCheckBox("Hide") self.checkbox_show.setChecked(show) self.checkbox_hide.setChecked(not show) if name == 'main': self.color.setEnabled(False) self.color_edit.setEnabled(False) self.point_size.setEnabled(False) self.point_size_edit.setEnabled(False) if self.use_slider: self.point_size_slider_edit.setEnabled(False) self.cancel_button = QPushButton("Close") self.create_layout() self.set_connections() def on_delete(self, irow): """deletes an actor based on the row number""" if irow == 0: # main return nkeys = len(self.keys) if nkeys in [0, 1]: return name = self.keys[irow] nrows = nkeys - 1 self.keys.pop(irow) header_labels = ['Groups'] table_model = Model(self.keys, header_labels, self) self.table.setModel(table_model) if len(self.keys) == 0: self.update() self.set_as_null() return if irow == nrows: irow -= 1 new_name = self.keys[irow] self.update_active_name(new_name) if self.is_gui: self.win_parent.delete_actor(name) def set_as_null(self): """sets the null case""" self.name.setVisible(False) self.name_edit.setVisible(False) self.color.setVisible(False) self.color_edit.setVisible(False) self.line_width.setVisible(False) self.line_width_edit.setVisible(False) self.point_size.setVisible(False) self.point_size_edit.setVisible(False) self.bar_scale.setVisible(False) self.bar_scale_edit.setVisible(False) self.opacity.setVisible(False) self.opacity_edit.setVisible(False) self.opacity_slider_edit.setVisible(False) self.point_size_slider_edit.setVisible(False) self.line_width_slider_edit.setVisible(False) self.checkbox_show.setVisible(False) self.checkbox_hide.setVisible(False) def on_update_geometry_properties_window(self, data): """Not Implemented""" return #new_keys = sorted(data.keys()) #if self.active_key in new_keys: #i = new_keys.index(self.active_key) #else: #i = 0 #self.table.update_data(new_keys) #self.out_data = data #self.update_active_key(i) def update_active_key(self, index): """ Parameters ---------- index : PyQt4.QtCore.QModelIndex the index of the list Internal Parameters ------------------- name : str the name of obj obj : CoordProperties, AltGeometry the storage object for things like line_width, point_size, etc. """ name = str(index.data()) #print('name = %r' % name) #i = self.keys.index(self.active_key) self.update_active_name(name) def update_active_name(self, name): self.active_key = name self.name_edit.setText(name) obj = self.out_data[name] if isinstance(obj, CoordProperties): opacity = 1.0 representation = 'coord' is_visible = obj.is_visible elif isinstance(obj, AltGeometry): line_width = obj.line_width point_size = obj.point_size bar_scale = obj.bar_scale opacity = obj.opacity representation = obj.representation is_visible = obj.is_visible self.color_edit.setStyleSheet("QPushButton {" "background-color: rgb(%s, %s, %s);" % tuple(obj.color) + #"border:1px solid rgb(255, 170, 255); " "}") self.allow_update = False self.force = False self.line_width_edit.setValue(line_width) self.point_size_edit.setValue(point_size) self.bar_scale_edit.setValue(bar_scale) self.force = True self.allow_update = True else: raise NotImplementedError(obj) #allowed_representations = [ #'main', 'surface', 'coord', 'toggle', 'wire', 'point', 'bar'] if self.representation != representation: self.representation = representation #if representation not in allowed_representations: #msg = 'name=%r; representation=%r is invalid\nrepresentations=%r' % ( #name, representation, allowed_representations) if self.representation == 'coord': self.color.setVisible(False) self.color_edit.setVisible(False) self.line_width.setVisible(False) self.line_width_edit.setVisible(False) self.point_size.setVisible(False) self.point_size_edit.setVisible(False) self.bar_scale.setVisible(False) self.bar_scale_edit.setVisible(False) self.opacity.setVisible(False) self.opacity_edit.setVisible(False) if self.use_slider: self.opacity_slider_edit.setVisible(False) self.point_size_slider_edit.setVisible(False) self.line_width_slider_edit.setVisible(False) #self.bar_scale_slider_edit.setVisible(False) else: self.color.setVisible(True) self.color_edit.setVisible(True) self.line_width.setVisible(True) self.line_width_edit.setVisible(True) self.point_size.setVisible(True) self.point_size_edit.setVisible(True) self.bar_scale.setVisible(True) #self.bar_scale_edit.setVisible(True) self.opacity.setVisible(True) self.opacity_edit.setVisible(True) if self.use_slider: self.opacity_slider_edit.setVisible(True) self.line_width_slider_edit.setVisible(True) self.point_size_slider_edit.setVisible(True) #self.bar_scale_slider_edit.setVisible(True) if name == 'main': self.color.setEnabled(False) self.color_edit.setEnabled(False) self.point_size.setEnabled(False) self.point_size_edit.setEnabled(False) self.line_width.setEnabled(True) self.line_width_edit.setEnabled(True) self.bar_scale.setEnabled(False) self.bar_scale_edit.setEnabled(False) show_points = False show_line_width = True show_bar_scale = False if self.use_slider: self.line_width_slider_edit.setEnabled(True) #self.bar_scale_slider_edit.setVisible(False) else: self.color.setEnabled(True) self.color_edit.setEnabled(True) show_points = False if self.representation in ['point', 'wire+point']: show_points = True show_line_width = False if self.representation in ['wire', 'wire+point', 'bar', 'toggle']: show_line_width = True if representation == 'bar': show_bar_scale = True else: show_bar_scale = False #self.bar_scale_button.setVisible(show_bar_scale) #self.bar_scale_edit.setSingleStep(bar_scale / 10.) #if self.use_slider: #self.bar_scale_slider_edit.setEnabled(False) self.point_size.setEnabled(show_points) self.point_size_edit.setEnabled(show_points) self.point_size.setVisible(show_points) self.point_size_edit.setVisible(show_points) self.line_width.setEnabled(show_line_width) self.line_width_edit.setEnabled(show_line_width) self.bar_scale.setEnabled(show_bar_scale) self.bar_scale_edit.setEnabled(show_bar_scale) self.bar_scale.setVisible(show_bar_scale) self.bar_scale_edit.setVisible(show_bar_scale) if self.use_slider: self.point_size_slider_edit.setEnabled(show_points) self.point_size_slider_edit.setVisible(show_points) self.line_width_slider_edit.setEnabled(show_line_width) #if self.representation in ['wire', 'surface']: self.opacity_edit.setValue(opacity) #if self.use_slider: #self.opacity_slider_edit.setValue(opacity*10) self.checkbox_show.setChecked(is_visible) self.checkbox_hide.setChecked(not is_visible) passed = self.on_validate() #self.on_apply(force=True) # TODO: was turned on...do I want this??? #self.allow_update = True def create_layout(self): ok_cancel_box = QHBoxLayout() ok_cancel_box.addWidget(self.cancel_button) grid = QGridLayout() irow = 0 grid.addWidget(self.name, irow, 0) grid.addWidget(self.name_edit, irow, 1) irow += 1 grid.addWidget(self.color, irow, 0) grid.addWidget(self.color_edit, irow, 1) irow += 1 grid.addWidget(self.opacity, irow, 0) if self.use_slider: grid.addWidget(self.opacity_edit, irow, 2) grid.addWidget(self.opacity_slider_edit, irow, 1) else: grid.addWidget(self.opacity_edit, irow, 1) irow += 1 grid.addWidget(self.line_width, irow, 0) if self.use_slider: grid.addWidget(self.line_width_edit, irow, 2) grid.addWidget(self.line_width_slider_edit, irow, 1) else: grid.addWidget(self.line_width_edit, irow, 1) irow += 1 grid.addWidget(self.point_size, irow, 0) if self.use_slider: grid.addWidget(self.point_size_edit, irow, 2) grid.addWidget(self.point_size_slider_edit, irow, 1) else: grid.addWidget(self.point_size_edit, irow, 1) irow += 1 grid.addWidget(self.bar_scale, irow, 0) if self.use_slider and 0: grid.addWidget(self.bar_scale_edit, irow, 2) grid.addWidget(self.bar_scale_slider_edit, irow, 1) else: grid.addWidget(self.bar_scale_edit, irow, 1) irow += 1 checkboxs = QButtonGroup(self) checkboxs.addButton(self.checkbox_show) checkboxs.addButton(self.checkbox_hide) vbox = QVBoxLayout() vbox.addWidget(self.table, stretch=1) vbox.addLayout(grid) vbox1 = QVBoxLayout() vbox1.addWidget(self.checkbox_show) vbox1.addWidget(self.checkbox_hide) vbox.addLayout(vbox1) vbox.addStretch() #vbox.addWidget(self.check_apply) vbox.addLayout(ok_cancel_box) self.setLayout(vbox) def set_connections(self): self.opacity_edit.valueChanged.connect(self.on_opacity) self.line_width_edit.valueChanged.connect(self.on_line_width) self.point_size_edit.valueChanged.connect(self.on_point_size) self.bar_scale_edit.valueChanged.connect(self.on_bar_scale) if self.use_slider: self.opacity_slider_edit.valueChanged.connect(self.on_opacity_slider) self.line_width_slider_edit.valueChanged.connect(self.on_line_width_slider) self.point_size_slider_edit.valueChanged.connect(self.on_point_size_slider) #self.bar_scale_slider_edit.valueChanged.connect(self.on_bar_scale_slider) # self.connect(self.opacity_edit, QtCore.SIGNAL('clicked()'), self.on_opacity) # self.connect(self.line_width, QtCore.SIGNAL('clicked()'), self.on_line_width) # self.connect(self.point_size, QtCore.SIGNAL('clicked()'), self.on_point_size) if qt_version == 4: self.connect(self, QtCore.SIGNAL('triggered()'), self.closeEvent) self.color_edit.clicked.connect(self.on_color) self.checkbox_show.clicked.connect(self.on_show) self.checkbox_hide.clicked.connect(self.on_hide) self.cancel_button.clicked.connect(self.on_cancel) # closeEvent def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Escape: self.close() def closeEvent(self, event): self.on_cancel() def on_color(self): """called when the user clicks on the color box""" name = self.active_key obj = self.out_data[name] rgb_color_ints = obj.color msg = name col = QColorDialog.getColor(QtGui.QColor(*rgb_color_ints), self, "Choose a %s color" % msg) if col.isValid(): color_float = col.getRgbF()[:3] obj.color = color_float color_int = [int(colori * 255) for colori in color_float] self.color_edit.setStyleSheet("QPushButton {" "background-color: rgb(%s, %s, %s);" % tuple(color_int) + #"border:1px solid rgb(255, 170, 255); " "}") self.on_apply(force=self.force) #print(self.allow_update) def on_show(self): """shows the actor""" name = self.active_key is_checked = self.checkbox_show.isChecked() self.out_data[name].is_visible = is_checked self.on_apply(force=self.force) def on_hide(self): """hides the actor""" name = self.active_key is_checked = self.checkbox_hide.isChecked() self.out_data[name].is_visible = not is_checked self.on_apply(force=self.force) def on_line_width(self): """increases/decreases the wireframe (for solid bodies) or the bar thickness""" self.is_line_width_edit_active = True name = self.active_key line_width = self.line_width_edit.value() self.out_data[name].line_width = line_width if not self.is_line_width_edit_slider_active: if self.use_slider: self.line_width_slider_edit.setValue(line_width) self.is_line_width_edit_active = False self.on_apply(force=self.force) self.is_line_width_edit_active = False def on_line_width_slider(self): """increases/decreases the wireframe (for solid bodies) or the bar thickness""" self.is_line_width_edit_slider_active = True #name = self.active_key line_width = self.line_width_slider_edit.value() if not self.is_line_width_edit_active: self.line_width_edit.setValue(line_width) self.is_line_width_edit_slider_active = False def on_point_size(self): """increases/decreases the point size""" self.is_point_size_edit_active = True name = self.active_key point_size = self.point_size_edit.value() self.out_data[name].point_size = point_size if not self.is_point_size_edit_slider_active: if self.use_slider: self.point_size_slider_edit.setValue(point_size) self.is_point_size_edit_active = False self.on_apply(force=self.force) self.is_point_size_edit_active = False def on_point_size_slider(self): """increases/decreases the point size""" self.is_point_size_edit_slider_active = True #name = self.active_key point_size = self.point_size_slider_edit.value() if not self.is_point_size_edit_active: self.point_size_edit.setValue(point_size) self.is_point_size_edit_slider_active = False def on_bar_scale(self): """ Vectors start at some xyz coordinate and can increase in length. Increases/decreases the length scale factor. """ self.is_bar_scale_edit_active = True name = self.active_key float_bar_scale = self.bar_scale_edit.value() self.out_data[name].bar_scale = float_bar_scale if not self.is_bar_scale_edit_slider_active: #int_bar_scale = int(round(float_bar_scale * 20, 0)) #if self.use_slider: #self.bar_scale_slider_edit.setValue(int_bar_scale) self.is_bar_scale_edit_active = False self.on_apply(force=self.force) self.is_bar_scale_edit_active = False def on_bar_scale_slider(self): """ Vectors start at some xyz coordinate and can increase in length. Increases/decreases the length scale factor. """ self.is_bar_scale_edit_slider_active = True #name = self.active_key int_bar_scale = self.bar_scale_slider_edit.value() if not self.is_bar_scale_edit_active: float_bar_scale = int_bar_scale / 20. self.bar_scale_edit.setValue(float_bar_scale) self.is_bar_scale_edit_slider_active = False def on_opacity(self): """ opacity = 1.0 (solid/opaque) opacity = 0.0 (invisible) """ self.is_opacity_edit_active = True name = self.active_key float_opacity = self.opacity_edit.value() self.out_data[name].opacity = float_opacity if not self.is_opacity_edit_slider_active: int_opacity = int(round(float_opacity * 10, 0)) if self.use_slider: self.opacity_slider_edit.setValue(int_opacity) self.is_opacity_edit_active = False self.on_apply(force=self.force) self.is_opacity_edit_active = False def on_opacity_slider(self): """ opacity = 1.0 (solid/opaque) opacity = 0.0 (invisible) """ self.is_opacity_edit_slider_active = True #name = self.active_key int_opacity = self.opacity_slider_edit.value() if not self.is_opacity_edit_active: float_opacity = int_opacity / 10. self.opacity_edit.setValue(float_opacity) self.is_opacity_edit_slider_active = False def on_validate(self): self.out_data['clicked_ok'] = True self.out_data['clicked_cancel'] = False old_obj = self.out_data[self.active_key] old_obj.line_width = self.line_width_edit.value() old_obj.point_size = self.point_size_edit.value() old_obj.bar_scale = self.bar_scale_edit.value() old_obj.opacity = self.opacity_edit.value() #old_obj.color = self.color_edit old_obj.is_visible = self.checkbox_show.isChecked() return True #name_value, flag0 = self.check_name(self.name_edit) #ox_value, flag1 = self.check_float(self.transparency_edit) #if flag0 and flag1: #self.out_data['clicked_ok'] = True #return True #return False @property def is_gui(self): return hasattr(self.win_parent, 'on_update_geometry_properties') def on_apply(self, force=False): passed = self.on_validate() #print("passed=%s force=%s allow=%s" % (passed, force, self.allow_update)) if (passed or force) and self.allow_update and self.is_gui: #print('obj = %s' % self.out_data[self.active_key]) self.win_parent.on_update_geometry_properties(self.out_data, name=self.active_key) return passed def on_cancel(self): passed = self.on_apply(force=True) if passed: self.close()
class PyDMSlider(QFrame, TextFormatter, PyDMWritableWidget): """ A QSlider with support for Channels and more from PyDM. Parameters ---------- parent : QWidget The parent widget for the Label init_channel : str, optional The channel to be used by the widget. """ actionTriggered = Signal(int) rangeChanged = Signal(float, float) sliderMoved = Signal(float) sliderPressed = Signal() sliderReleased = Signal() valueChanged = Signal(float) def __init__(self, parent=None, init_channel=None): QFrame.__init__(self, parent) PyDMWritableWidget.__init__(self, init_channel=init_channel) self.alarmSensitiveContent = True self.alarmSensitiveBorder = False # Internal values for properties self._show_limit_labels = True self._show_value_label = True self._user_defined_limits = False self._needs_limit_info = True self._minimum = None self._maximum = None self._user_minimum = -10.0 self._user_maximum = 10.0 self._num_steps = 101 self._orientation = Qt.Horizontal # Set up all the internal widgets that make up a PyDMSlider. # We'll add all these things to layouts when we call setup_widgets_for_orientation label_size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) self.low_lim_label = QLabel(self) self.low_lim_label.setObjectName("lowLimLabel") self.low_lim_label.setSizePolicy(label_size_policy) self.low_lim_label.setAlignment(Qt.AlignLeft | Qt.AlignTrailing | Qt.AlignVCenter) self.value_label = QLabel(self) self.value_label.setObjectName("valueLabel") self.value_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.high_lim_label = QLabel(self) self.high_lim_label.setObjectName("highLimLabel") self.high_lim_label.setSizePolicy(label_size_policy) self.high_lim_label.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) self._slider = QSlider(parent=self) self._slider.setOrientation(Qt.Horizontal) self._slider.sliderMoved.connect(self.internal_slider_moved) self._slider.sliderPressed.connect(self.internal_slider_pressed) self._slider.sliderReleased.connect(self.internal_slider_released) self._slider.valueChanged.connect(self.internal_slider_value_changed) # self.vertical_layout.addWidget(self._slider) # Other internal variables and final setup steps self._slider_position_to_value_map = None self._mute_internal_slider_changes = False self.setup_widgets_for_orientation(self._orientation) self.reset_slider_limits() def init_for_designer(self): """ Method called after the constructor to tweak configurations for when using the widget with the Qt Designer """ self.value = 0.0 @Property(Qt.Orientation) def orientation(self): """ The slider orientation (Horizontal or Vertical) Returns ------- int Qt.Horizontal or Qt.Vertical """ return self._orientation @orientation.setter def orientation(self, new_orientation): """ The slider orientation (Horizontal or Vertical) Parameters ---------- new_orientation : int Qt.Horizontal or Qt.Vertical """ self._orientation = new_orientation self.setup_widgets_for_orientation(new_orientation) def setup_widgets_for_orientation(self, new_orientation): """ Reconstruct the widget given the orientation. Parameters ---------- new_orientation : int Qt.Horizontal or Qt.Vertical """ if new_orientation not in (Qt.Horizontal, Qt.Vertical): logger.error( "Invalid orientation '{0}'. The existing layout will not change." .format(new_orientation)) return layout = None if new_orientation == Qt.Horizontal: layout = QVBoxLayout() layout.setContentsMargins(4, 0, 4, 4) label_layout = QHBoxLayout() label_layout.addWidget(self.low_lim_label) label_layout.addStretch(0) label_layout.addWidget(self.value_label) label_layout.addStretch(0) label_layout.addWidget(self.high_lim_label) layout.addLayout(label_layout) self._slider.setOrientation(new_orientation) layout.addWidget(self._slider) elif new_orientation == Qt.Vertical: layout = QHBoxLayout() layout.setContentsMargins(0, 4, 4, 4) label_layout = QVBoxLayout() label_layout.addWidget(self.high_lim_label) label_layout.addStretch(0) label_layout.addWidget(self.value_label) label_layout.addStretch(0) label_layout.addWidget(self.low_lim_label) layout.addLayout(label_layout) self._slider.setOrientation(new_orientation) layout.addWidget(self._slider) if self.layout() is not None: # Trick to remove the existing layout by re-parenting it in an empty widget. QWidget().setLayout(self.layout()) self.setLayout(layout) def update_labels(self): """ Update the limits and value labels with the correct values. """ def set_label(value, label_widget): if value is None: label_widget.setText("") else: label_widget.setText(self.format_string.format(value)) set_label(self.minimum, self.low_lim_label) set_label(self.maximum, self.high_lim_label) set_label(self.value, self.value_label) def reset_slider_limits(self): """ Reset the limits and adjust the labels properly for the slider. """ if self.minimum is None or self.maximum is None: self._needs_limit_info = True self.set_enable_state() return self._needs_limit_info = False self._slider.setMinimum(0) self._slider.setMaximum(self._num_steps - 1) self._slider.setSingleStep(1) self._slider.setPageStep(10) self._slider_position_to_value_map = np.linspace(self.minimum, self.maximum, num=self._num_steps) self.update_labels() self.set_slider_to_closest_value(self.value) self.rangeChanged.emit(self.minimum, self.maximum) self.set_enable_state() def find_closest_slider_position_to_value(self, val): """ Find and returns the index for the closest position on the slider for a given value. Parameters ---------- val : float Returns ------- int """ diff = abs(self._slider_position_to_value_map - float(val)) return np.argmin(diff) def set_slider_to_closest_value(self, val): """ Set the value for the slider according to a given value. Parameters ---------- val : float """ if val is None or self._needs_limit_info: return # When we set the slider to the closest value, it may end up at a slightly # different position than val (if val is not in self._slider_position_to_value_map) # We don't want that slight difference to get broacast out and put the channel # somewhere new. For example, if the slider can only be at 0.4 or 0.5, but a # new value comes in of 0.45, its more important to keep the 0.45 than to change # it to where the slider gets set. Therefore, we mute the internal slider changes # so that its valueChanged signal doesn't cause us to emit a signal to PyDM to change # the value of the channel. self._mute_internal_slider_changes = True self._slider.setValue(self.find_closest_slider_position_to_value(val)) self._mute_internal_slider_changes = False def value_changed(self, new_val): """ Callback invoked when the Channel value is changed. Parameters ---------- new_val : int or float The new value from the channel. """ PyDMWritableWidget.value_changed(self, new_val) if hasattr(self, "value_label"): self.value_label.setText(self.format_string.format(self.value)) if not self._slider.isSliderDown(): self.set_slider_to_closest_value(self.value) def ctrl_limit_changed(self, which, new_limit): """ Callback invoked when the Channel receives new control limit values. Parameters ---------- which : str Which control limit was changed. "UPPER" or "LOWER" new_limit : float New value for the control limit """ PyDMWritableWidget.ctrl_limit_changed(self, which, new_limit) if not self.userDefinedLimits: self.reset_slider_limits() def update_format_string(self): """ Reconstruct the format string to be used when representing the output value. Returns ------- format_string : str The format string to be used including or not the precision and unit """ fs = super(PyDMSlider, self).update_format_string() self.update_labels() return fs def set_enable_state(self): """ Determines wether or not the widget must be enabled or not depending on the write access, connection state and presence of limits information """ # Even though by documentation disabling parent QFrame (self), should disable internal # slider, in practice it still remains interactive (PyQt 5.12.1). Disabling explicitly, solves # the problem. should_enable = self._write_access and self._connected and not self._needs_limit_info self.setEnabled(should_enable) self._slider.setEnabled(should_enable) @Slot(int) def internal_slider_action_triggered(self, action): self.actionTriggered.emit(action) @Slot(int) def internal_slider_moved(self, val): """ Method invoked when the slider is moved. Parameters ---------- val : float """ # Avoid potential crash if limits are undefined if self._slider_position_to_value_map is None: return # The user has moved the slider, we need to update our value. # Only update the underlying value, not the self.value property, # because we don't need to reset the slider position. If we change # self.value, we can get into a loop where the position changes, which # updates the value, which changes the position again, etc etc. self.value = self._slider_position_to_value_map[val] self.sliderMoved.emit(self.value) @Slot() def internal_slider_pressed(self): """ Method invoked when the slider is pressed """ self.sliderPressed.emit() @Slot() def internal_slider_released(self): """ Method invoked when the slider is released """ self.sliderReleased.emit() @Slot(int) def internal_slider_value_changed(self, val): """ Method invoked when a new value is selected on the slider. This will cause the new value to be emitted to the signal unless `mute_internal_slider_changes` is True. Parameters ---------- val : int """ # At this point, our local copy of the value reflects the position of the # slider, now all we need to do is emit a signal to PyDM so that the data # plugin will send a put to the channel. Don't update self.value or self._value # in here, it is pointless at best, and could cause an infinite loop at worst. if not self._mute_internal_slider_changes: self.send_value_signal[float].emit(self.value) @Property(bool) def showLimitLabels(self): """ Whether or not the high and low limits should be displayed on the slider. Returns ------- bool """ return self._show_limit_labels @showLimitLabels.setter def showLimitLabels(self, checked): """ Whether or not the high and low limits should be displayed on the slider. Parameters ---------- checked : bool """ self._show_limit_labels = checked if checked: self.low_lim_label.show() self.high_lim_label.show() else: self.low_lim_label.hide() self.high_lim_label.hide() @Property(bool) def showValueLabel(self): """ Whether or not the current value should be displayed on the slider. Returns ------- bool """ return self._show_value_label @showValueLabel.setter def showValueLabel(self, checked): """ Whether or not the current value should be displayed on the slider. Parameters ---------- checked : bool """ self._show_value_label = checked if checked: self.value_label.show() else: self.value_label.hide() @Property(QSlider.TickPosition) def tickPosition(self): """ Where to draw tick marks for the slider. Returns ------- QSlider.TickPosition """ return self._slider.tickPosition() @tickPosition.setter def tickPosition(self, position): """ Where to draw tick marks for the slider. Parameter --------- position : QSlider.TickPosition """ self._slider.setTickPosition(position) @Property(bool) def userDefinedLimits(self): """ Wether or not to use limits defined by the user and not from the channel Returns ------- bool """ return self._user_defined_limits @userDefinedLimits.setter def userDefinedLimits(self, user_defined_limits): """ Wether or not to use limits defined by the user and not from the channel Parameters ---------- user_defined_limits : bool """ self._user_defined_limits = user_defined_limits self.reset_slider_limits() @Property(float) def userMinimum(self): """ Lower user defined limit value Returns ------- float """ return self._user_minimum @userMinimum.setter def userMinimum(self, new_min): """ Lower user defined limit value Parameters ---------- new_min : float """ self._user_minimum = float(new_min) if new_min is not None else None if self.userDefinedLimits: self.reset_slider_limits() @Property(float) def userMaximum(self): """ Upper user defined limit value Returns ------- float """ return self._user_maximum @userMaximum.setter def userMaximum(self, new_max): """ Upper user defined limit value Parameters ---------- new_max : float """ self._user_maximum = float(new_max) if new_max is not None else None if self.userDefinedLimits: self.reset_slider_limits() @property def minimum(self): """ The current value being used for the lower limit Returns ------- float """ if self.userDefinedLimits: return self._user_minimum return self._lower_ctrl_limit @property def maximum(self): """ The current value being used for the upper limit Returns ------- float """ if self.userDefinedLimits: return self._user_maximum return self._upper_ctrl_limit @Property(int) def num_steps(self): """ The number of steps on the slider Returns ------- int """ return self._num_steps @num_steps.setter def num_steps(self, new_steps): """ The number of steps on the slider Parameters ---------- new_steps : int """ self._num_steps = int(new_steps) self.reset_slider_limits()
class PyDMChartingDisplay(Display): def __init__(self, parent=None, args=[], macros=None): """ Create all the widgets, including any child dialogs. Parameters ---------- parent : QWidget The parent widget of the charting display args : list The command parameters macros : str Macros to modify the UI parameters at runtime """ super(PyDMChartingDisplay, self).__init__(parent=parent, args=args, macros=macros) self.channel_map = dict() self.setWindowTitle("PyDM Charting Tool") self.main_layout = QVBoxLayout() self.body_layout = QVBoxLayout() self.pv_layout = QHBoxLayout() self.pv_name_line_edt = QLineEdit() self.pv_name_line_edt.setAcceptDrops(True) self.pv_name_line_edt.installEventFilter(self) self.pv_protocol_cmb = QComboBox() self.pv_protocol_cmb.addItems(["ca://", "archive://"]) self.pv_connect_push_btn = QPushButton("Connect") self.pv_connect_push_btn.clicked.connect(self.add_curve) self.tab_panel = QTabWidget() self.tab_panel.setMaximumWidth(450) self.curve_settings_tab = QWidget() self.chart_settings_tab = QWidget() self.charting_layout = QHBoxLayout() self.chart = PyDMTimePlot(plot_by_timestamps=False, plot_display=self) self.chart.setPlotTitle("Time Plot") self.splitter = QSplitter() self.curve_settings_layout = QVBoxLayout() self.curve_settings_layout.setAlignment(Qt.AlignTop) self.curve_settings_layout.setSizeConstraint(QLayout.SetMinAndMaxSize) self.curve_settings_layout.setSpacing(5) self.crosshair_settings_layout = QVBoxLayout() self.crosshair_settings_layout.setAlignment(Qt.AlignTop) self.crosshair_settings_layout.setSpacing(5) self.enable_crosshair_chk = QCheckBox("Enable Crosshair") self.cross_hair_coord_lbl = QLabel() self.curve_settings_inner_frame = QFrame() self.curve_settings_inner_frame.setLayout(self.curve_settings_layout) self.curve_settings_scroll = QScrollArea() self.curve_settings_scroll.setVerticalScrollBarPolicy( Qt.ScrollBarAsNeeded) self.curve_settings_scroll.setWidget(self.curve_settings_inner_frame) self.curves_tab_layout = QHBoxLayout() self.curves_tab_layout.addWidget(self.curve_settings_scroll) self.enable_crosshair_chk.setChecked(False) self.enable_crosshair_chk.clicked.connect( self.handle_enable_crosshair_checkbox_clicked) self.enable_crosshair_chk.clicked.emit(False) self.chart_settings_layout = QVBoxLayout() self.chart_settings_layout.setAlignment(Qt.AlignTop) self.chart_layout = QVBoxLayout() self.chart_panel = QWidget() self.chart_control_layout = QHBoxLayout() self.chart_control_layout.setAlignment(Qt.AlignHCenter) self.chart_control_layout.setSpacing(10) self.view_all_btn = QPushButton("View All") self.view_all_btn.clicked.connect(self.handle_view_all_button_clicked) self.view_all_btn.setEnabled(False) self.auto_scale_btn = QPushButton("Auto Scale") self.auto_scale_btn.clicked.connect(self.handle_auto_scale_btn_clicked) self.auto_scale_btn.setEnabled(False) self.reset_chart_btn = QPushButton("Reset") self.reset_chart_btn.clicked.connect( self.handle_reset_chart_btn_clicked) self.reset_chart_btn.setEnabled(False) self.resume_chart_text = "Resume" self.pause_chart_text = "Pause" self.pause_chart_btn = QPushButton(self.pause_chart_text) self.pause_chart_btn.clicked.connect( self.handle_pause_chart_btn_clicked) self.title_settings_layout = QVBoxLayout() self.title_settings_layout.setSpacing(10) self.title_settings_grpbx = QGroupBox() self.title_settings_grpbx.setFixedHeight(150) self.import_data_btn = QPushButton("Import Data...") self.import_data_btn.clicked.connect( self.handle_import_data_btn_clicked) self.export_data_btn = QPushButton("Export Data...") self.export_data_btn.clicked.connect( self.handle_export_data_btn_clicked) self.chart_title_lbl = QLabel(text="Chart Title") self.chart_title_line_edt = QLineEdit() self.chart_title_line_edt.setText(self.chart.getPlotTitle()) self.chart_title_line_edt.textChanged.connect( self.handle_title_text_changed) self.chart_change_axis_settings_btn = QPushButton( text="Change Axis Settings...") self.chart_change_axis_settings_btn.clicked.connect( self.handle_change_axis_settings_clicked) self.update_datetime_timer = QTimer(self) self.update_datetime_timer.timeout.connect( self.handle_update_datetime_timer_timeout) self.chart_sync_mode_layout = QVBoxLayout() self.chart_sync_mode_layout.setSpacing(5) self.chart_sync_mode_grpbx = QGroupBox("Data Sampling Mode") self.chart_sync_mode_grpbx.setFixedHeight(80) self.chart_sync_mode_sync_radio = QRadioButton("Synchronous") self.chart_sync_mode_async_radio = QRadioButton("Asynchronous") self.chart_sync_mode_async_radio.setChecked(True) self.graph_drawing_settings_layout = QVBoxLayout() self.chart_redraw_rate_lbl = QLabel("Redraw Rate (Hz)") self.chart_redraw_rate_spin = QSpinBox() self.chart_redraw_rate_spin.setRange(MIN_REDRAW_RATE_HZ, MAX_REDRAW_RATE_HZ) self.chart_redraw_rate_spin.setValue(DEFAULT_REDRAW_RATE_HZ) self.chart_redraw_rate_spin.valueChanged.connect( self.handle_redraw_rate_changed) self.chart_data_sampling_rate_lbl = QLabel( "Asynchronous Data Sampling Rate (Hz)") self.chart_data_async_sampling_rate_spin = QSpinBox() self.chart_data_async_sampling_rate_spin.setRange( MIN_DATA_SAMPLING_RATE_HZ, MAX_DATA_SAMPLING_RATE_HZ) self.chart_data_async_sampling_rate_spin.setValue( DEFAULT_DATA_SAMPLING_RATE_HZ) self.chart_data_async_sampling_rate_spin.valueChanged.connect( self.handle_data_sampling_rate_changed) self.chart_data_sampling_rate_lbl.hide() self.chart_data_async_sampling_rate_spin.hide() self.chart_limit_time_span_layout = QHBoxLayout() self.chart_limit_time_span_layout.setSpacing(5) self.limit_time_plan_text = "Limit Time Span" self.chart_limit_time_span_chk = QCheckBox(self.limit_time_plan_text) self.chart_limit_time_span_chk.hide() self.chart_limit_time_span_lbl = QLabel("Hours : Minutes : Seconds") self.chart_limit_time_span_hours_line_edt = QLineEdit() self.chart_limit_time_span_minutes_line_edt = QLineEdit() self.chart_limit_time_span_seconds_line_edt = QLineEdit() self.chart_limit_time_span_activate_btn = QPushButton("Apply") self.chart_limit_time_span_activate_btn.setDisabled(True) self.chart_ring_buffer_size_lbl = QLabel("Ring Buffer Size") self.chart_ring_buffer_size_edt = QLineEdit() self.chart_ring_buffer_size_edt.installEventFilter(self) self.chart_ring_buffer_size_edt.textChanged.connect( self.handle_buffer_size_changed) self.chart_ring_buffer_size_edt.setText(str(DEFAULT_BUFFER_SIZE)) self.show_legend_chk = QCheckBox("Show Legend") self.show_legend_chk.setChecked(self.chart.showLegend) self.show_legend_chk.clicked.connect( self.handle_show_legend_checkbox_clicked) self.graph_background_color_layout = QFormLayout() self.background_color_lbl = QLabel("Graph Background Color ") self.background_color_btn = QPushButton() self.background_color_btn.setStyleSheet( "background-color: " + self.chart.getBackgroundColor().name()) self.background_color_btn.setContentsMargins(10, 0, 5, 5) self.background_color_btn.setMaximumWidth(20) self.background_color_btn.clicked.connect( self.handle_background_color_button_clicked) self.axis_settings_layout = QVBoxLayout() self.axis_settings_layout.setSpacing(5) self.show_x_grid_chk = QCheckBox("Show x Grid") self.show_x_grid_chk.setChecked(self.chart.showXGrid) self.show_x_grid_chk.clicked.connect( self.handle_show_x_grid_checkbox_clicked) self.show_y_grid_chk = QCheckBox("Show y Grid") self.show_y_grid_chk.setChecked(self.chart.showYGrid) self.show_y_grid_chk.clicked.connect( self.handle_show_y_grid_checkbox_clicked) self.axis_color_lbl = QLabel("Axis and Grid Color") self.axis_color_lbl.setEnabled(False) self.axis_color_btn = QPushButton() self.axis_color_btn.setStyleSheet("background-color: " + DEFAULT_CHART_AXIS_COLOR.name()) self.axis_color_btn.setContentsMargins(10, 0, 5, 5) self.axis_color_btn.setMaximumWidth(20) self.axis_color_btn.clicked.connect( self.handle_axis_color_button_clicked) self.axis_color_btn.setEnabled(False) self.grid_opacity_lbl = QLabel("Grid Opacity") self.grid_opacity_lbl.setEnabled(False) self.grid_opacity_slr = QSlider(Qt.Horizontal) self.grid_opacity_slr.setFocusPolicy(Qt.StrongFocus) self.grid_opacity_slr.setRange(0, 10) self.grid_opacity_slr.setValue(5) self.grid_opacity_slr.setTickInterval(1) self.grid_opacity_slr.setSingleStep(1) self.grid_opacity_slr.setTickPosition(QSlider.TicksBelow) self.grid_opacity_slr.valueChanged.connect( self.handle_grid_opacity_slider_mouse_release) self.grid_opacity_slr.setEnabled(False) self.reset_chart_settings_btn = QPushButton("Reset Chart Settings") self.reset_chart_settings_btn.clicked.connect( self.handle_reset_chart_settings_btn_clicked) self.curve_checkbox_panel = QWidget() self.graph_drawing_settings_grpbx = QGroupBox() self.graph_drawing_settings_grpbx.setFixedHeight(270) self.axis_settings_grpbx = QGroupBox() self.axis_settings_grpbx.setFixedHeight(180) self.app = QApplication.instance() self.setup_ui() self.curve_settings_disp = None self.axis_settings_disp = None self.chart_data_export_disp = None self.chart_data_import_disp = None self.grid_alpha = 5 self.time_span_limit_hours = None self.time_span_limit_minutes = None self.time_span_limit_seconds = None self.data_sampling_mode = ASYNC_DATA_SAMPLING def minimumSizeHint(self): """ The minimum recommended size of the main window. """ return QSize(1490, 800) def ui_filepath(self): """ The path to the UI file created by Qt Designer, if applicable. """ # No UI file is being used return None def ui_filename(self): """ The name the UI file created by Qt Designer, if applicable. """ # No UI file is being used return None def setup_ui(self): """ Initialize the widgets and layouts. """ self.setLayout(self.main_layout) self.pv_layout.addWidget(self.pv_protocol_cmb) self.pv_layout.addWidget(self.pv_name_line_edt) self.pv_layout.addWidget(self.pv_connect_push_btn) QTimer.singleShot(0, self.pv_name_line_edt.setFocus) self.curve_settings_tab.setLayout(self.curves_tab_layout) self.chart_settings_tab.setLayout(self.chart_settings_layout) self.setup_chart_settings_layout() self.tab_panel.addTab(self.curve_settings_tab, "Curves") self.tab_panel.addTab(self.chart_settings_tab, "Chart") self.tab_panel.hide() self.crosshair_settings_layout.addWidget(self.enable_crosshair_chk) self.crosshair_settings_layout.addWidget(self.cross_hair_coord_lbl) self.chart_control_layout.addWidget(self.auto_scale_btn) self.chart_control_layout.addWidget(self.view_all_btn) self.chart_control_layout.addWidget(self.reset_chart_btn) self.chart_control_layout.addWidget(self.pause_chart_btn) self.chart_control_layout.addLayout(self.crosshair_settings_layout) self.chart_control_layout.addWidget(self.import_data_btn) self.chart_control_layout.addWidget(self.export_data_btn) self.chart_control_layout.setStretch(4, 15) self.chart_control_layout.insertSpacing(5, 350) self.chart_layout.addWidget(self.chart) self.chart_layout.addLayout(self.chart_control_layout) self.chart_panel.setLayout(self.chart_layout) self.splitter.addWidget(self.chart_panel) self.splitter.addWidget(self.tab_panel) self.splitter.setStretchFactor(0, 0) self.splitter.setStretchFactor(1, 1) self.charting_layout.addWidget(self.splitter) self.body_layout.addLayout(self.pv_layout) self.body_layout.addLayout(self.charting_layout) self.body_layout.addLayout(self.chart_control_layout) self.main_layout.addLayout(self.body_layout) self.enable_chart_control_buttons(False) def setup_chart_settings_layout(self): self.chart_sync_mode_sync_radio.toggled.connect( partial(self.handle_sync_mode_radio_toggle, self.chart_sync_mode_sync_radio)) self.chart_sync_mode_async_radio.toggled.connect( partial(self.handle_sync_mode_radio_toggle, self.chart_sync_mode_async_radio)) self.title_settings_layout.addWidget(self.chart_title_lbl) self.title_settings_layout.addWidget(self.chart_title_line_edt) self.title_settings_layout.addWidget(self.show_legend_chk) self.title_settings_layout.addWidget( self.chart_change_axis_settings_btn) self.title_settings_grpbx.setLayout(self.title_settings_layout) self.chart_settings_layout.addWidget(self.title_settings_grpbx) self.chart_sync_mode_layout.addWidget(self.chart_sync_mode_sync_radio) self.chart_sync_mode_layout.addWidget(self.chart_sync_mode_async_radio) self.chart_sync_mode_grpbx.setLayout(self.chart_sync_mode_layout) self.chart_settings_layout.addWidget(self.chart_sync_mode_grpbx) self.chart_settings_layout.addWidget(self.chart_sync_mode_grpbx) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_lbl) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_hours_line_edt) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_minutes_line_edt) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_seconds_line_edt) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_activate_btn) self.chart_limit_time_span_lbl.hide() self.chart_limit_time_span_hours_line_edt.hide() self.chart_limit_time_span_minutes_line_edt.hide() self.chart_limit_time_span_seconds_line_edt.hide() self.chart_limit_time_span_activate_btn.hide() self.chart_limit_time_span_hours_line_edt.textChanged.connect( self.handle_time_span_edt_text_changed) self.chart_limit_time_span_minutes_line_edt.textChanged.connect( self.handle_time_span_edt_text_changed) self.chart_limit_time_span_seconds_line_edt.textChanged.connect( self.handle_time_span_edt_text_changed) self.chart_limit_time_span_chk.clicked.connect( self.handle_limit_time_span_checkbox_clicked) self.chart_limit_time_span_activate_btn.clicked.connect( self.handle_chart_limit_time_span_activate_btn_clicked) self.chart_limit_time_span_activate_btn.installEventFilter(self) self.graph_background_color_layout.addRow(self.background_color_lbl, self.background_color_btn) self.graph_drawing_settings_layout.addLayout( self.graph_background_color_layout) self.graph_drawing_settings_layout.addWidget( self.chart_redraw_rate_lbl) self.graph_drawing_settings_layout.addWidget( self.chart_redraw_rate_spin) self.graph_drawing_settings_layout.addWidget( self.chart_data_sampling_rate_lbl) self.graph_drawing_settings_layout.addWidget( self.chart_data_async_sampling_rate_spin) self.graph_drawing_settings_layout.addWidget( self.chart_limit_time_span_chk) self.graph_drawing_settings_layout.addLayout( self.chart_limit_time_span_layout) self.graph_drawing_settings_layout.addWidget( self.chart_ring_buffer_size_lbl) self.graph_drawing_settings_layout.addWidget( self.chart_ring_buffer_size_edt) self.graph_drawing_settings_grpbx.setLayout( self.graph_drawing_settings_layout) self.axis_settings_layout.addWidget(self.show_x_grid_chk) self.axis_settings_layout.addWidget(self.show_y_grid_chk) self.axis_settings_layout.addWidget(self.axis_color_lbl) self.axis_settings_layout.addWidget(self.axis_color_btn) self.axis_settings_layout.addWidget(self.grid_opacity_lbl) self.axis_settings_layout.addWidget(self.grid_opacity_slr) self.axis_settings_grpbx.setLayout(self.axis_settings_layout) self.chart_settings_layout.addWidget(self.graph_drawing_settings_grpbx) self.chart_settings_layout.addWidget(self.axis_settings_grpbx) self.chart_settings_layout.addWidget(self.reset_chart_settings_btn) self.chart_sync_mode_async_radio.toggled.emit(True) self.update_datetime_timer.start(1000) def eventFilter(self, obj, event): """ Handle key and mouse events for any applicable widget. Parameters ---------- obj : QWidget The current widget that accepts the event event : QEvent The key or mouse event to handle Returns ------- True if the event was handled successfully; False otherwise """ if obj == self.pv_name_line_edt and event.type() == QEvent.KeyPress: if event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return: self.add_curve() return True elif obj == self.chart_limit_time_span_activate_btn and event.type( ) == QEvent.KeyPress: if event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return: self.handle_chart_limit_time_span_activate_btn_clicked() return True elif obj == self.chart_ring_buffer_size_edt: if event.type() == QEvent.KeyPress and (event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return) or \ event.type() == QEvent.FocusOut: try: buffer_size = int(self.chart_ring_buffer_size_edt.text()) if buffer_size < MINIMUM_BUFFER_SIZE: self.chart_ring_buffer_size_edt.setText( str(MINIMUM_BUFFER_SIZE)) except ValueError: display_message_box(QMessageBox.Critical, "Invalid Values", "Only integer values are accepted.") return True return super(PyDMChartingDisplay, self).eventFilter(obj, event) def add_curve(self): """ Add a new curve to the chart. """ pv_name = self._get_full_pv_name(self.pv_name_line_edt.text()) color = random_color() for k, v in self.channel_map.items(): if color == v.color: color = random_color() self.add_y_channel(pv_name=pv_name, curve_name=pv_name, color=color) def handle_enable_crosshair_checkbox_clicked(self, is_checked): self.chart.enableCrosshair(is_checked) self.cross_hair_coord_lbl.setVisible(is_checked) def add_y_channel(self, pv_name, curve_name, color, line_style=Qt.SolidLine, line_width=2, symbol=None, symbol_size=None): if pv_name in self.channel_map: logger.error("'{0}' has already been added.".format(pv_name)) return curve = self.chart.addYChannel(y_channel=pv_name, name=curve_name, color=color, lineStyle=line_style, lineWidth=line_width, symbol=symbol, symbolSize=symbol_size) self.channel_map[pv_name] = curve self.generate_pv_controls(pv_name, color) self.enable_chart_control_buttons() self.app.establish_widget_connections(self) def generate_pv_controls(self, pv_name, curve_color): """ Generate a set of widgets to manage the appearance of a curve. The set of widgets includes: 1. A checkbox which shows the curve on the chart if checked, and hide the curve if not checked 2. Two buttons -- Modify... and Remove. Modify... will bring up the Curve Settings dialog. Remove will delete the curve from the chart This set of widgets will be hidden initially, until the first curve is plotted. Parameters ---------- pv_name: str The name of the PV the current curve is being plotted for curve_color : QColor The color of the curve to paint for the checkbox label to help the user track the curve to the checkbox """ checkbox = QCheckBox() checkbox.setObjectName(pv_name) palette = checkbox.palette() palette.setColor(QPalette.Active, QPalette.WindowText, curve_color) checkbox.setPalette(palette) display_name = pv_name.split("://")[1] if len(display_name) > MAX_DISPLAY_PV_NAME_LENGTH: # Only display max allowed number of characters of the PV Name display_name = display_name[:int(MAX_DISPLAY_PV_NAME_LENGTH / 2) - 1] + "..." + \ display_name[-int(MAX_DISPLAY_PV_NAME_LENGTH / 2) + 2:] checkbox.setText(display_name) data_text = QLabel() data_text.setObjectName(pv_name) data_text.setPalette(palette) checkbox.setChecked(True) checkbox.clicked.connect( partial(self.handle_curve_chkbox_toggled, checkbox)) curve_btn_layout = QHBoxLayout() modify_curve_btn = QPushButton("Modify...") modify_curve_btn.setObjectName(pv_name) modify_curve_btn.setMaximumWidth(100) modify_curve_btn.clicked.connect( partial(self.display_curve_settings_dialog, pv_name)) focus_curve_btn = QPushButton("Focus") focus_curve_btn.setObjectName(pv_name) focus_curve_btn.setMaximumWidth(100) focus_curve_btn.clicked.connect(partial(self.focus_curve, pv_name)) annotate_curve_btn = QPushButton("Annotate...") annotate_curve_btn.setObjectName(pv_name) annotate_curve_btn.setMaximumWidth(100) annotate_curve_btn.clicked.connect( partial(self.annotate_curve, pv_name)) remove_curve_btn = QPushButton("Remove") remove_curve_btn.setObjectName(pv_name) remove_curve_btn.setMaximumWidth(100) remove_curve_btn.clicked.connect(partial(self.remove_curve, pv_name)) curve_btn_layout.addWidget(modify_curve_btn) curve_btn_layout.addWidget(focus_curve_btn) curve_btn_layout.addWidget(annotate_curve_btn) curve_btn_layout.addWidget(remove_curve_btn) individual_curve_layout = QVBoxLayout() individual_curve_layout.addWidget(checkbox) individual_curve_layout.addWidget(data_text) individual_curve_layout.addLayout(curve_btn_layout) size_policy = QSizePolicy() size_policy.setVerticalPolicy(QSizePolicy.Fixed) individual_curve_grpbx = QGroupBox() individual_curve_grpbx.setSizePolicy(size_policy) individual_curve_grpbx.setObjectName(pv_name) individual_curve_grpbx.setLayout(individual_curve_layout) self.curve_settings_layout.addWidget(individual_curve_grpbx) self.tab_panel.show() def handle_curve_chkbox_toggled(self, checkbox): """ Handle a checkbox's checked and unchecked events. If a checkbox is checked, find the curve from the channel map. If found, re-draw the curve with its previous appearance settings. If a checkbox is unchecked, remove the curve from the chart, but keep the cached data in the channel map. Parameters ---------- checkbox : QCheckBox The current checkbox being toggled """ pv_name = self._get_full_pv_name(checkbox.text()) if checkbox.isChecked(): curve = self.channel_map.get(pv_name, None) if curve: self.chart.addLegendItem(curve, pv_name, self.show_legend_chk.isChecked()) curve.show() else: curve = self.chart.findCurve(pv_name) if curve: curve.hide() self.chart.removeLegendItem(pv_name) def display_curve_settings_dialog(self, pv_name): """ Bring up the Curve Settings dialog to modify the appearance of a curve. Parameters ---------- pv_name : str The name of the PV the curve is being plotted for """ self.curve_settings_disp = CurveSettingsDisplay(self, pv_name) self.curve_settings_disp.show() def focus_curve(self, pv_name): curve = self.chart.findCurve(pv_name) if curve: self.chart.plotItem.setYRange(curve.minY, curve.maxY, padding=0) def annotate_curve(self, pv_name): curve = self.chart.findCurve(pv_name) if curve: annot = TextItem( html= '<div style="text-align: center"><span style="color: #FFF;">This is the' '</span><br><span style="color: #FF0; font-size: 16pt;">PEAK</span></div>', anchor=(-0.3, 0.5), border='w', fill=(0, 0, 255, 100)) annot = TextItem("test", anchor=(-0.3, 0.5)) self.chart.annotateCurve(curve, annot) def remove_curve(self, pv_name): """ Remove a curve from the chart permanently. This will also clear the channel map cache from retaining the removed curve's appearance settings. Parameters ---------- pv_name : str The name of the PV the curve is being plotted for """ curve = self.chart.findCurve(pv_name) if curve: self.chart.removeYChannel(curve) del self.channel_map[pv_name] self.chart.removeLegendItem(pv_name) widgets = self.findChildren( (QCheckBox, QLabel, QPushButton, QGroupBox), pv_name) for w in widgets: w.deleteLater() if len(self.chart.getCurves()) < 1: self.enable_chart_control_buttons(False) self.show_legend_chk.setChecked(False) def handle_title_text_changed(self, new_text): self.chart.setPlotTitle(new_text) def handle_change_axis_settings_clicked(self): self.axis_settings_disp = AxisSettingsDisplay(self) self.axis_settings_disp.show() def handle_limit_time_span_checkbox_clicked(self, is_checked): self.chart_limit_time_span_lbl.setVisible(is_checked) self.chart_limit_time_span_hours_line_edt.setVisible(is_checked) self.chart_limit_time_span_minutes_line_edt.setVisible(is_checked) self.chart_limit_time_span_seconds_line_edt.setVisible(is_checked) self.chart_limit_time_span_activate_btn.setVisible(is_checked) self.chart_ring_buffer_size_lbl.setDisabled(is_checked) self.chart_ring_buffer_size_edt.setDisabled(is_checked) if not is_checked: self.chart_limit_time_span_chk.setText(self.limit_time_plan_text) def handle_time_span_edt_text_changed(self, new_text): try: self.time_span_limit_hours = int( self.chart_limit_time_span_hours_line_edt.text()) self.time_span_limit_minutes = int( self.chart_limit_time_span_minutes_line_edt.text()) self.time_span_limit_seconds = int( self.chart_limit_time_span_seconds_line_edt.text()) except ValueError as e: self.time_span_limit_hours = None self.time_span_limit_minutes = None self.time_span_limit_seconds = None if self.time_span_limit_hours is not None and self.time_span_limit_minutes is not None and \ self.time_span_limit_seconds is not None: self.chart_limit_time_span_activate_btn.setEnabled(True) else: self.chart_limit_time_span_activate_btn.setEnabled(False) def handle_chart_limit_time_span_activate_btn_clicked(self): if self.time_span_limit_hours is None or self.time_span_limit_minutes is None or \ self.time_span_limit_seconds is None: display_message_box( QMessageBox.Critical, "Invalid Values", "Hours, minutes, and seconds expect only integer values.") else: timeout_milliseconds = (self.time_span_limit_hours * 3600 + self.time_span_limit_minutes * 60 + self.time_span_limit_seconds) * 1000 self.chart.setTimeSpan(timeout_milliseconds / 1000.0) self.chart_ring_buffer_size_edt.setText( str(self.chart.getBufferSize())) def handle_buffer_size_changed(self, new_buffer_size): try: if new_buffer_size and int(new_buffer_size) > MINIMUM_BUFFER_SIZE: self.chart.setBufferSize(new_buffer_size) except ValueError: display_message_box(QMessageBox.Critical, "Invalid Values", "Only integer values are accepted.") def handle_redraw_rate_changed(self, new_redraw_rate): self.chart.maxRedrawRate = new_redraw_rate def handle_data_sampling_rate_changed(self, new_data_sampling_rate): # The chart expects the value in milliseconds sampling_rate_seconds = 1 / new_data_sampling_rate self.chart.setUpdateInterval(sampling_rate_seconds) def handle_background_color_button_clicked(self): selected_color = QColorDialog.getColor() self.chart.setBackgroundColor(selected_color) self.background_color_btn.setStyleSheet("background-color: " + selected_color.name()) def handle_axis_color_button_clicked(self): selected_color = QColorDialog.getColor() self.chart.setAxisColor(selected_color) self.axis_color_btn.setStyleSheet("background-color: " + selected_color.name()) def handle_grid_opacity_slider_mouse_release(self): self.grid_alpha = float(self.grid_opacity_slr.value()) / 10.0 self.chart.setShowXGrid(self.show_x_grid_chk.isChecked(), self.grid_alpha) self.chart.setShowYGrid(self.show_y_grid_chk.isChecked(), self.grid_alpha) def handle_show_x_grid_checkbox_clicked(self, is_checked): self.chart.setShowXGrid(is_checked, self.grid_alpha) self.axis_color_lbl.setEnabled(is_checked or self.show_y_grid_chk.isChecked()) self.axis_color_btn.setEnabled(is_checked or self.show_y_grid_chk.isChecked()) self.grid_opacity_lbl.setEnabled(is_checked or self.show_y_grid_chk.isChecked()) self.grid_opacity_slr.setEnabled(is_checked or self.show_y_grid_chk.isChecked()) def handle_show_y_grid_checkbox_clicked(self, is_checked): self.chart.setShowYGrid(is_checked, self.grid_alpha) self.axis_color_lbl.setEnabled(is_checked or self.show_x_grid_chk.isChecked()) self.axis_color_btn.setEnabled(is_checked or self.show_x_grid_chk.isChecked()) self.grid_opacity_lbl.setEnabled(is_checked or self.show_x_grid_chk.isChecked()) self.grid_opacity_slr.setEnabled(is_checked or self.show_x_grid_chk.isChecked()) def handle_show_legend_checkbox_clicked(self, is_checked): self.chart.setShowLegend(is_checked) def handle_export_data_btn_clicked(self): self.chart_data_export_disp = ChartDataExportDisplay(self) self.chart_data_export_disp.show() def handle_import_data_btn_clicked(self): open_file_info = QFileDialog.getOpenFileName(self, caption="Save File", filter="*." + IMPORT_FILE_FORMAT) open_file_name = open_file_info[0] if open_file_name: importer = SettingsImporter(self) importer.import_settings(open_file_name) def handle_sync_mode_radio_toggle(self, radio_btn): if radio_btn.isChecked(): if radio_btn.text() == "Synchronous": self.data_sampling_mode = SYNC_DATA_SAMPLING self.chart_data_sampling_rate_lbl.hide() self.chart_data_async_sampling_rate_spin.hide() self.chart.resetTimeSpan() self.chart_limit_time_span_chk.setChecked(False) self.chart_limit_time_span_chk.clicked.emit(False) self.chart_limit_time_span_chk.hide() self.graph_drawing_settings_grpbx.setFixedHeight(180) self.chart.setUpdatesAsynchronously(False) elif radio_btn.text() == "Asynchronous": self.data_sampling_mode = ASYNC_DATA_SAMPLING self.chart_data_sampling_rate_lbl.show() self.chart_data_async_sampling_rate_spin.show() self.chart_limit_time_span_chk.show() self.graph_drawing_settings_grpbx.setFixedHeight(270) self.chart.setUpdatesAsynchronously(True) self.app.establish_widget_connections(self) def handle_auto_scale_btn_clicked(self): self.chart.resetAutoRangeX() self.chart.resetAutoRangeY() def handle_view_all_button_clicked(self): self.chart.getViewBox().autoRange() def handle_pause_chart_btn_clicked(self): if self.chart.pausePlotting(): self.pause_chart_btn.setText(self.pause_chart_text) else: self.pause_chart_btn.setText(self.resume_chart_text) def handle_reset_chart_btn_clicked(self): self.chart.getViewBox().setXRange(DEFAULT_X_MIN, 0) self.chart.resetAutoRangeY() @Slot() def handle_reset_chart_settings_btn_clicked(self): self.chart_ring_buffer_size_edt.setText(str(DEFAULT_BUFFER_SIZE)) self.chart_redraw_rate_spin.setValue(DEFAULT_REDRAW_RATE_HZ) self.chart_data_async_sampling_rate_spin.setValue( DEFAULT_DATA_SAMPLING_RATE_HZ) self.chart_data_sampling_rate_lbl.hide() self.chart_data_async_sampling_rate_spin.hide() self.chart_sync_mode_async_radio.setChecked(True) self.chart_sync_mode_async_radio.toggled.emit(True) self.chart_limit_time_span_chk.setChecked(False) self.chart_limit_time_span_chk.setText(self.limit_time_plan_text) self.chart_limit_time_span_chk.clicked.emit(False) self.chart.setUpdatesAsynchronously(True) self.chart.resetTimeSpan() self.chart.resetUpdateInterval() self.chart.setBufferSize(DEFAULT_BUFFER_SIZE) self.chart.setBackgroundColor(DEFAULT_CHART_BACKGROUND_COLOR) self.background_color_btn.setStyleSheet( "background-color: " + DEFAULT_CHART_BACKGROUND_COLOR.name()) self.chart.setAxisColor(DEFAULT_CHART_AXIS_COLOR) self.axis_color_btn.setStyleSheet("background-color: " + DEFAULT_CHART_AXIS_COLOR.name()) self.grid_opacity_slr.setValue(5) self.show_x_grid_chk.setChecked(False) self.show_x_grid_chk.clicked.emit(False) self.show_y_grid_chk.setChecked(False) self.show_y_grid_chk.clicked.emit(False) self.show_legend_chk.setChecked(False) self.chart.setShowXGrid(False) self.chart.setShowYGrid(False) self.chart.setShowLegend(False) def enable_chart_control_buttons(self, enabled=True): self.auto_scale_btn.setEnabled(enabled) self.view_all_btn.setEnabled(enabled) self.reset_chart_btn.setEnabled(enabled) self.pause_chart_btn.setText(self.pause_chart_text) self.pause_chart_btn.setEnabled(enabled) self.export_data_btn.setEnabled(enabled) def _get_full_pv_name(self, pv_name): """ Append the protocol to the PV Name. Parameters ---------- pv_name : str The name of the PV the curve is being plotted for """ if pv_name and "://" not in pv_name: pv_name = ''.join([self.pv_protocol_cmb.currentText(), pv_name]) return pv_name def handle_update_datetime_timer_timeout(self): current_label = self.chart.getBottomAxisLabel() new_label = "Current Time: " + PyDMChartingDisplay.get_current_datetime( ) if X_AXIS_LABEL_SEPARATOR in current_label: current_label = current_label[current_label. find(X_AXIS_LABEL_SEPARATOR) + len(X_AXIS_LABEL_SEPARATOR):] new_label += X_AXIS_LABEL_SEPARATOR + current_label self.chart.setLabel("bottom", text=new_label) def update_curve_data(self, curve): """ Determine if the PV is active. If not, disable the related PV controls. If the PV is active, update the PV controls' states. Parameters ---------- curve : PlotItem A PlotItem, i.e. a plot, to draw on the chart. """ pv_name = curve.name() max_x = self.chart.getViewBox().viewRange()[1][0] max_y = self.chart.getViewBox().viewRange()[1][1] current_y = curve.data_buffer[1, -1] widgets = self.findChildren((QCheckBox, QLabel, QPushButton), pv_name) for w in widgets: if np.isnan(current_y): if isinstance(w, QCheckBox): w.setChecked(False) else: if isinstance(w, QCheckBox) and not w.isEnabled(): w.setChecked(True) if isinstance(w, QLabel): w.clear() w.setText( "(yMin = {0:.3f}, yMax = {1:.3f}) y = {2:.3f}".format( max_x, max_y, current_y)) w.show() w.setEnabled(not np.isnan(current_y)) if isinstance(w, QPushButton) and w.text() == "Remove": # Enable the Remove button to make removing inactive PVs possible anytime w.setEnabled(True) def show_mouse_coordinates(self, x, y): self.cross_hair_coord_lbl.clear() self.cross_hair_coord_lbl.setText("x = {0:.3f}, y = {1:.3f}".format( x, y)) @staticmethod def get_current_datetime(): current_date = datetime.datetime.now().strftime("%b %d, %Y") current_time = datetime.datetime.now().strftime("%H:%M:%S") current_datetime = current_time + ' (' + current_date + ')' return current_datetime @property def gridAlpha(self): return self.grid_alpha
class _ImageRaster2DSpectralWidget(_DatumWidget): MODE_SUM = 'sum' MODE_MAX = 'max' MODE_SINGLE = 'single' MODE_RANGE = 'range' def __init__(self, controller, datum=None, parent=None): self._datum = datum _DatumWidget.__init__(self, ImageRaster2DSpectral, controller, datum, parent) def _init_ui(self): # Widgets self._rdb_sum = QRadioButton("Sum") self._rdb_sum.setChecked(True) self._rdb_max = QRadioButton("Maximum") self._rdb_single = QRadioButton("Single") self._rdb_range = QRadioButton("Range") self._sld_start = QSlider(Qt.Horizontal) self._sld_start.setTickPosition(QSlider.TicksBelow) self._sld_start.setEnabled(False) self._sld_end = QSlider(Qt.Horizontal) self._sld_end.setTickPosition(QSlider.TicksBelow) self._sld_end.setEnabled(False) self._wdg_imageraster2d = self._create_imageraster2d_widget() self._wdg_analysis = self._create_analysis1d_widget() # Layouts layout = _DatumWidget._init_ui(self) sublayout = QHBoxLayout() sublayout.addWidget(self._rdb_sum) sublayout.addWidget(self._rdb_max) sublayout.addWidget(self._rdb_single) sublayout.addWidget(self._rdb_range) layout.addLayout(sublayout) sublayout = QFormLayout() sublayout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) # Fix for Mac OS sublayout.addRow('Channels (Start)', self._sld_start) sublayout.addRow('Channels (End)', self._sld_end) layout.addLayout(sublayout) splitter = QSplitter() splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) splitter.addWidget(self._wdg_imageraster2d) splitter.addWidget(self._wdg_analysis) layout.addWidget(splitter) # Signals self._rdb_sum.toggled.connect(self._on_mode_sum) self._rdb_max.toggled.connect(self._on_mode_max) self._rdb_single.toggled.connect(self._on_mode_single) self._rdb_range.toggled.connect(self._on_mode_range) self._sld_start.valueChanged.connect(self._on_slide_start) self._sld_end.valueChanged.connect(self._on_slide_end) self._wdg_imageraster2d.valueSelected.connect(self._on_value_selected) # Defaults self.setMode(self.MODE_SUM) return layout def _create_analysis1d_widget(self): raise NotImplementedError def _create_imageraster2d_widget(self): raise NotImplementedError def _on_mode_sum(self, checked): if checked: self.setMode(self.MODE_SUM) def _on_mode_max(self, checked): if checked: self.setMode(self.MODE_MAX) def _on_mode_single(self, checked): if checked: self.setMode(self.MODE_SINGLE) def _on_mode_range(self, checked): if checked: self.setMode(self.MODE_RANGE) def _update_data(self, mode=None): if mode is None: mode = self.mode() if mode == self.MODE_SUM: self._update_mode_sum() elif mode == self.MODE_MAX: self._update_mode_max() elif mode == self.MODE_SINGLE: self._update_mode_single() elif mode == self.MODE_RANGE: self._update_mode_range() def _update_mode_sum(self): if self._datum is None: return subdatum = np.sum(self._datum, 2) self._wdg_imageraster2d.setDatum(subdatum) def _update_mode_max(self): if self._datum is None: return subdatum = np.amax(self._datum, 2) self._wdg_imageraster2d.setDatum(subdatum) def _update_mode_single(self): if self._datum is None: return channel = self._sld_start.value() subdatum = self._datum[:, :, channel] self._wdg_imageraster2d.setDatum(subdatum) def _update_mode_range(self): if self._datum is None: return start = self._sld_start.value() end = self._sld_end.value() start2 = min(start, end) end2 = max(start, end) subdatum = np.sum(self._datum[:, :, start2:end2 + 1], 2) self._wdg_imageraster2d.setDatum(subdatum) def _on_slide_start(self, channel): if self._rdb_single.isChecked(): self._update_mode_single() elif self._rdb_range.isChecked(): self._update_mode_range() def _on_slide_end(self, channel): self._update_mode_range() def _on_value_selected(self, x, y): if self._datum is None: return subdatum = self._datum[x, y] self._wdg_analysis.setDatum(subdatum.view(Analysis1D)) def setDatum(self, datum): _DatumWidget.setDatum(self, datum) self._datum = datum maximum = datum.channels - 1 if datum is not None else 0 self._sld_start.setMaximum(maximum) self._sld_end.setMaximum(maximum) self._update_data() def setMode(self, mode): rsum = rmax = rsingle = rrange = False sstart = send = False if mode == self.MODE_SUM: rsum = True elif mode == self.MODE_MAX: rmax = True elif mode == self.MODE_SINGLE: rsingle = True sstart = True elif mode == self.MODE_RANGE: rrange = True sstart = send = True else: raise ValueError('Unknown mode') self._rdb_sum.setChecked(rsum) self._rdb_max.setChecked(rmax) self._rdb_single.setChecked(rsingle) self._rdb_range.setChecked(rrange) self._sld_start.setEnabled(sstart) self._sld_end.setEnabled(send) self._update_data(mode) def mode(self): if self._rdb_sum.isChecked(): return self.MODE_SUM elif self._rdb_max.isChecked(): return self.MODE_MAX elif self._rdb_single.isChecked(): return self.MODE_SINGLE elif self._rdb_range.isChecked(): return self.MODE_RANGE else: raise ValueError('Unknown mode')
class PyDMSlider(QFrame, TextFormatter, PyDMWritableWidget): """ A QSlider with support for Channels and more from PyDM. Parameters ---------- parent : QWidget The parent widget for the Label init_channel : str, optional The channel to be used by the widget. """ actionTriggered = Signal(int) rangeChanged = Signal(float, float) sliderMoved = Signal(float) sliderPressed = Signal() sliderReleased = Signal() valueChanged = Signal(float) def __init__(self, parent=None, init_channel=None): QFrame.__init__(self, parent) PyDMWritableWidget.__init__(self, init_channel=init_channel) self.alarmSensitiveContent = True self.alarmSensitiveBorder = False # Internal values for properties self._ignore_mouse_wheel = False self._show_limit_labels = True self._show_value_label = True self._user_defined_limits = False self._needs_limit_info = True self._minimum = None self._maximum = None self._user_minimum = -10.0 self._user_maximum = 10.0 self._num_steps = 101 self._orientation = Qt.Horizontal # Set up all the internal widgets that make up a PyDMSlider. # We'll add all these things to layouts when we call setup_widgets_for_orientation label_size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) self.low_lim_label = QLabel(self) self.low_lim_label.setObjectName("lowLimLabel") self.low_lim_label.setSizePolicy(label_size_policy) self.low_lim_label.setAlignment(Qt.AlignLeft | Qt.AlignTrailing | Qt.AlignVCenter) self.value_label = QLabel(self) self.value_label.setObjectName("valueLabel") self.value_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.high_lim_label = QLabel(self) self.high_lim_label.setObjectName("highLimLabel") self.high_lim_label.setSizePolicy(label_size_policy) self.high_lim_label.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) self._slider = QSlider(parent=self) self._slider.setOrientation(Qt.Horizontal) self._orig_wheel_event = self._slider.wheelEvent self._slider.sliderMoved.connect(self.internal_slider_moved) self._slider.sliderPressed.connect(self.internal_slider_pressed) self._slider.sliderReleased.connect(self.internal_slider_released) self._slider.valueChanged.connect(self.internal_slider_value_changed) # self.vertical_layout.addWidget(self._slider) # Other internal variables and final setup steps self._slider_position_to_value_map = None self._mute_internal_slider_changes = False self.setup_widgets_for_orientation(self._orientation) self.reset_slider_limits() def wheelEvent(self, e): #We specifically want to ignore mouse wheel events. if self._ignore_mouse_wheel: e.ignore() else: super(PyDMSlider, self).wheelEvent(e) return def init_for_designer(self): """ Method called after the constructor to tweak configurations for when using the widget with the Qt Designer """ self.value = 0.0 @Property(Qt.Orientation) def orientation(self): """ The slider orientation (Horizontal or Vertical) Returns ------- int Qt.Horizontal or Qt.Vertical """ return self._orientation @orientation.setter def orientation(self, new_orientation): """ The slider orientation (Horizontal or Vertical) Parameters ---------- new_orientation : int Qt.Horizontal or Qt.Vertical """ self._orientation = new_orientation self.setup_widgets_for_orientation(new_orientation) def setup_widgets_for_orientation(self, new_orientation): """ Reconstruct the widget given the orientation. Parameters ---------- new_orientation : int Qt.Horizontal or Qt.Vertical """ if new_orientation not in (Qt.Horizontal, Qt.Vertical): logger.error("Invalid orientation '{0}'. The existing layout will not change.".format(new_orientation)) return layout = None if new_orientation == Qt.Horizontal: layout = QVBoxLayout() layout.setContentsMargins(4, 0, 4, 4) label_layout = QHBoxLayout() label_layout.addWidget(self.low_lim_label) label_layout.addStretch(0) label_layout.addWidget(self.value_label) label_layout.addStretch(0) label_layout.addWidget(self.high_lim_label) layout.addLayout(label_layout) self._slider.setOrientation(new_orientation) layout.addWidget(self._slider) elif new_orientation == Qt.Vertical: layout = QHBoxLayout() layout.setContentsMargins(0, 4, 4, 4) label_layout = QVBoxLayout() label_layout.addWidget(self.high_lim_label) label_layout.addStretch(0) label_layout.addWidget(self.value_label) label_layout.addStretch(0) label_layout.addWidget(self.low_lim_label) layout.addLayout(label_layout) self._slider.setOrientation(new_orientation) layout.addWidget(self._slider) if self.layout() is not None: # Trick to remove the existing layout by re-parenting it in an empty widget. QWidget().setLayout(self.layout()) self.setLayout(layout) def update_labels(self): """ Update the limits and value labels with the correct values. """ def set_label(value, label_widget): if value is None: label_widget.setText("") else: label_widget.setText(self.format_string.format(value)) set_label(self.minimum, self.low_lim_label) set_label(self.maximum, self.high_lim_label) set_label(self.value, self.value_label) def reset_slider_limits(self): """ Reset the limits and adjust the labels properly for the slider. """ logger.debug("Running reset_slider_limits.") if self.minimum is None or self.maximum is None: self._needs_limit_info = True logger.debug("Need both limits before reset_slider_limits can work.") self.set_enable_state() return logger.debug("Has both limits, proceeding.") self._needs_limit_info = False self._slider.setMinimum(0) self._slider.setMaximum(self._num_steps - 1) self._slider.setSingleStep(1) self._slider.setPageStep(10) self._slider_position_to_value_map = np.linspace(self.minimum, self.maximum, num=self._num_steps) self.update_labels() self.set_slider_to_closest_value(self.value) self.rangeChanged.emit(self.minimum, self.maximum) self.set_enable_state() def find_closest_slider_position_to_value(self, val): """ Find and returns the index for the closest position on the slider for a given value. Parameters ---------- val : float Returns ------- int """ diff = abs(self._slider_position_to_value_map - float(val)) logger.debug("The closest value to %f is: %f", val, self._slider_position_to_value_map[np.argmin(diff)]) return np.argmin(diff) def set_slider_to_closest_value(self, val): """ Set the value for the slider according to a given value. Parameters ---------- val : float """ if val is None or self._needs_limit_info: logger.debug("Not setting slider to closest value because we need limits.") return # When we set the slider to the closest value, it may end up at a slightly # different position than val (if val is not in self._slider_position_to_value_map) # We don't want that slight difference to get broacast out and put the channel # somewhere new. For example, if the slider can only be at 0.4 or 0.5, but a # new value comes in of 0.45, its more important to keep the 0.45 than to change # it to where the slider gets set. Therefore, we mute the internal slider changes # so that its valueChanged signal doesn't cause us to emit a signal to PyDM to change # the value of the channel. logger.debug("Setting slider to closest value.") self._mute_internal_slider_changes = True self._slider.setValue(self.find_closest_slider_position_to_value(val)) self._mute_internal_slider_changes = False def connection_changed(self, connected): """ Callback invoked when the connection state of the Channel is changed. This callback acts on the connection state to enable/disable the widget and also trigger the change on alarm severity to ALARM_DISCONNECTED. Parameters ---------- connected : int When this value is 0 the channel is disconnected, 1 otherwise. """ super(PyDMSlider, self).connection_changed(connected) self.set_enable_state() def write_access_changed(self, new_write_access): """ Callback invoked when the Channel has new write access value. This callback calls check_enable_state so it can act on the widget enabling or disabling it accordingly Parameters ---------- new_write_access : bool True if write operations to the channel are allowed. """ super(PyDMSlider, self).write_access_changed(new_write_access) self.set_enable_state() def value_changed(self, new_val): """ Callback invoked when the Channel value is changed. Parameters ---------- new_val : int or float The new value from the channel. """ logger.debug("Slider got a new value = %f", float(new_val)) PyDMWritableWidget.value_changed(self, new_val) if hasattr(self, "value_label"): logger.debug("Setting text for value label.") self.value_label.setText(self.format_string.format(self.value)) if not self._slider.isSliderDown(): self.set_slider_to_closest_value(self.value) def ctrl_limit_changed(self, which, new_limit): """ Callback invoked when the Channel receives new control limit values. Parameters ---------- which : str Which control limit was changed. "UPPER" or "LOWER" new_limit : float New value for the control limit """ logger.debug("%s limit changed to %f", which, new_limit) PyDMWritableWidget.ctrl_limit_changed(self, which, new_limit) if not self.userDefinedLimits: self.reset_slider_limits() def update_format_string(self): """ Reconstruct the format string to be used when representing the output value. Returns ------- format_string : str The format string to be used including or not the precision and unit """ fs = super(PyDMSlider, self).update_format_string() self.update_labels() return fs def set_enable_state(self): """ Determines wether or not the widget must be enabled or not depending on the write access, connection state and presence of limits information """ # Even though by documentation disabling parent QFrame (self), should disable internal # slider, in practice it still remains interactive (PyQt 5.12.1). Disabling explicitly, solves # the problem. should_enable = self._write_access and self._connected and not self._needs_limit_info self.setEnabled(should_enable) self._slider.setEnabled(should_enable) @Slot(int) def internal_slider_action_triggered(self, action): self.actionTriggered.emit(action) @Slot(int) def internal_slider_moved(self, val): """ Method invoked when the slider is moved. Parameters ---------- val : float """ self.sliderMoved.emit(self.value) @Slot() def internal_slider_pressed(self): """ Method invoked when the slider is pressed """ self.sliderPressed.emit() @Slot() def internal_slider_released(self): """ Method invoked when the slider is released """ self.sliderReleased.emit() @Slot(int) def internal_slider_value_changed(self, val): """ Method invoked when a new value is selected on the slider. This will cause the new value to be emitted to the signal unless `mute_internal_slider_changes` is True. Parameters ---------- val : int """ # Avoid potential crash if limits are undefined if self._slider_position_to_value_map is None: return if not self._mute_internal_slider_changes: self.value = self._slider_position_to_value_map[val] self.send_value_signal[float].emit(self.value) @Property(bool) def tracking(self): """ If tracking is enabled (the default), the slider emits new values while the slider is being dragged. If tracking is disabled, it will only emit new values when the user releases the slider. Tracking can cause PyDM to rapidly send new values to the channel. If you are using the slider to control physical hardware, consider whether the device you want to control can handle large amounts of changes in a short timespan. """ return self._slider.hasTracking() @tracking.setter def tracking(self, checked): self._slider.setTracking(checked) def hasTracking(self): """ An alternative function to get the tracking property, to match what Qt provides for QSlider. """ return self.tracking def setTracking(self, checked): """ An alternative function to set the tracking property, to match what Qt provides for QSlider. """ self.tracking = checked @Property(bool) def ignoreMouseWheel(self): """ If true, the mouse wheel will not change the value of the slider. This is useful if you want to put sliders inside a scroll view, and don't want to accidentally change the slider as you are scrolling. """ return self._ignore_mouse_wheel @ignoreMouseWheel.setter def ignoreMouseWheel(self, checked): self._ignore_mouse_wheel = checked if checked: self._slider.wheelEvent = self.wheelEvent else: self._slider.wheelEvent = self._orig_wheel_event @Property(bool) def showLimitLabels(self): """ Whether or not the high and low limits should be displayed on the slider. Returns ------- bool """ return self._show_limit_labels @showLimitLabels.setter def showLimitLabels(self, checked): """ Whether or not the high and low limits should be displayed on the slider. Parameters ---------- checked : bool """ self._show_limit_labels = checked if checked: self.low_lim_label.show() self.high_lim_label.show() else: self.low_lim_label.hide() self.high_lim_label.hide() @Property(bool) def showValueLabel(self): """ Whether or not the current value should be displayed on the slider. Returns ------- bool """ return self._show_value_label @showValueLabel.setter def showValueLabel(self, checked): """ Whether or not the current value should be displayed on the slider. Parameters ---------- checked : bool """ self._show_value_label = checked if checked: self.value_label.show() else: self.value_label.hide() @Property(QSlider.TickPosition) def tickPosition(self): """ Where to draw tick marks for the slider. Returns ------- QSlider.TickPosition """ return self._slider.tickPosition() @tickPosition.setter def tickPosition(self, position): """ Where to draw tick marks for the slider. Parameter --------- position : QSlider.TickPosition """ self._slider.setTickPosition(position) @Property(bool) def userDefinedLimits(self): """ Wether or not to use limits defined by the user and not from the channel Returns ------- bool """ return self._user_defined_limits @userDefinedLimits.setter def userDefinedLimits(self, user_defined_limits): """ Wether or not to use limits defined by the user and not from the channel Parameters ---------- user_defined_limits : bool """ self._user_defined_limits = user_defined_limits self.reset_slider_limits() @Property(float) def userMinimum(self): """ Lower user defined limit value Returns ------- float """ return self._user_minimum @userMinimum.setter def userMinimum(self, new_min): """ Lower user defined limit value Parameters ---------- new_min : float """ self._user_minimum = float(new_min) if new_min is not None else None if self.userDefinedLimits: self.reset_slider_limits() @Property(float) def userMaximum(self): """ Upper user defined limit value Returns ------- float """ return self._user_maximum @userMaximum.setter def userMaximum(self, new_max): """ Upper user defined limit value Parameters ---------- new_max : float """ self._user_maximum = float(new_max) if new_max is not None else None if self.userDefinedLimits: self.reset_slider_limits() @property def minimum(self): """ The current value being used for the lower limit Returns ------- float """ if self.userDefinedLimits: return self._user_minimum return self._lower_ctrl_limit @property def maximum(self): """ The current value being used for the upper limit Returns ------- float """ if self.userDefinedLimits: return self._user_maximum return self._upper_ctrl_limit @Property(int) def num_steps(self): """ The number of steps on the slider Returns ------- int """ return self._num_steps @num_steps.setter def num_steps(self, new_steps): """ The number of steps on the slider Parameters ---------- new_steps : int """ self._num_steps = int(new_steps) self.reset_slider_limits()
class TimeChartDisplay(Display): def __init__(self, parent=None, args=[], macros=None, show_pv_add_panel=True, config_file=None): """ Create all the widgets, including any child dialogs. Parameters ---------- parent : QWidget The parent widget of the charting display args : list The command parameters macros : str Macros to modify the UI parameters at runtime show_pv_add_panel : bool Whether or not to show the PV add panel on top of the graph """ super(TimeChartDisplay, self).__init__(parent=parent, args=args, macros=macros) self.legend_font = None self.channel_map = dict() self.setWindowTitle("TimeChart Tool") self.main_layout = QVBoxLayout() self.body_layout = QVBoxLayout() self.pv_add_panel = QFrame() self.pv_add_panel.setVisible(show_pv_add_panel) self.pv_add_panel.setMaximumHeight(50) self.pv_layout = QHBoxLayout() self.pv_name_line_edt = QLineEdit() self.pv_name_line_edt.setAcceptDrops(True) self.pv_name_line_edt.returnPressed.connect(self.add_curve) self.pv_protocol_cmb = QComboBox() self.pv_protocol_cmb.addItems(["ca://", "archive://"]) self.pv_protocol_cmb.setEnabled(False) self.pv_connect_push_btn = QPushButton("Connect") self.pv_connect_push_btn.clicked.connect(self.add_curve) self.tab_panel = QTabWidget() self.tab_panel.setMinimumWidth(350) self.tab_panel.setMaximumWidth(350) self.curve_settings_tab = QWidget() self.data_settings_tab = QWidget() self.chart_settings_tab = QWidget() self.charting_layout = QHBoxLayout() self.chart = PyDMTimePlot(plot_by_timestamps=False) self.chart.setDownsampling(ds=False, auto=False, mode=None) self.chart.plot_redrawn_signal.connect(self.update_curve_data) self.chart.setBufferSize(DEFAULT_BUFFER_SIZE) self.chart.setPlotTitle(DEFAULT_CHART_TITLE) self.splitter = QSplitter() self.curve_settings_layout = QVBoxLayout() self.curve_settings_layout.setAlignment(Qt.AlignTop) self.curve_settings_layout.setSizeConstraint(QLayout.SetMinAndMaxSize) self.curve_settings_layout.setSpacing(5) self.crosshair_settings_layout = QVBoxLayout() self.crosshair_settings_layout.setAlignment(Qt.AlignTop) self.crosshair_settings_layout.setSpacing(5) self.enable_crosshair_chk = QCheckBox("Crosshair") self.crosshair_coord_lbl = QLabel() self.crosshair_coord_lbl.setWordWrap(True) self.curve_settings_inner_frame = QFrame() self.curve_settings_inner_frame.setLayout(self.curve_settings_layout) self.curve_settings_scroll = QScrollArea() self.curve_settings_scroll.setVerticalScrollBarPolicy( Qt.ScrollBarAsNeeded) self.curve_settings_scroll.setHorizontalScrollBarPolicy( Qt.ScrollBarAlwaysOff) self.curve_settings_scroll.setWidget(self.curve_settings_inner_frame) self.curve_settings_scroll.setWidgetResizable(True) self.enable_crosshair_chk.setChecked(False) self.enable_crosshair_chk.clicked.connect( self.handle_enable_crosshair_checkbox_clicked) self.enable_crosshair_chk.clicked.emit(False) self.curves_tab_layout = QHBoxLayout() self.curves_tab_layout.addWidget(self.curve_settings_scroll) self.data_tab_layout = QVBoxLayout() self.data_tab_layout.setAlignment(Qt.AlignTop) self.data_tab_layout.setSpacing(5) self.chart_settings_layout = QVBoxLayout() self.chart_settings_layout.setAlignment(Qt.AlignTop) self.chart_settings_layout.setSpacing(5) self.chart_layout = QVBoxLayout() self.chart_layout.setSpacing(10) self.chart_panel = QWidget() self.chart_panel.setMinimumHeight(400) self.chart_control_layout = QHBoxLayout() self.chart_control_layout.setAlignment(Qt.AlignHCenter) self.chart_control_layout.setSpacing(10) self.zoom_x_layout = QVBoxLayout() self.zoom_x_layout.setAlignment(Qt.AlignTop) self.zoom_x_layout.setSpacing(5) self.plus_icon = IconFont().icon("plus", color=QColor("green")) self.minus_icon = IconFont().icon("minus", color=QColor("red")) self.view_all_icon = IconFont().icon("globe", color=QColor("blue")) self.reset_icon = IconFont().icon("circle-o-notch", color=QColor("green")) self.zoom_in_x_btn = QPushButton("X Zoom") self.zoom_in_x_btn.setIcon(self.plus_icon) self.zoom_in_x_btn.clicked.connect( partial(self.handle_zoom_in_btn_clicked, "x", True)) self.zoom_in_x_btn.setEnabled(False) self.zoom_out_x_btn = QPushButton("X Zoom") self.zoom_out_x_btn.setIcon(self.minus_icon) self.zoom_out_x_btn.clicked.connect( partial(self.handle_zoom_in_btn_clicked, "x", False)) self.zoom_out_x_btn.setEnabled(False) self.zoom_y_layout = QVBoxLayout() self.zoom_y_layout.setAlignment(Qt.AlignTop) self.zoom_y_layout.setSpacing(5) self.zoom_in_y_btn = QPushButton("Y Zoom") self.zoom_in_y_btn.setIcon(self.plus_icon) self.zoom_in_y_btn.clicked.connect( partial(self.handle_zoom_in_btn_clicked, "y", True)) self.zoom_in_y_btn.setEnabled(False) self.zoom_out_y_btn = QPushButton("Y Zoom") self.zoom_out_y_btn.setIcon(self.minus_icon) self.zoom_out_y_btn.clicked.connect( partial(self.handle_zoom_in_btn_clicked, "y", False)) self.zoom_out_y_btn.setEnabled(False) self.view_all_btn = QPushButton("View All") self.view_all_btn.setIcon(self.view_all_icon) self.view_all_btn.clicked.connect(self.handle_view_all_button_clicked) self.view_all_btn.setEnabled(False) self.view_all_reset_chart_layout = QVBoxLayout() self.view_all_reset_chart_layout.setAlignment(Qt.AlignTop) self.view_all_reset_chart_layout.setSpacing(5) self.pause_chart_layout = QVBoxLayout() self.pause_chart_layout.setAlignment(Qt.AlignTop) self.pause_chart_layout.setSpacing(5) self.reset_chart_btn = QPushButton("Reset") self.reset_chart_btn.setIcon(self.reset_icon) self.reset_chart_btn.clicked.connect( self.handle_reset_chart_btn_clicked) self.reset_chart_btn.setEnabled(False) self.pause_icon = IconFont().icon("pause", color=QColor("red")) self.play_icon = IconFont().icon("play", color=QColor("green")) self.pause_chart_btn = QPushButton() self.pause_chart_btn.setIcon(self.pause_icon) self.pause_chart_btn.clicked.connect( self.handle_pause_chart_btn_clicked) self.title_settings_layout = QVBoxLayout() self.title_settings_layout.setAlignment(Qt.AlignTop) self.title_settings_layout.setSpacing(5) self.title_settings_grpbx = QGroupBox("Title and Legend") self.title_settings_grpbx.setMaximumHeight(120) self.import_export_data_layout = QVBoxLayout() self.import_export_data_layout.setAlignment(Qt.AlignTop) self.import_export_data_layout.setSpacing(5) self.import_data_btn = QPushButton("Import...") self.import_data_btn.clicked.connect( self.handle_import_data_btn_clicked) self.export_data_btn = QPushButton("Export...") self.export_data_btn.clicked.connect( self.handle_export_data_btn_clicked) self.chart_title_layout = QHBoxLayout() self.chart_title_layout.setSpacing(10) self.chart_title_lbl = QLabel(text="Graph Title") self.chart_title_line_edt = QLineEdit() self.chart_title_line_edt.setText(self.chart.getPlotTitle()) self.chart_title_line_edt.textChanged.connect( self.handle_title_text_changed) self.chart_title_font_btn = QPushButton() self.chart_title_font_btn.setFixedHeight(24) self.chart_title_font_btn.setFixedWidth(24) self.chart_title_font_btn.setIcon(IconFont().icon("font")) self.chart_title_font_btn.clicked.connect( partial(self.handle_chart_font_changed, "title")) self.chart_change_axis_settings_btn = QPushButton( text="Change Axis Settings...") self.chart_change_axis_settings_btn.clicked.connect( self.handle_change_axis_settings_clicked) self.update_datetime_timer = QTimer(self) self.update_datetime_timer.timeout.connect( self.handle_update_datetime_timer_timeout) self.chart_sync_mode_layout = QVBoxLayout() self.chart_sync_mode_layout.setSpacing(5) self.chart_sync_mode_grpbx = QGroupBox("Data Sampling Mode") self.chart_sync_mode_grpbx.setMaximumHeight(100) self.chart_sync_mode_sync_radio = QRadioButton("Synchronous") self.chart_sync_mode_async_radio = QRadioButton("Asynchronous") self.chart_sync_mode_async_radio.setChecked(True) self.graph_drawing_settings_layout = QVBoxLayout() self.graph_drawing_settings_layout.setAlignment(Qt.AlignVCenter) self.chart_interval_layout = QFormLayout() self.chart_redraw_rate_lbl = QLabel("Redraw Rate (Hz)") self.chart_redraw_rate_spin = QSpinBox() self.chart_redraw_rate_spin.setRange(MIN_REDRAW_RATE_HZ, MAX_REDRAW_RATE_HZ) self.chart_redraw_rate_spin.setValue(DEFAULT_REDRAW_RATE_HZ) self.chart_redraw_rate_spin.editingFinished.connect( self.handle_redraw_rate_changed) self.chart_data_sampling_rate_lbl = QLabel("Data Sampling Rate (Hz)") self.chart_data_async_sampling_rate_spin = QSpinBox() self.chart_data_async_sampling_rate_spin.setRange( MIN_DATA_SAMPLING_RATE_HZ, MAX_DATA_SAMPLING_RATE_HZ) self.chart_data_async_sampling_rate_spin.setValue( DEFAULT_DATA_SAMPLING_RATE_HZ) self.chart_data_async_sampling_rate_spin.editingFinished.connect( self.handle_data_sampling_rate_changed) self.chart_data_sampling_rate_lbl.hide() self.chart_data_async_sampling_rate_spin.hide() self.chart_limit_time_span_layout = QHBoxLayout() self.chart_limit_time_span_layout.setSpacing(5) self.limit_time_plan_text = "Limit Time Span" self.chart_limit_time_span_chk = QCheckBox(self.limit_time_plan_text) self.chart_limit_time_span_chk.hide() self.chart_limit_time_span_lbl = QLabel("Hr:Min:Sec") self.chart_limit_time_span_hours_spin_box = QSpinBox() self.chart_limit_time_span_hours_spin_box.setMaximum(999) self.chart_limit_time_span_minutes_spin_box = QSpinBox() self.chart_limit_time_span_minutes_spin_box.setMaximum(59) self.chart_limit_time_span_seconds_spin_box = QSpinBox() self.chart_limit_time_span_seconds_spin_box.setMaximum(59) self.chart_limit_time_span_activate_btn = QPushButton("Apply") self.chart_limit_time_span_activate_btn.setDisabled(True) self.chart_ring_buffer_layout = QFormLayout() self.chart_ring_buffer_size_lbl = QLabel("Ring Buffer Size") self.chart_ring_buffer_size_edt = QLineEdit() self.chart_ring_buffer_size_edt.returnPressed.connect( self.handle_buffer_size_changed) self.chart_ring_buffer_size_edt.setText(str(DEFAULT_BUFFER_SIZE)) self.show_legend_chk = QCheckBox("Show Legend") self.show_legend_chk.clicked.connect( self.handle_show_legend_checkbox_clicked) self.show_legend_chk.setChecked(self.chart.showLegend) self.legend_font_btn = QPushButton() self.legend_font_btn.setFixedHeight(24) self.legend_font_btn.setFixedWidth(24) self.legend_font_btn.setIcon(IconFont().icon("font")) self.legend_font_btn.clicked.connect( partial(self.handle_chart_font_changed, "legend")) self.graph_background_color_layout = QFormLayout() self.axis_grid_color_layout = QFormLayout() self.background_color_lbl = QLabel("Graph Background Color ") self.background_color_btn = QPushButton() self.background_color_btn.setStyleSheet( "background-color: " + self.chart.getBackgroundColor().name()) self.background_color_btn.setContentsMargins(10, 0, 5, 5) self.background_color_btn.setMaximumWidth(20) self.background_color_btn.clicked.connect( self.handle_background_color_button_clicked) self.axis_settings_layout = QVBoxLayout() self.axis_settings_layout.setSpacing(10) self.show_x_grid_chk = QCheckBox("Show x Grid") self.show_x_grid_chk.setChecked(self.chart.showXGrid) self.show_x_grid_chk.clicked.connect( self.handle_show_x_grid_checkbox_clicked) self.show_y_grid_chk = QCheckBox("Show y Grid") self.show_y_grid_chk.setChecked(self.chart.showYGrid) self.show_y_grid_chk.clicked.connect( self.handle_show_y_grid_checkbox_clicked) self.axis_color_lbl = QLabel("Axis and Grid Color") self.axis_color_btn = QPushButton() self.axis_color_btn.setStyleSheet("background-color: " + DEFAULT_CHART_AXIS_COLOR.name()) self.axis_color_btn.setContentsMargins(10, 0, 5, 5) self.axis_color_btn.setMaximumWidth(20) self.axis_color_btn.clicked.connect( self.handle_axis_color_button_clicked) self.grid_opacity_lbl = QLabel("Grid Opacity") self.grid_opacity_lbl.setEnabled(False) self.grid_opacity_slr = QSlider(Qt.Horizontal) self.grid_opacity_slr.setFocusPolicy(Qt.StrongFocus) self.grid_opacity_slr.setRange(0, 10) self.grid_opacity_slr.setValue(5) self.grid_opacity_slr.setTickInterval(1) self.grid_opacity_slr.setSingleStep(1) self.grid_opacity_slr.setTickPosition(QSlider.TicksBelow) self.grid_opacity_slr.valueChanged.connect( self.handle_grid_opacity_slider_mouse_release) self.grid_opacity_slr.setEnabled(False) self.reset_data_settings_btn = QPushButton("Reset Data Settings") self.reset_data_settings_btn.clicked.connect( self.handle_reset_data_settings_btn_clicked) self.reset_chart_settings_btn = QPushButton("Reset Chart Settings") self.reset_chart_settings_btn.clicked.connect( self.handle_reset_chart_settings_btn_clicked) self.curve_checkbox_panel = QWidget() self.graph_drawing_settings_grpbx = QGroupBox("Graph Intervals") self.graph_drawing_settings_grpbx.setAlignment(Qt.AlignTop) self.axis_settings_grpbx = QGroupBox("Graph Appearance") self.app = QApplication.instance() self.setup_ui() self.curve_settings_disp = None self.axis_settings_disp = None self.chart_data_export_disp = None self.chart_data_import_disp = None self.grid_alpha = 5 self.time_span_limit_hours = None self.time_span_limit_minutes = None self.time_span_limit_seconds = None self.data_sampling_mode = ASYNC_DATA_SAMPLING # If there is an imported config file, let's start TimeChart with the imported configuration data if config_file: importer = SettingsImporter(self) try: importer.import_settings(config_file) except SettingsImporterException: display_message_box( QMessageBox.Critical, "Import Failure", "Cannot import the file '{0}'. Check the log for the error details." .format(config_file)) logger.exception( "Cannot import the file '{0}'.".format(config_file)) def ui_filepath(self): """ The path to the UI file created by Qt Designer, if applicable. """ # No UI file is being used return None def ui_filename(self): """ The name the UI file created by Qt Designer, if applicable. """ # No UI file is being used return None def setup_ui(self): """ Initialize the widgets and layouts. """ self.setLayout(self.main_layout) self.pv_layout.addWidget(self.pv_protocol_cmb) self.pv_layout.addWidget(self.pv_name_line_edt) self.pv_layout.addWidget(self.pv_connect_push_btn) self.pv_add_panel.setLayout(self.pv_layout) QTimer.singleShot(0, self.pv_name_line_edt.setFocus) self.curve_settings_tab.setLayout(self.curves_tab_layout) self.chart_settings_tab.setLayout(self.chart_settings_layout) self.setup_chart_settings_layout() self.data_settings_tab.setLayout(self.data_tab_layout) self.setup_data_tab_layout() self.tab_panel.addTab(self.curve_settings_tab, "Curves") self.tab_panel.addTab(self.data_settings_tab, "Data") self.tab_panel.addTab(self.chart_settings_tab, "Graph") self.crosshair_settings_layout.addWidget(self.enable_crosshair_chk) self.crosshair_settings_layout.addWidget(self.crosshair_coord_lbl) self.zoom_x_layout.addWidget(self.zoom_in_x_btn) self.zoom_x_layout.addWidget(self.zoom_out_x_btn) self.zoom_y_layout.addWidget(self.zoom_in_y_btn) self.zoom_y_layout.addWidget(self.zoom_out_y_btn) self.view_all_reset_chart_layout.addWidget(self.reset_chart_btn) self.view_all_reset_chart_layout.addWidget(self.view_all_btn) self.pause_chart_layout.addWidget(self.pause_chart_btn) self.import_export_data_layout.addWidget(self.import_data_btn) self.import_export_data_layout.addWidget(self.export_data_btn) self.chart_control_layout.addLayout(self.zoom_x_layout) self.chart_control_layout.addLayout(self.zoom_y_layout) self.chart_control_layout.addLayout(self.view_all_reset_chart_layout) self.chart_control_layout.addLayout(self.pause_chart_layout) self.chart_control_layout.addLayout(self.crosshair_settings_layout) self.chart_control_layout.addLayout(self.import_export_data_layout) self.chart_control_layout.insertSpacing(5, 30) self.chart_layout.addWidget(self.chart) self.chart_layout.addLayout(self.chart_control_layout) self.chart_panel.setLayout(self.chart_layout) self.splitter.addWidget(self.chart_panel) self.splitter.addWidget(self.tab_panel) self.splitter.setSizes([1, 0]) self.splitter.setHandleWidth(10) self.splitter.setStretchFactor(0, 0) self.splitter.setStretchFactor(1, 1) self.charting_layout.addWidget(self.splitter) self.body_layout.addWidget(self.pv_add_panel) self.body_layout.addLayout(self.charting_layout) self.body_layout.setSpacing(0) self.body_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.addLayout(self.body_layout) self.enable_chart_control_buttons(False) handle = self.splitter.handle(1) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) button = QToolButton(handle) button.setArrowType(Qt.LeftArrow) button.clicked.connect(lambda: self.handle_splitter_button(True)) layout.addWidget(button) button = QToolButton(handle) button.setArrowType(Qt.RightArrow) button.clicked.connect(lambda: self.handle_splitter_button(False)) layout.addWidget(button) handle.setLayout(layout) def handle_splitter_button(self, left=True): if left: self.splitter.setSizes([1, 1]) else: self.splitter.setSizes([1, 0]) def change_legend_font(self, font): if font is None: return self.legend_font = font items = self.chart.plotItem.legend.items for i in items: i[1].item.setFont(font) i[1].resizeEvent(None) i[1].updateGeometry() def change_title_font(self, font): current_text = self.chart.plotItem.titleLabel.text args = { "family": font.family, "size": "{}pt".format(font.pointSize()), "bold": font.bold(), "italic": font.italic(), } self.chart.plotItem.titleLabel.setText(current_text, **args) def handle_chart_font_changed(self, target): if target not in ("title", "legend"): return dialog = QFontDialog(self) dialog.setOption(QFontDialog.DontUseNativeDialog, True) if target == "title": dialog.fontSelected.connect(self.change_title_font) else: dialog.fontSelected.connect(self.change_legend_font) dialog.open() def setup_data_tab_layout(self): self.chart_sync_mode_sync_radio.toggled.connect( partial(self.handle_sync_mode_radio_toggle, self.chart_sync_mode_sync_radio)) self.chart_sync_mode_async_radio.toggled.connect( partial(self.handle_sync_mode_radio_toggle, self.chart_sync_mode_async_radio)) self.chart_sync_mode_layout.addWidget(self.chart_sync_mode_sync_radio) self.chart_sync_mode_layout.addWidget(self.chart_sync_mode_async_radio) self.chart_sync_mode_grpbx.setLayout(self.chart_sync_mode_layout) self.data_tab_layout.addWidget(self.chart_sync_mode_grpbx) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_lbl) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_hours_spin_box) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_minutes_spin_box) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_seconds_spin_box) self.chart_limit_time_span_layout.addWidget( self.chart_limit_time_span_activate_btn) self.chart_limit_time_span_lbl.hide() self.chart_limit_time_span_hours_spin_box.hide() self.chart_limit_time_span_minutes_spin_box.hide() self.chart_limit_time_span_seconds_spin_box.hide() self.chart_limit_time_span_activate_btn.hide() self.chart_limit_time_span_hours_spin_box.valueChanged.connect( self.handle_time_span_changed) self.chart_limit_time_span_minutes_spin_box.valueChanged.connect( self.handle_time_span_changed) self.chart_limit_time_span_seconds_spin_box.valueChanged.connect( self.handle_time_span_changed) self.chart_limit_time_span_chk.clicked.connect( self.handle_limit_time_span_checkbox_clicked) self.chart_limit_time_span_activate_btn.clicked.connect( self.handle_chart_limit_time_span_activate_btn_clicked) self.chart_interval_layout.addRow(self.chart_redraw_rate_lbl, self.chart_redraw_rate_spin) self.chart_interval_layout.addRow( self.chart_data_sampling_rate_lbl, self.chart_data_async_sampling_rate_spin) self.graph_drawing_settings_layout.addLayout( self.chart_interval_layout) self.graph_drawing_settings_layout.addWidget( self.chart_limit_time_span_chk) self.graph_drawing_settings_layout.addLayout( self.chart_limit_time_span_layout) self.chart_ring_buffer_layout.addRow(self.chart_ring_buffer_size_lbl, self.chart_ring_buffer_size_edt) self.graph_drawing_settings_layout.addLayout( self.chart_ring_buffer_layout) self.graph_drawing_settings_grpbx.setLayout( self.graph_drawing_settings_layout) self.data_tab_layout.addWidget(self.graph_drawing_settings_grpbx) self.chart_sync_mode_async_radio.toggled.emit(True) self.data_tab_layout.addWidget(self.reset_data_settings_btn) def setup_chart_settings_layout(self): self.chart_title_layout.addWidget(self.chart_title_lbl) self.chart_title_layout.addWidget(self.chart_title_line_edt) self.chart_title_layout.addWidget(self.chart_title_font_btn) self.title_settings_layout.addLayout(self.chart_title_layout) legend_layout = QHBoxLayout() legend_layout.addWidget(self.show_legend_chk) legend_layout.addWidget(self.legend_font_btn) self.title_settings_layout.addLayout(legend_layout) self.title_settings_layout.addWidget( self.chart_change_axis_settings_btn) self.title_settings_grpbx.setLayout(self.title_settings_layout) self.chart_settings_layout.addWidget(self.title_settings_grpbx) self.graph_background_color_layout.addRow(self.background_color_lbl, self.background_color_btn) self.axis_settings_layout.addLayout(self.graph_background_color_layout) self.axis_grid_color_layout.addRow(self.axis_color_lbl, self.axis_color_btn) self.axis_settings_layout.addLayout(self.axis_grid_color_layout) self.axis_settings_layout.addWidget(self.show_x_grid_chk) self.axis_settings_layout.addWidget(self.show_y_grid_chk) self.axis_settings_layout.addWidget(self.grid_opacity_lbl) self.axis_settings_layout.addWidget(self.grid_opacity_slr) self.axis_settings_grpbx.setLayout(self.axis_settings_layout) self.chart_settings_layout.addWidget(self.axis_settings_grpbx) self.chart_settings_layout.addWidget(self.reset_chart_settings_btn) self.update_datetime_timer.start(1000) def add_curve(self): """ Add a new curve to the chart. """ pv_name = self._get_full_pv_name(self.pv_name_line_edt.text()) if pv_name and len(pv_name): color = random_color(curve_colors_only=True) for k, v in self.channel_map.items(): if color == v.color: color = random_color(curve_colors_only=True) self.add_y_channel(pv_name=pv_name, curve_name=pv_name, color=color) self.handle_splitter_button(left=True) def show_mouse_coordinates(self, x, y): self.crosshair_coord_lbl.clear() self.crosshair_coord_lbl.setText("x = {0:.3f}\ny = {1:.3f}".format( x, y)) def handle_enable_crosshair_checkbox_clicked(self, is_checked): self.chart.enableCrosshair(is_checked) self.crosshair_coord_lbl.setVisible(is_checked) self.chart.crosshair_position_updated.connect( self.show_mouse_coordinates) def add_y_channel(self, pv_name, curve_name, color, line_style=Qt.SolidLine, line_width=2, symbol=None, symbol_size=None, is_visible=True): if pv_name in self.channel_map: logger.error("'{0}' has already been added.".format(pv_name)) return curve = self.chart.addYChannel(y_channel=pv_name, name=curve_name, color=color, lineStyle=line_style, lineWidth=line_width, symbol=symbol, symbolSize=symbol_size) curve.show() if is_visible else curve.hide() if self.show_legend_chk.isChecked(): self.change_legend_font(self.legend_font) self.channel_map[pv_name] = curve self.generate_pv_controls(pv_name, color) self.enable_chart_control_buttons() try: self.app.add_connection(curve.channel) except AttributeError: # these methods are not needed on future versions of pydm pass def generate_pv_controls(self, pv_name, curve_color): """ Generate a set of widgets to manage the appearance of a curve. The set of widgets includes: 1. A checkbox which shows the curve on the chart if checked, and hide the curve if not checked 2. Three buttons -- Modify..., Focus, and Remove. Modify... will bring up the Curve Settings dialog. Focus adjusts the chart's zooming for the current curve. Remove will delete the curve from the chart Parameters ---------- pv_name: str The name of the PV the current curve is being plotted for curve_color : QColor The color of the curve to paint for the checkbox label to help the user track the curve to the checkbox """ individual_curve_layout = QVBoxLayout() size_policy = QSizePolicy() size_policy.setVerticalPolicy(QSizePolicy.Fixed) size_policy.setHorizontalPolicy(QSizePolicy.Fixed) individual_curve_grpbx = QGroupBox() individual_curve_grpbx.setMinimumWidth(300) individual_curve_grpbx.setMinimumHeight(120) individual_curve_grpbx.setAlignment(Qt.AlignTop) individual_curve_grpbx.setSizePolicy(size_policy) individual_curve_grpbx.setObjectName(pv_name + "_grb") individual_curve_grpbx.setLayout(individual_curve_layout) checkbox = QCheckBox(parent=individual_curve_grpbx) checkbox.setObjectName(pv_name + "_chb") palette = checkbox.palette() palette.setColor(QPalette.Active, QPalette.WindowText, curve_color) checkbox.setPalette(palette) display_name = pv_name.split("://")[1] if len(display_name) > MAX_DISPLAY_PV_NAME_LENGTH: # Only display max allowed number of characters of the PV Name display_name = display_name[ :int(MAX_DISPLAY_PV_NAME_LENGTH / 2) - 1] + "..." + \ display_name[ -int(MAX_DISPLAY_PV_NAME_LENGTH / 2) + 2:] checkbox.setText(display_name) data_text = QLabel(parent=individual_curve_grpbx) data_text.setWordWrap(True) data_text.setObjectName(pv_name + "_lbl") data_text.setPalette(palette) checkbox.setChecked(True) checkbox.toggled.connect( partial(self.handle_curve_chkbox_toggled, checkbox)) if not self.chart.findCurve(pv_name).isVisible(): checkbox.setChecked(False) modify_curve_btn = QPushButton("Modify...", parent=individual_curve_grpbx) modify_curve_btn.setObjectName(pv_name + "_btn_modify") modify_curve_btn.setMaximumWidth(80) modify_curve_btn.clicked.connect( partial(self.display_curve_settings_dialog, pv_name)) focus_curve_btn = QPushButton("Focus", parent=individual_curve_grpbx) focus_curve_btn.setObjectName(pv_name + "_btn_focus") focus_curve_btn.setMaximumWidth(80) focus_curve_btn.clicked.connect(partial(self.focus_curve, pv_name)) clear_curve_btn = QPushButton("Clear", parent=individual_curve_grpbx) clear_curve_btn.setObjectName(pv_name + "_btn_clear") clear_curve_btn.setMaximumWidth(80) clear_curve_btn.clicked.connect(partial(self.clear_curve, pv_name)) # annotate_curve_btn = QPushButton("Annotate...", # parent=individual_curve_grpbx) # annotate_curve_btn.setObjectName(pv_name+"_btn_ann") # annotate_curve_btn.setMaximumWidth(80) # annotate_curve_btn.clicked.connect( # partial(self.annotate_curve, pv_name)) remove_curve_btn = QPushButton("Remove", parent=individual_curve_grpbx) remove_curve_btn.setObjectName(pv_name + "_btn_remove") remove_curve_btn.setMaximumWidth(80) remove_curve_btn.clicked.connect(partial(self.remove_curve, pv_name)) curve_btn_layout = QHBoxLayout() curve_btn_layout.setSpacing(5) curve_btn_layout.addWidget(modify_curve_btn) curve_btn_layout.addWidget(focus_curve_btn) curve_btn_layout.addWidget(clear_curve_btn) # curve_btn_layout.addWidget(annotate_curve_btn) curve_btn_layout.addWidget(remove_curve_btn) individual_curve_layout.addWidget(checkbox) individual_curve_layout.addWidget(data_text) individual_curve_layout.addLayout(curve_btn_layout) self.curve_settings_layout.addWidget(individual_curve_grpbx) self.tab_panel.setCurrentIndex(0) def handle_curve_chkbox_toggled(self, checkbox): """ Handle a checkbox's checked and unchecked events. If a checkbox is checked, find the curve from the channel map. If found, re-draw the curve with its previous appearance settings. If a checkbox is unchecked, remove the curve from the chart, but keep the cached data in the channel map. Parameters ---------- checkbox : QCheckBox The current checkbox being toggled """ pv_name = self._get_full_pv_name(checkbox.text()) if checkbox.isChecked(): curve = self.channel_map.get(pv_name, None) if curve: curve.show() self.chart.addLegendItem(curve, pv_name, self.show_legend_chk.isChecked()) self.change_legend_font(self.legend_font) else: curve = self.chart.findCurve(pv_name) if curve: curve.hide() self.chart.removeLegendItem(pv_name) def display_curve_settings_dialog(self, pv_name): """ Bring up the Curve Settings dialog to modify the appearance of a curve. Parameters ---------- pv_name : str The name of the PV the curve is being plotted for """ self.curve_settings_disp = CurveSettingsDisplay(self, pv_name) self.curve_settings_disp.show() def focus_curve(self, pv_name): curve = self.chart.findCurve(pv_name) if curve: self.chart.plotItem.setYRange(curve.minY, curve.maxY, padding=0) def clear_curve(self, pv_name): curve = self.chart.findCurve(pv_name) if curve: curve.initialize_buffer() def annotate_curve(self, pv_name): curve = self.chart.findCurve(pv_name) if curve: annot = TextItem( html= '<div style="text-align: center"><span style="color: #FFF;">This is the' '</span><br><span style="color: #FF0; font-size: 16pt;">PEAK</span></div>', anchor=(-0.3, 0.5), border='w', fill=(0, 0, 255, 100)) self.chart.annotateCurve(curve, annot) def remove_curve(self, pv_name): """ Remove a curve from the chart permanently. This will also clear the channel map cache from retaining the removed curve's appearance settings. Parameters ---------- pv_name : str The name of the PV the curve is being plotted for """ curve = self.chart.findCurve(pv_name) if curve: try: self.app.remove_connection(curve.channel) except AttributeError: # these methods are not needed on future versions of pydm pass self.chart.removeYChannel(curve) del self.channel_map[pv_name] self.chart.removeLegendItem(pv_name) widget = self.findChild(QGroupBox, pv_name + "_grb") if widget: widget.deleteLater() if len(self.chart.getCurves()) < 1: self.enable_chart_control_buttons(False) self.show_legend_chk.setChecked(False) def handle_title_text_changed(self, new_text): self.chart.setPlotTitle(new_text) def handle_change_axis_settings_clicked(self): self.axis_settings_disp = AxisSettingsDisplay(self) self.axis_settings_disp.show() def handle_limit_time_span_checkbox_clicked(self, is_checked): self.chart_limit_time_span_lbl.setVisible(is_checked) self.chart_limit_time_span_hours_spin_box.setVisible(is_checked) self.chart_limit_time_span_minutes_spin_box.setVisible(is_checked) self.chart_limit_time_span_seconds_spin_box.setVisible(is_checked) self.chart_limit_time_span_activate_btn.setVisible(is_checked) self.chart_ring_buffer_size_lbl.setDisabled(is_checked) self.chart_ring_buffer_size_edt.setDisabled(is_checked) if not is_checked: self.chart_limit_time_span_chk.setText(self.limit_time_plan_text) def handle_time_span_changed(self): self.time_span_limit_hours = self.chart_limit_time_span_hours_spin_box.value( ) self.time_span_limit_minutes = self.chart_limit_time_span_minutes_spin_box.value( ) self.time_span_limit_seconds = self.chart_limit_time_span_seconds_spin_box.value( ) status = self.time_span_limit_hours > 0 or self.time_span_limit_minutes > 0 or self.time_span_limit_seconds > 0 self.chart_limit_time_span_activate_btn.setEnabled(status) def handle_chart_limit_time_span_activate_btn_clicked(self): timeout_milliseconds = (self.time_span_limit_hours * 3600 + self.time_span_limit_minutes * 60 + self.time_span_limit_seconds) * 1000 self.chart.setTimeSpan(timeout_milliseconds / 1000.0) self.chart_ring_buffer_size_edt.setText(str( self.chart.getBufferSize())) def handle_buffer_size_changed(self): try: new_buffer_size = int(self.chart_ring_buffer_size_edt.text()) if new_buffer_size and int(new_buffer_size) >= MINIMUM_BUFFER_SIZE: self.chart.setBufferSize(new_buffer_size) except ValueError: display_message_box(QMessageBox.Critical, "Invalid Values", "Only integer values are accepted.") def handle_redraw_rate_changed(self): self.chart.maxRedrawRate = self.chart_redraw_rate_spin.value() def handle_data_sampling_rate_changed(self): # The chart expects the value in milliseconds sampling_rate_seconds = 1.0 / self.chart_data_async_sampling_rate_spin.value( ) buffer_size = self.chart.getBufferSize() self.chart.setUpdateInterval(sampling_rate_seconds) if self.chart.getBufferSize() < buffer_size: self.chart.setBufferSize(buffer_size) self.chart_ring_buffer_size_edt.setText(str( self.chart.getBufferSize())) def handle_background_color_button_clicked(self): selected_color = QColorDialog.getColor() self.chart.setBackgroundColor(selected_color) self.background_color_btn.setStyleSheet("background-color: " + selected_color.name()) def handle_axis_color_button_clicked(self): selected_color = QColorDialog.getColor() self.chart.setAxisColor(selected_color) self.axis_color_btn.setStyleSheet("background-color: " + selected_color.name()) def handle_grid_opacity_slider_mouse_release(self): self.grid_alpha = float(self.grid_opacity_slr.value()) / 10.0 self.chart.setShowXGrid(self.show_x_grid_chk.isChecked(), self.grid_alpha) self.chart.setShowYGrid(self.show_y_grid_chk.isChecked(), self.grid_alpha) def handle_show_x_grid_checkbox_clicked(self, is_checked): self.chart.setShowXGrid(is_checked, self.grid_alpha) self.grid_opacity_lbl.setEnabled(is_checked or self.show_y_grid_chk.isChecked()) self.grid_opacity_slr.setEnabled(is_checked or self.show_y_grid_chk.isChecked()) def handle_show_y_grid_checkbox_clicked(self, is_checked): self.chart.setShowYGrid(is_checked, self.grid_alpha) self.grid_opacity_lbl.setEnabled(is_checked or self.show_x_grid_chk.isChecked()) self.grid_opacity_slr.setEnabled(is_checked or self.show_x_grid_chk.isChecked()) def handle_show_legend_checkbox_clicked(self, is_checked): self.chart.setShowLegend(is_checked) def handle_export_data_btn_clicked(self): self.chart_data_export_disp = ChartDataExportDisplay(self) self.chart_data_export_disp.show() def handle_import_data_btn_clicked(self): open_file_info = QFileDialog.getOpenFileName( self, caption="Open File", directory=os.path.expanduser('~'), filter=IMPORT_FILE_FORMAT) open_filename = open_file_info[0] if open_filename: try: importer = SettingsImporter(self) importer.import_settings(open_filename) except SettingsImporterException: display_message_box( QMessageBox.Critical, "Import Failure", "Cannot import the file '{0}'. Check the log for the error details." .format(open_filename)) logger.exception( "Cannot import the file '{0}'".format(open_filename)) def handle_sync_mode_radio_toggle(self, radio_btn): if radio_btn.isChecked(): if radio_btn.text() == "Synchronous": self.data_sampling_mode = SYNC_DATA_SAMPLING self.chart_data_sampling_rate_lbl.hide() self.chart_data_async_sampling_rate_spin.hide() self.chart.resetTimeSpan() self.chart_limit_time_span_chk.setChecked(False) self.chart_limit_time_span_chk.clicked.emit(False) self.chart_limit_time_span_chk.hide() self.chart.setUpdatesAsynchronously(False) elif radio_btn.text() == "Asynchronous": self.data_sampling_mode = ASYNC_DATA_SAMPLING self.chart_data_sampling_rate_lbl.show() self.chart_data_async_sampling_rate_spin.show() self.chart_limit_time_span_chk.show() self.chart.setUpdatesAsynchronously(True) def handle_zoom_in_btn_clicked(self, axis, is_zoom_in): scale_factor = 0.5 if not is_zoom_in: scale_factor += 1.0 if axis == "x": self.chart.getViewBox().scaleBy(x=scale_factor) elif axis == "y": self.chart.getViewBox().scaleBy(y=scale_factor) def handle_view_all_button_clicked(self): self.chart.plotItem.getViewBox().autoRange() def handle_pause_chart_btn_clicked(self): if self.chart.pausePlotting(): self.pause_chart_btn.setIcon(self.pause_icon) else: self.pause_chart_btn.setIcon(self.play_icon) def handle_reset_chart_btn_clicked(self): self.chart.getViewBox().setXRange(DEFAULT_X_MIN, 0) self.chart.resetAutoRangeY() @Slot() def handle_reset_chart_settings_btn_clicked(self): self.chart.setBackgroundColor(DEFAULT_CHART_BACKGROUND_COLOR) self.background_color_btn.setStyleSheet( "background-color: " + DEFAULT_CHART_BACKGROUND_COLOR.name()) self.chart.setAxisColor(DEFAULT_CHART_AXIS_COLOR) self.axis_color_btn.setStyleSheet("background-color: " + DEFAULT_CHART_AXIS_COLOR.name()) self.grid_opacity_slr.setValue(5) self.show_x_grid_chk.setChecked(False) self.show_x_grid_chk.clicked.emit(False) self.show_y_grid_chk.setChecked(False) self.show_y_grid_chk.clicked.emit(False) self.show_legend_chk.setChecked(False) self.chart.setShowXGrid(False) self.chart.setShowYGrid(False) self.chart.setShowLegend(False) @Slot() def handle_reset_data_settings_btn_clicked(self): self.chart_ring_buffer_size_edt.setText(str(DEFAULT_BUFFER_SIZE)) self.chart_redraw_rate_spin.setValue(DEFAULT_REDRAW_RATE_HZ) self.handle_redraw_rate_changed() self.chart_data_async_sampling_rate_spin.setValue( DEFAULT_DATA_SAMPLING_RATE_HZ) self.chart_data_sampling_rate_lbl.hide() self.chart_data_async_sampling_rate_spin.hide() self.chart_sync_mode_async_radio.setChecked(True) self.chart_sync_mode_async_radio.toggled.emit(True) self.chart_limit_time_span_chk.setChecked(False) self.chart_limit_time_span_chk.setText(self.limit_time_plan_text) self.chart_limit_time_span_chk.clicked.emit(False) self.chart.setUpdatesAsynchronously(True) self.chart.resetTimeSpan() self.chart.resetUpdateInterval() self.chart.setBufferSize(DEFAULT_BUFFER_SIZE) def enable_chart_control_buttons(self, enabled=True): self.zoom_in_x_btn.setEnabled(enabled) self.zoom_out_x_btn.setEnabled(enabled) self.zoom_in_y_btn.setEnabled(enabled) self.zoom_out_y_btn.setEnabled(enabled) self.view_all_btn.setEnabled(enabled) self.reset_chart_btn.setEnabled(enabled) self.pause_chart_btn.setIcon(self.pause_icon) self.pause_chart_btn.setEnabled(enabled) self.export_data_btn.setEnabled(enabled) def _get_full_pv_name(self, pv_name): """ Append the protocol to the PV Name. Parameters ---------- pv_name : str The name of the PV the curve is being plotted for """ if pv_name and "://" not in pv_name: pv_name = ''.join([self.pv_protocol_cmb.currentText(), pv_name]) return pv_name def handle_update_datetime_timer_timeout(self): current_label = self.chart.getBottomAxisLabel() new_label = "Current Time: " + TimeChartDisplay.get_current_datetime() if X_AXIS_LABEL_SEPARATOR in current_label: current_label = current_label[current_label. find(X_AXIS_LABEL_SEPARATOR) + len(X_AXIS_LABEL_SEPARATOR):] new_label += X_AXIS_LABEL_SEPARATOR + current_label self.chart.setLabel("bottom", text=new_label) def update_curve_data(self, curve): """ Determine if the PV is active. If not, disable the related PV controls. If the PV is active, update the PV controls' states. Parameters ---------- curve : PlotItem A PlotItem, i.e. a plot, to draw on the chart. """ pv_name = curve.address min_y = curve.minY if curve.minY else 0 max_y = curve.maxY if curve.maxY else 0 current_y = curve.data_buffer[1, -1] grb = self.findChild(QGroupBox, pv_name + "_grb") lbl = grb.findChild(QLabel, pv_name + "_lbl") lbl.setText("(yMin = {0:.3f}, yMax = {1:.3f}) y = {2:.3f}".format( min_y, max_y, current_y)) chb = grb.findChild(QCheckBox, pv_name + "_chb") connected = curve.connected if connected and chb.isEnabled(): return chb.setEnabled(connected) btn_modify = grb.findChild(QPushButton, pv_name + "_btn_modify") btn_modify.setEnabled(connected) btn_focus = grb.findChild(QPushButton, pv_name + "_btn_focus") btn_focus.setEnabled(connected) # btn_ann = grb.findChild(QPushButton, pv_name + "_btn_ann") # btn_ann.setEnabled(connected) @staticmethod def get_current_datetime(): current_date = datetime.datetime.now().strftime("%b %d, %Y") current_time = datetime.datetime.now().strftime("%H:%M:%S") current_datetime = current_time + ' (' + current_date + ')' return current_datetime @property def gridAlpha(self): return self.grid_alpha
class PyDMSlider(QFrame, TextFormatter, PyDMWritableWidget): """ A QSlider with support for Channels and more from PyDM. Parameters ---------- parent : QWidget The parent widget for the Label init_channel : str, optional The channel to be used by the widget. """ actionTriggered = Signal(int) rangeChanged = Signal(float, float) sliderMoved = Signal(float) sliderPressed = Signal() sliderReleased = Signal() valueChanged = Signal(float) def __init__(self, parent=None, init_channel=None): QFrame.__init__(self, parent) PyDMWritableWidget.__init__(self, init_channel=init_channel) self.alarmSensitiveContent = True self.alarmSensitiveBorder = False # Internal values for properties self._show_limit_labels = True self._show_value_label = True self._user_defined_limits = False self._needs_limit_info = True self._minimum = None self._maximum = None self._user_minimum = -10.0 self._user_maximum = 10.0 self._num_steps = 101 self._orientation = Qt.Horizontal # Set up all the internal widgets that make up a PyDMSlider. # We'll add all these things to layouts when we call setup_widgets_for_orientation label_size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) self.low_lim_label = QLabel(self) self.low_lim_label.setObjectName("lowLimLabel") self.low_lim_label.setSizePolicy(label_size_policy) self.low_lim_label.setAlignment(Qt.AlignLeft | Qt.AlignTrailing | Qt.AlignVCenter) self.value_label = QLabel(self) self.value_label.setObjectName("valueLabel") self.value_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.high_lim_label = QLabel(self) self.high_lim_label.setObjectName("highLimLabel") self.high_lim_label.setSizePolicy(label_size_policy) self.high_lim_label.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) self._slider = QSlider(parent=self) self._slider.setOrientation(Qt.Horizontal) self._slider.sliderMoved.connect(self.internal_slider_moved) self._slider.sliderPressed.connect(self.internal_slider_pressed) self._slider.sliderReleased.connect(self.internal_slider_released) self._slider.valueChanged.connect(self.internal_slider_value_changed) # self.vertical_layout.addWidget(self._slider) # Other internal variables and final setup steps self._slider_position_to_value_map = None self._mute_internal_slider_changes = False self.setup_widgets_for_orientation(self._orientation) self.reset_slider_limits() def init_for_designer(self): """ Method called after the constructor to tweak configurations for when using the widget with the Qt Designer """ self.value = 0.0 @Property(Qt.Orientation) def orientation(self): """ The slider orientation (Horizontal or Vertical) Returns ------- int Qt.Horizontal or Qt.Vertical """ return self._orientation @orientation.setter def orientation(self, new_orientation): """ The slider orientation (Horizontal or Vertical) Parameters ---------- new_orientation : int Qt.Horizontal or Qt.Vertical """ self._orientation = new_orientation self.setup_widgets_for_orientation(new_orientation) def setup_widgets_for_orientation(self, new_orientation): """ Reconstruct the widget given the orientation. Parameters ---------- new_orientation : int Qt.Horizontal or Qt.Vertical """ if new_orientation not in (Qt.Horizontal, Qt.Vertical): logger.error("Invalid orientation '{0}'. The existing layout will not change.".format(new_orientation)) return layout = None if new_orientation == Qt.Horizontal: layout = QVBoxLayout() layout.setContentsMargins(4, 0, 4, 4) label_layout = QHBoxLayout() label_layout.addWidget(self.low_lim_label) label_layout.addStretch(0) label_layout.addWidget(self.value_label) label_layout.addStretch(0) label_layout.addWidget(self.high_lim_label) layout.addLayout(label_layout) self._slider.setOrientation(new_orientation) layout.addWidget(self._slider) elif new_orientation == Qt.Vertical: layout = QHBoxLayout() layout.setContentsMargins(0, 4, 4, 4) label_layout = QVBoxLayout() label_layout.addWidget(self.high_lim_label) label_layout.addStretch(0) label_layout.addWidget(self.value_label) label_layout.addStretch(0) label_layout.addWidget(self.low_lim_label) layout.addLayout(label_layout) self._slider.setOrientation(new_orientation) layout.addWidget(self._slider) if self.layout() is not None: # Trick to remove the existing layout by re-parenting it in an empty widget. QWidget().setLayout(self.layout()) self.setLayout(layout) def update_labels(self): """ Update the limits and value labels with the correct values. """ def set_label(value, label_widget): if value is None: label_widget.setText("") else: label_widget.setText(self.format_string.format(value)) set_label(self.minimum, self.low_lim_label) set_label(self.maximum, self.high_lim_label) set_label(self.value, self.value_label) def reset_slider_limits(self): """ Reset the limits and adjust the labels properly for the slider. """ if self.minimum is None or self.maximum is None: self._needs_limit_info = True self.set_enable_state() return self._needs_limit_info = False self._slider.setMinimum(0) self._slider.setMaximum(self._num_steps - 1) self._slider.setSingleStep(1) self._slider.setPageStep(10) self._slider_position_to_value_map = np.linspace(self.minimum, self.maximum, num=self._num_steps) self.update_labels() self.set_slider_to_closest_value(self.value) self.rangeChanged.emit(self.minimum, self.maximum) self.set_enable_state() def find_closest_slider_position_to_value(self, val): """ Find and returns the index for the closest position on the slider for a given value. Parameters ---------- val : float Returns ------- int """ diff = abs(self._slider_position_to_value_map - float(val)) return np.argmin(diff) def set_slider_to_closest_value(self, val): """ Set the value for the slider according to a given value. Parameters ---------- val : float """ if val is None or self._needs_limit_info: return # When we set the slider to the closest value, it may end up at a slightly # different position than val (if val is not in self._slider_position_to_value_map) # We don't want that slight difference to get broacast out and put the channel # somewhere new. For example, if the slider can only be at 0.4 or 0.5, but a # new value comes in of 0.45, its more important to keep the 0.45 than to change # it to where the slider gets set. Therefore, we mute the internal slider changes # so that its valueChanged signal doesn't cause us to emit a signal to PyDM to change # the value of the channel. self._mute_internal_slider_changes = True self._slider.setValue(self.find_closest_slider_position_to_value(val)) self._mute_internal_slider_changes = False def value_changed(self, new_val): """ Callback invoked when the Channel value is changed. Parameters ---------- new_val : int or float The new value from the channel. """ PyDMWritableWidget.value_changed(self, new_val) if hasattr(self, "value_label"): self.value_label.setText(self.format_string.format(self.value)) if not self._slider.isSliderDown(): self.set_slider_to_closest_value(self.value) def ctrl_limit_changed(self, which, new_limit): """ Callback invoked when the Channel receives new control limit values. Parameters ---------- which : str Which control limit was changed. "UPPER" or "LOWER" new_limit : float New value for the control limit """ PyDMWritableWidget.ctrl_limit_changed(self, which, new_limit) if not self.userDefinedLimits: self.reset_slider_limits() def update_format_string(self): """ Reconstruct the format string to be used when representing the output value. Returns ------- format_string : str The format string to be used including or not the precision and unit """ fs = super(PyDMSlider, self).update_format_string() self.update_labels() return fs def set_enable_state(self): """ Determines wether or not the widget must be enabled or not depending on the write access, connection state and presence of limits information """ # Even though by documentation disabling parent QFrame (self), should disable internal # slider, in practice it still remains interactive (PyQt 5.12.1). Disabling explicitly, solves # the problem. should_enable = self._write_access and self._connected and not self._needs_limit_info self.setEnabled(should_enable) self._slider.setEnabled(should_enable) @Slot(int) def internal_slider_action_triggered(self, action): self.actionTriggered.emit(action) @Slot(int) def internal_slider_moved(self, val): """ Method invoked when the slider is moved. Parameters ---------- val : float """ # Avoid potential crash if limits are undefined if self._slider_position_to_value_map is None: return # The user has moved the slider, we need to update our value. # Only update the underlying value, not the self.value property, # because we don't need to reset the slider position. If we change # self.value, we can get into a loop where the position changes, which # updates the value, which changes the position again, etc etc. self.value = self._slider_position_to_value_map[val] self.sliderMoved.emit(self.value) @Slot() def internal_slider_pressed(self): """ Method invoked when the slider is pressed """ self.sliderPressed.emit() @Slot() def internal_slider_released(self): """ Method invoked when the slider is released """ self.sliderReleased.emit() @Slot(int) def internal_slider_value_changed(self, val): """ Method invoked when a new value is selected on the slider. This will cause the new value to be emitted to the signal unless `mute_internal_slider_changes` is True. Parameters ---------- val : int """ # At this point, our local copy of the value reflects the position of the # slider, now all we need to do is emit a signal to PyDM so that the data # plugin will send a put to the channel. Don't update self.value or self._value # in here, it is pointless at best, and could cause an infinite loop at worst. if not self._mute_internal_slider_changes: self.send_value_signal[float].emit(self.value) @Property(bool) def showLimitLabels(self): """ Whether or not the high and low limits should be displayed on the slider. Returns ------- bool """ return self._show_limit_labels @showLimitLabels.setter def showLimitLabels(self, checked): """ Whether or not the high and low limits should be displayed on the slider. Parameters ---------- checked : bool """ self._show_limit_labels = checked if checked: self.low_lim_label.show() self.high_lim_label.show() else: self.low_lim_label.hide() self.high_lim_label.hide() @Property(bool) def showValueLabel(self): """ Whether or not the current value should be displayed on the slider. Returns ------- bool """ return self._show_value_label @showValueLabel.setter def showValueLabel(self, checked): """ Whether or not the current value should be displayed on the slider. Parameters ---------- checked : bool """ self._show_value_label = checked if checked: self.value_label.show() else: self.value_label.hide() @Property(QSlider.TickPosition) def tickPosition(self): """ Where to draw tick marks for the slider. Returns ------- QSlider.TickPosition """ return self._slider.tickPosition() @tickPosition.setter def tickPosition(self, position): """ Where to draw tick marks for the slider. Parameter --------- position : QSlider.TickPosition """ self._slider.setTickPosition(position) @Property(bool) def userDefinedLimits(self): """ Wether or not to use limits defined by the user and not from the channel Returns ------- bool """ return self._user_defined_limits @userDefinedLimits.setter def userDefinedLimits(self, user_defined_limits): """ Wether or not to use limits defined by the user and not from the channel Parameters ---------- user_defined_limits : bool """ self._user_defined_limits = user_defined_limits self.reset_slider_limits() @Property(float) def userMinimum(self): """ Lower user defined limit value Returns ------- float """ return self._user_minimum @userMinimum.setter def userMinimum(self, new_min): """ Lower user defined limit value Parameters ---------- new_min : float """ self._user_minimum = float(new_min) if new_min is not None else None if self.userDefinedLimits: self.reset_slider_limits() @Property(float) def userMaximum(self): """ Upper user defined limit value Returns ------- float """ return self._user_maximum @userMaximum.setter def userMaximum(self, new_max): """ Upper user defined limit value Parameters ---------- new_max : float """ self._user_maximum = float(new_max) if new_max is not None else None if self.userDefinedLimits: self.reset_slider_limits() @property def minimum(self): """ The current value being used for the lower limit Returns ------- float """ if self.userDefinedLimits: return self._user_minimum return self._lower_ctrl_limit @property def maximum(self): """ The current value being used for the upper limit Returns ------- float """ if self.userDefinedLimits: return self._user_maximum return self._upper_ctrl_limit @Property(int) def num_steps(self): """ The number of steps on the slider Returns ------- int """ return self._num_steps @num_steps.setter def num_steps(self, new_steps): """ The number of steps on the slider Parameters ---------- new_steps : int """ self._num_steps = int(new_steps) self.reset_slider_limits()
class AnimationWidget(QWidget): """Widget for interatviely making animations using the napari viewer. Parameters ---------- viewer : napari.Viewer napari viewer. Attributes ---------- viewer : napari.Viewer napari viewer. animation : napari_animation.Animation napari-animation animation in sync with the GUI. """ def __init__(self, viewer: Viewer, parent=None): super().__init__(parent=parent) # Store reference to viewer and create animation self.viewer = viewer self.animation = Animation(self.viewer) # Initialise User Interface self.keyframesListControlWidget = KeyFrameListControlWidget( animation=self.animation, parent=self) self.keyframesListWidget = KeyFramesListWidget( self.animation.key_frames, parent=self) self.frameWidget = FrameWidget(parent=self) self.saveButton = QPushButton("Save Animation", parent=self) self.animationSlider = QSlider(Qt.Horizontal, parent=self) self.animationSlider.setToolTip("Scroll through animation") self.animationSlider.setRange(0, len(self.animation._frames) - 1) # Create layout self.setLayout(QVBoxLayout()) self.layout().addWidget(self.keyframesListControlWidget) self.layout().addWidget(self.keyframesListWidget) self.layout().addWidget(self.frameWidget) self.layout().addWidget(self.saveButton) self.layout().addWidget(self.animationSlider) # establish key bindings and callbacks self._add_keybind_callbacks() self._add_callbacks() def _add_keybind_callbacks(self): """Bind keys""" self._keybindings = [ ("Alt-f", self._capture_keyframe_callback), ("Alt-r", self._replace_keyframe_callback), ("Alt-d", self._delete_keyframe_callback), ("Alt-a", lambda e: self.animation.key_frames.select_next()), ("Alt-b", lambda e: self.animation.key_frames.select_previous()), ] for key, cb in self._keybindings: self.viewer.bind_key(key, cb) def _add_callbacks(self): """Establish callbacks""" self.keyframesListControlWidget.deleteButton.clicked.connect( self._delete_keyframe_callback) self.keyframesListControlWidget.captureButton.clicked.connect( self._capture_keyframe_callback) self.saveButton.clicked.connect(self._save_callback) self.animationSlider.valueChanged.connect(self._on_slider_moved) self.animation._frames.events.n_frames.connect(self._nframes_changed) keyframe_list = self.animation.key_frames keyframe_list.events.inserted.connect(self._on_keyframes_changed) keyframe_list.events.removed.connect(self._on_keyframes_changed) keyframe_list.events.changed.connect(self._on_keyframes_changed) keyframe_list.selection.events.active.connect( self._on_active_keyframe_changed) def _input_state(self): """Get current state of input widgets as {key->value} parameters.""" return { "steps": int(self.frameWidget.stepsSpinBox.value()), "ease": self.frameWidget.get_easing_func(), } def _capture_keyframe_callback(self, event=None): """Record current key-frame""" self.animation.capture_keyframe(**self._input_state()) def _replace_keyframe_callback(self, event=None): """Replace current key-frame with new view""" self.animation.capture_keyframe(**self._input_state(), insert=False) def _delete_keyframe_callback(self, event=None): """Delete current key-frame""" if self.animation.key_frames.selection.active: self.animation.key_frames.remove_selected() else: raise ValueError("No selected keyframe to delete !") def _on_keyframes_changed(self, event=None): has_frames = bool(self.animation.key_frames) self.keyframesListControlWidget.deleteButton.setEnabled(has_frames) self.keyframesListWidget.setEnabled(has_frames) self.frameWidget.setEnabled(has_frames) def _on_active_keyframe_changed(self, event=None): n_frames = len(self.animation._frames) active_keyframe = event.value if active_keyframe and n_frames: self.animationSlider.blockSignals(True) kf1_list = [ self.animation._frames._frame_index[n][0] for n in range(n_frames) ] frame_index = kf1_list.index(active_keyframe) self.animationSlider.setValue(frame_index) self.animationSlider.blockSignals(False) self.keyframesListControlWidget.deleteButton.setEnabled( bool(active_keyframe)) def _on_slider_moved(self, event=None): frame_index = event if frame_index < len(self.animation._frames) - 1: with self.animation.key_frames.selection.events.active.blocker(): self.animation.set_movie_frame_index(frame_index) def _save_callback(self, event=None): if len(self.animation.key_frames) < 2: error_dialog = QErrorMessage() error_dialog.showMessage( f"You need at least two key frames to generate \ an animation. Your only have {len(self.animation.key_frames)}") error_dialog.exec_() else: filters = ( "Video files (*.mp4 *.gif *.mov *.avi *.mpg *.mpeg *.mkv *.wmv)" ";;Folder of PNGs (*)" # sep filters with ";;" ) filename, _filter = QFileDialog.getSaveFileName( self, "Save animation", str(Path.home()), filters) if filename: self.animation.animate(filename) def _nframes_changed(self, event): has_frames = bool(event.value) self.animationSlider.setEnabled(has_frames) self.animationSlider.blockSignals(has_frames) self.animationSlider.setMaximum(event.value - 1 if has_frames else 0) def closeEvent(self, ev) -> None: # release callbacks for key, _ in self._keybindings: self.viewer.bind_key(key, None) return super().closeEvent(ev)