class QTiffStackView(QWidget): #the view which the user of the videoviewer sees. #This class contains relevant 'client side' attributes e.g. buttons to get a frame, a slide bar and a timer. These attributes submit requests to the QTiffStackController to give the next frame etc. The controller returns either the requested frame or an error message def __init__(self): super(QTiffStackView, self).__init__() #add the image display. This is a subclass of QLabel, where paintEvent is overriden. self.frame_view = FrameView() #self.frame_view.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding) #add the slide bar which allows the user to manual flick through self.slideBar = QSlider(Qt.Horizontal) self.slideBar.setTickPosition(QSlider.TicksAbove) self.slideBar.setTracking(True) self.slideBar.setTickInterval(100) #add a counter which displays the frame which is currently displayed self.counter = QSpinBox() self.counter.setSingleStep(1) self.counter.setRange(self.slideBar.minimum(), self.slideBar.maximum()) #self explanatory self.play = QPushButton('Play') #when play button is pressed the timer takes control of the displaying of frames self.frametimer = QTimer() frame_rate = 30 self.frametimer.setInterval(frame_rate) #Add a sublayout to align the slidebar and frame counter next to eachother slidelyt = QHBoxLayout() slidelyt.addWidget(self.slideBar) slidelyt.addWidget(self.counter) #Add the main layout for the widget lyt = QVBoxLayout() lyt.addWidget(self.frame_view) lyt.addLayout(slidelyt) lyt.addWidget(self.play) self.setLayout(lyt) def updateRanges(self, maximum): assert type(maximum) == int self.slideBar.setMaximum(maximum) self.counter.setRange(self.slideBar.minimum(), self.slideBar.maximum())
class AnimateDialog(QDialog): def __init__(self, vpoints: Sequence[VPoint], vlinks: Sequence[VLink], path: _Paths, slider_path: _SliderPaths, monochrome: bool, parent: QWidget): super(AnimateDialog, self).__init__(parent) self.setWindowTitle("Vector Animation") self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint & ~Qt.WindowContextHelpButtonHint) self.setMinimumSize(800, 600) self.setModal(True) main_layout = QVBoxLayout(self) self.canvas = _DynamicCanvas(vpoints, vlinks, path, slider_path, self) self.canvas.set_monochrome_mode(monochrome) self.canvas.update_pos.connect(self.__set_pos) layout = QHBoxLayout(self) pt_option = QComboBox(self) pt_option.addItems([f"P{p}" for p in range(len(vpoints))]) layout.addWidget(pt_option) value_label = QLabel(self) @Slot(int) def show_values(ind: int): vel, vel_deg = self.canvas.get_vel(ind) acc, acc_deg = self.canvas.get_acc(ind) value_label.setText( f"Velocity: {vel:.04f} ({vel_deg:.04f}deg) | " f"Acceleration: {acc:.04f} ({acc_deg:.04f}deg)") pt_option.currentIndexChanged.connect(show_values) layout.addWidget(value_label) self.pos_label = QLabel(self) layout.addItem( QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) layout.addWidget(self.pos_label) main_layout.addLayout(layout) main_layout.addWidget(self.canvas) layout = QHBoxLayout(self) self.play = QPushButton(QIcon(QPixmap(":/icons/play.png")), "", self) self.play.setCheckable(True) self.play.clicked.connect(self.__play) layout.addWidget(self.play) self.slider = QSlider(Qt.Horizontal, self) self.slider.setMaximum(max(len(p) for p in path) - 1) self.slider.valueChanged.connect(self.canvas.set_index) layout.addWidget(self.slider) layout.addWidget(QLabel("Total times:", self)) factor = QDoubleSpinBox(self) factor.valueChanged.connect(self.canvas.set_factor) factor.setSuffix('s') factor.setRange(0.01, 999999) factor.setValue(10) layout.addWidget(factor) main_layout.addLayout(layout) self.timer = QTimer() self.timer.setInterval(10) self.timer.timeout.connect(self.__move_ind) @Slot() def __move_ind(self): """Move indicator.""" value = self.slider.value() + 1 self.slider.setValue(value) if value > self.slider.maximum(): self.slider.setValue(0) @Slot(float, float) def __set_pos(self, x: float, y: float) -> None: """Set mouse position.""" self.pos_label.setText(f"({x:.04f}, {y:.04f})") @Slot() def __play(self): """Start playing.""" if self.play.isChecked(): self.timer.start() else: self.timer.stop()
class livestream(QWidget): i = 0 def __init__(self,qnd,images = None,annotations_on = True,annotate_coords = None,threshold_switch = False): QWidget.__init__(self) self.threshold_switch = threshold_switch self.video = images #frames buffer self.videobox = Label() if annotations_on and annotate_coords is not None: self.coords = annotate_coords self.videobox.switch = annotations_on self.videobox.activecoord = self.coords[0] if self.video is not None: self.videobox.activeframe = self.video[0] self.videobox.maxintens = self.video.shape[0] else: self.videobox.activeframe = np.loadtxt(os.getcwd() + '/defaultimage.txt') print(self.videobox.activeframe.shape) self.videobox.maxintens = np.max(self.videobox.activeframe) self.videobox.setGeometry(QtCore.QRect(70, 80, 310, 310)) self.videobox.h = 310 self.videobox.w = 310 self.lyt = QVBoxLayout() self.lyt.addWidget(self.videobox,5) self.setLayout(self.lyt) self.sl = QSlider(Qt.Horizontal) self.sl.setMinimum(0.0) if self.video is not None: self.sl.setMaximum(self.video.shape[0]) self.sl.valueChanged.connect(self.whenslidechanges) self.sl.setTickPosition(QSlider.TicksAbove) self.sl.setTracking(True) self.sl.setTickInterval(100) self.frame_counter = QDoubleSpinBox() if images is not None: self.frame = images[0] self.frame_counter.valueChanged.connect(self.video_time_update) self.frame_counter.setSingleStep(1) self.frame_counter.setRange(self.sl.minimum(),self.sl.maximum()) self.frame_counter.valueChanged.connect(self.sl.setValue) self.video_time = QDoubleSpinBox() self.video_time.setSingleStep(30) self.video_time.setRange(self.sl.minimum(),30*self.sl.maximum()) self.frameratetimer = QTimer() self.frameratetimer.setInterval(30) if self.video is not None: self.frameratetimer.timeout.connect(self.update_display) self.play_button = QPushButton('Play Video') self.play_button.clicked.connect(self.frameratetimer.start) self.stop_button = QPushButton('Stop Video') self.stop_button.clicked.connect(self.frameratetimer.stop) if self.video is not None: self.sl.valueChanged.connect(self.whenslidechanges) self.lyt.addWidget(self.play_button,0) self.lyt.addWidget(self.stop_button,1) self.lyt.addWidget(self.sl,2) self.lyt.addWidget(self.frame_counter,3) self.lyt.addWidget(self.video_time,4) self.show() def assign_images(self,images,centres = None): '''#first disconnect signals from eachother so nothing should change whilst video data is being updated self.sl.valueChanged.disconnect(self.video_time_update) self.frameratetimer.timeout.disconnect(self.update_display) self.frame_counter.valueChanged.disconnect(self.whenslidechanges) ''' self.video = images self.coords = centres self.videobox.activeframe = self.video[0] if self.coords is not None: self.videobox.activecoord = self.coords[0] #readjust slider and ticker values to dimensions of video self.sl.setMaximum(len(self.video)-1) self.frame_counter.setRange(self.sl.minimum(),self.sl.maximum()) self.video_time.setRange(self.sl.minimum(),30*self.sl.maximum()) #connect slider and timer etc. self.sl.valueChanged.connect(self.whenslidechanges) self.frameratetimer.timeout.connect(self.update_display) self.frame_counter.valueChanged.connect(self.video_time_update) self.videobox.maxintens = np.max(self.video) self.videobox.update() def update_display(self): if self.threshold_switch: frame = self.video[livestream.i] threshold = threshold_otsu(frame) mask = np.zeros_like(frame) mask[frame > threshold] = 1 self.videobox.maxintens = 1 self.videobox.activeframe = mask else: #if threshold switch is off display usual video, so change active frame source and reset maximum intensity for passing to qimage2ndarray self.videobox.activeframe = self.video[livestream.i] self.videobox.maxintens = np.max(self.video) try: self.videobox.activecoord = self.coords[livestream.i] if not self.videobox.switch: self.videobox.switch = True except: self.videobox.activecoord = None self.videobox.switch = False self.videobox.update() self.frame_counter.setValue(float(livestream.i)) livestream.i+=1 def whenslidechanges(self): if self.frameratetimer.isActive(): self.frameratetimer.stop() livestream.i = self.sl.value() self.update_display() livestream.i -=1 self.frameratetimer.start() else: livestream.i = self.sl.value() self.update_display() livestream.i -=1 def video_time_update(self): self.video_time.setValue(30*self.frame_counter.value()) def turn_on_threshold(self,threshold_switch): self.threshold_switch = threshold_switch self.update_display()
class VideoPlayerWidget(QWidget): def __init__(self, **kwargs): super(VideoPlayerWidget, self).__init__(**kwargs) self._video = None self._playing = False self._moving_seekbar = False self.view = GraphicsView() self.play_button = QPushButton() self.play_button.setIcon(get_standard_icon(QStyle.SP_MediaPlay)) self.play_button.clicked.connect(self.on_play_button_clicked) self.seekbar = QSlider(Qt.Horizontal) self.seekbar.setMinimum(0) self.seekbar.setMaximum(0) self.seekbar.sliderPressed.connect(self.on_seekbar_sliderPressed) self.seekbar.sliderMoved.connect(self.on_seekbar_sliderMoved) self.seekbar.sliderReleased.connect(self.on_seekbar_sliderReleased) self.seekbar.valueChanged.connect(self.on_seekbar_valueChanged) self.nframes_label = QLabel('----- / -----') self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.fetch_frame) layout = vbox([ self.view, hbox([self.play_button, self.seekbar, self.nframes_label]) ]) self.setLayout(layout) def open_video(self, src_path): self._video = cv2.VideoCapture(src_path) if self.video_is_opened(): ret, frame = self._video.read() self.view.update_image(frame) self.seekbar.setMaximum(self._video.get(cv2.CAP_PROP_FRAME_COUNT)) return True else: return False def video_is_opened(self): return self._video is not None and self._video.isOpened() def start_video(self): self._playing = True self.play_button.setIcon(get_standard_icon(QStyle.SP_MediaPause)) fps = self._video.get(cv2.CAP_PROP_FPS) self.update_timer.start(1000. / fps) def stop_video(self): self._playing = False self.play_button.setIcon(get_standard_icon(QStyle.SP_MediaPlay)) self.update_timer.stop() def fetch_frame(self): if self._moving_seekbar: return ret, frame = self._video.read() if not ret: self.stop_video() return self.view.update_image(frame) pos = self._video.get(cv2.CAP_PROP_POS_FRAMES) self.seekbar.setValue(pos) def on_play_button_clicked(self): if not self.video_is_opened(): return if self._playing: self.stop_video() else: self.start_video() def on_seekbar_sliderPressed(self): if not self.video_is_opened(): return self._moving_seekbar = True def on_seekbar_sliderMoved(self): pass def on_seekbar_sliderReleased(self): if not self.video_is_opened(): return self._moving_seekbar = False pos = self.seekbar.value() self._video.set(cv2.CAP_PROP_POS_FRAMES, pos) self.fetch_frame() def on_seekbar_valueChanged(self): curr_pos = self.seekbar.value() nframes = self.seekbar.maximum() self.nframes_label.setText(f'{curr_pos:05d} / {nframes:05d}')
class TrapViewer(QWidget): i = 0 def __init__(self, qnd, images, trap_positions=None, labels=None): QWidget.__init__(self) self.video = images # This is a file object buffer containing the images self.trap_positions = trap_positions self.labels = labels self.videobox = Label(trap_positions, labels) self.videobox.activeframe = images.asarray(key=TrapViewer.i) try: self.videobox.maxintens = int(images.imagej_metadata['max']) self.videobox.maxintens = 15265 print(images.imagej_metadata) except KeyError: self.videobox.maxintens = int(np.max(self.videobox.activeframe)) self.videobox.setGeometry(QtCore.QRect(70, 80, 200, 200)) self.lyt = QVBoxLayout() self.lyt.addWidget(self.videobox, 5) self.setLayout(self.lyt) self.sl = QSlider(Qt.Horizontal) self.sl.setMinimum(0.0) self.sl.setMaximum(self.video.imagej_metadata['frames'] - 1) self.sl.setTickPosition(QSlider.TicksAbove) self.sl.setTracking(True) self.sl.setTickInterval(100) self.sl.valueChanged.connect(self.whenslidechanges) self.frame_counter = QDoubleSpinBox() self.frame = self.videobox.activeframe self.frame_counter.setSingleStep(1) self.frame_counter.setRange(self.sl.minimum(), self.sl.maximum() - 1) self.frame_counter.valueChanged.connect(self.sl.setValue) self.frame_counter.valueChanged.connect(self.video_time_update) self.video_time = QDoubleSpinBox() self.video_time.setSingleStep(30) self.video_time.setRange(self.sl.minimum(), 30 * self.sl.maximum() - 1) self.frameratetimer = QTimer() self.frameratetimer.setInterval(50) self.frameratetimer.timeout.connect(self.update_display) self.play_button = QPushButton('Play Video') self.play_button.clicked.connect(self.frameratetimer.start) self.stop_button = QPushButton('Stop Video') self.stop_button.clicked.connect(self.frameratetimer.stop) self.sl.valueChanged.connect(self.whenslidechanges) self.lyt.addWidget(self.play_button, 0) self.lyt.addWidget(self.stop_button, 1) self.lyt.addWidget(self.sl, 2) self.lyt.addWidget(self.frame_counter, 3) self.lyt.addWidget(self.video_time, 4) self.show() def update_display(self): self.frame = self.video.asarray(key=TrapViewer.i) self.videobox.activeframe = self.frame self.videobox.update() self.frame_counter.setValue(float(TrapViewer.i)) if TrapViewer.i < self.video.imagej_metadata['frames']: TrapViewer.i += 1 def whenslidechanges(self): if self.frameratetimer.isActive(): self.frameratetimer.stop() TrapViewer.i = self.sl.value() self.update_display() TrapViewer.i -= 1 self.frameratetimer.start() else: TrapViewer.i = self.sl.value() self.update_display() TrapViewer.i -= 1 def video_time_update(self): self.video_time.setValue(30 * self.frame_counter.value())
class RangeManager(QWidget): """Width of the widgets can be set using `setMaximumWidth`. The size policy is set so that the widget may shrink if there is not enough space.""" selection_changed = Signal(float, float, str) def __init__(self, *, name="", add_sliders=False, slider_steps=10000, selection_to_range_min=0.001): """ Class constructor for RangeManager Parameters ---------- add_sliders: bool True - the widget will include sliders for controlling the range, False - the widget will have no sliders without sliders slider_steps: int The number of slider steps. Determines the precision of the slider. Default value is sufficient in most cases selection_to_range_min: float Minimum ratio of the selected range and total range. Must be floating point number >=0. Used only when the value type is set to "float": `self.set_value_type("float")`. Minimum selected range is always 1 when "int" value type is set. """ super().__init__() self._name = name # Set the maximum number of steps for the sliders (resolution) self.sld_n_steps = slider_steps # Ratio of the minimum range and total range. It is used to compute # the value of 'self._range_min_diff'. The widget will prevent # range to be set to smaller value than 'self._range_min_diff'. self._selection_to_range_min = selection_to_range_min self._range_low = 0.0 self._range_high = 100.0 self._range_min_diff = (self._range_high - self._range_low) * self._selection_to_range_min self._value_per_step = (self._range_high - self._range_low) / self.sld_n_steps self._value_type = "float" # The following values are used to keep the low and high of the range. # Those values are 'accepted' values that reflect current selected range. self._value_low = self._range_low self._value_high = self._range_high max_element_width = 200 self.le_min_value = LineEditExtended() self.le_max_value = LineEditExtended() self.validator_low = DoubleValidatorRelaxed() self.validator_high = DoubleValidatorRelaxed() self.le_min_value.setMaximumWidth(max_element_width) self.le_min_value.textEdited.connect(self.le_min_value_text_edited) self.le_min_value.textChanged.connect(self.le_min_value_text_changed) self.le_min_value.editingFinished.connect( self.le_min_value_editing_finished) self.le_max_value.setMaximumWidth(max_element_width) self.le_max_value.textEdited.connect(self.le_max_value_text_edited) self.le_max_value.textChanged.connect(self.le_max_value_text_changed) self.le_max_value.editingFinished.connect( self.le_max_value_editing_finished) self.le_min_value.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.le_max_value.setAlignment(Qt.AlignRight | Qt.AlignVCenter) # The flag is set true if mouse is pressed on one of the sliders # Both sliders can not be pressed at once, so one variable is sufficient self._sld_mouse_pressed = False self.sld_min_value = QSlider(Qt.Horizontal) self.sld_min_value.valueChanged.connect( self.sld_min_value_value_changed) self.sld_min_value.sliderPressed.connect( self.sld_min_value_slider_pressed) self.sld_min_value.sliderReleased.connect( self.sld_min_value_slider_released) self.sld_max_value = QSlider(Qt.Horizontal) self.sld_max_value.valueChanged.connect( self.sld_max_value_value_changed) self.sld_max_value.sliderPressed.connect( self.sld_max_value_slider_pressed) self.sld_max_value.sliderReleased.connect( self.sld_max_value_slider_released) self.sld_min_value.setMaximumWidth(max_element_width) self.sld_max_value.setMaximumWidth(max_element_width) # The slider for controlling minimum is inverted self.sld_min_value.setInvertedAppearance(True) self.sld_min_value.setInvertedControls(True) self.sld_min_value.setMaximum(self.sld_n_steps) self.sld_max_value.setMaximum(self.sld_n_steps) self.sld_min_value.setValue(self.sld_min_value.maximum()) self.sld_max_value.setValue(self.sld_max_value.maximum()) self.set_value_type(self._value_type) # Set the validator grid = QGridLayout() grid.setHorizontalSpacing(0) grid.setVerticalSpacing(0) grid.setContentsMargins(0, 0, 0, 0) grid.addWidget(self.le_min_value, 0, 0) grid.addWidget(QLabel(".."), 0, 1) grid.addWidget(self.le_max_value, 0, 2) if add_sliders: grid.addWidget(self.sld_min_value, 1, 0) grid.addWidget(QLabel(""), 1, 1) grid.addWidget(self.sld_max_value, 1, 2) self.setLayout(grid) sp = QSizePolicy() sp.setControlType(QSizePolicy.PushButton) sp.setHorizontalPolicy(QSizePolicy.Maximum) self.setSizePolicy(sp) def le_min_value_text_edited(self, text): if self._min_value_validate(text): v = float(text) # Works even if the value is expected to be 'int' n_steps = self._value_to_slider(v) self.sld_min_value.setValue(self.sld_n_steps - n_steps) def le_min_value_text_changed(self, text): self._min_value_validate(text) def le_min_value_editing_finished(self): # We don't set validator, so this method is called each time QLineEdit # is losing focus or Enter is pressed val = self.le_min_value.text() if self._min_value_validate( ) else self._value_low if self._accept_value_low(val): self.emit_selection_changed() def le_max_value_text_edited(self, text): if self._max_value_validate(text): v = float(text) # Works even if the value is expected to be 'int' n_steps = self._value_to_slider(v) self.sld_max_value.setValue(n_steps) def le_max_value_text_changed(self, text): self._max_value_validate(text) def le_max_value_editing_finished(self): # We don't set validator, so this method is called each time QLineEdit # is losing focus or Enter is pressed val = self.le_max_value.text() if self._max_value_validate( ) else self._value_high if self._accept_value_high(val): self.emit_selection_changed() def sld_min_value_value_changed(self, n_steps): # Invert the reading for 'min' slider if self._sld_mouse_pressed: n_steps = self.sld_n_steps - n_steps v = self._slider_to_value(n_steps) self.le_min_value.setText(self._format_value(v)) def sld_min_value_slider_pressed(self): self._sld_mouse_pressed = True def sld_min_value_slider_released(self): self._sld_mouse_pressed = False n_steps = self.sld_n_steps - self.sld_min_value.value() v = self._slider_to_value(n_steps) if self._accept_value_low(v): self.emit_selection_changed() def sld_max_value_value_changed(self, n_steps): if self._sld_mouse_pressed: v = self._slider_to_value(n_steps) self.le_max_value.setText(self._format_value(v)) def sld_max_value_slider_pressed(self): self._sld_mouse_pressed = True def sld_max_value_slider_released(self): self._sld_mouse_pressed = False n_steps = self.sld_max_value.value() v = self._slider_to_value(n_steps) if self._accept_value_high(v): self.emit_selection_changed() def _format_value(self, value): return f"{value:.8g}" def _round_value(self, value): # Compute rounded value based on formatting used in the line edit boxes # This rounding is needed to properly set the validators s = self._format_value(value) if not isinstance(value, str) else value return self._convert_type(s) def _check_value_type(self, value_type): if value_type not in ("float", "int"): raise ValueError( f"RangeManager.set_value_type(): value type '{value_type}' is not supported" ) def _min_value_validate(self, text=None): text = text if text is not None else self.le_min_value.text() is_valid = self.validator_low.validate(text, 0)[0] == 2 self.le_min_value.setValid(is_valid) return is_valid def _max_value_validate(self, text=None): text = text if text is not None else self.le_max_value.text() is_valid = self.validator_high.validate(text, 0)[0] == 2 self.le_max_value.setValid(is_valid) return is_valid def _convert_type(self, val): if self._value_type == "float": return float(val) else: # Convert to int (input may be float or text string). # We want to round the value to the nearest int. return round(float(val)) def _slider_to_value(self, sld_n): v = self._range_low + (self._range_high - self._range_low) * (sld_n / self.sld_n_steps) return self._convert_type(v) def _value_to_slider(self, value): rng = self._range_high - self._range_low if rng > 1e-30: return round((value - self._range_low) / rng * self.sld_n_steps) else: return 0 def _accept_value_low(self, val): val = self._convert_type(val) val_max = self._value_high - self._range_min_diff val = val if val <= val_max else val_max return self.set_selection(value_low=val) def _accept_value_high(self, val): val = self._convert_type(val) val_min = self._value_low + self._range_min_diff val = val if val >= val_min else val_min return self.set_selection(value_high=val) def _adjust_min_diff(self): if self._value_type == "float": self._range_min_diff = (self._range_high - self._range_low ) * self._selection_to_range_min else: self._range_min_diff = 1 def _adjust_validators(self): """Set the range for validators based on full range and the selected range.""" if self._value_type == "float": # Validator type: QDoubleValidator # The range is set a little wider (1% wider) in order to cover the 'true' # boundary value. 'decimals=-1' - it seems that the precision is getting ignored. self.validator_low.setRange( self._round_value(self._range_low), self._round_value(self._value_high - self._range_min_diff * 0.99), decimals=-1, ) self.validator_high.setRange( self._round_value(self._value_low + self._range_min_diff * 0.99), self._round_value(self._range_high), decimals=-1, ) else: # Validator type: QIntValidator # With integer arithmetic we can set the range precisely self.validator_low.setRange( round(self._range_low), round(self._value_high - self._range_min_diff)) self.validator_high.setRange( round(self._value_low + self._range_min_diff), round(self._range_high)) def setAlignment(self, flags): """ Set text alignment in QLineEdit widgets Parameters ---------- flags: Qt.Alignment flags flags that set alignment of text in QLineEdit widgets For example `Qt.AlignCenter`. The default settings for the widget is `Qt.AlignRight | Qt.AlignVCenter`. """ self.le_min_value.setAlignment(flags) self.le_max_value.setAlignment(flags) def setBackground(self, rgb): """ Set background color of the widget. Similar to QTableWidgetItem.setBackground, but accepting a tuple of RGB values instead of QBrush. Parameters ---------- rgb: tuple(int) RGB color in the form of (R, G, B) """ self.setStyleSheet( get_background_css(rgb, widget="QWidget", editable=False)) self.le_min_value.setStyleSheet( get_background_css(rgb, widget="QLineEdit", editable=True)) self.le_max_value.setStyleSheet( get_background_css(rgb, widget="QLineEdit", editable=True)) def setTextColor(self, rgb): """ Set text color for the widget. This color is used in 'normal' (valid) state. Parameters ---------- rgb: tuple(int) RGB color in the form of (R, G, B) """ color = QColor(*rgb) pal = self.le_min_value.palette() pal.setColor(QPalette.Text, color) self.le_min_value.setPalette(pal) pal = self.le_max_value.palette() pal.setColor(QPalette.Text, color) self.le_max_value.setPalette(pal) def set_value_type(self, value_type="float"): """ Set value type for the range widget. The value type determines the type and format of the displayed and returned values and the type of the validator used by the line edit widgets. The current choices are: "float" used for working with ranges expressed as floating point (double) numbers; "int" is intended for ranges expressed as integers. Parameters ---------- value_type: str Type of values managed by the widget. The choices are "float" and "int". `ValueError` is raised if wrong value is supplied. Returns ------- True - selected range was changed when full range was changed, False otherwise. """ self._check_value_type(value_type) self._value_type = value_type # We don't set validators for QLineEdit widgets. Instead we call validators # explicitly when the input changes. Validators are used to indicate invalid # inputs and prevent user from accepting invalid inputs. There is no goal # to prevent users from typing invalid expressions. if self._value_type == "float": self.validator_low = DoubleValidatorRelaxed() self.validator_high = DoubleValidatorRelaxed() else: self.validator_low = IntValidatorRelaxed() self.validator_high = IntValidatorRelaxed() # Completely reset the widget return self.set_range(self._range_low, self._range_high) def set_range(self, low, high): """ Set the full range of the RangeManager widget. The range may not be negative or zero: `low` must be strictly smaller than `high`. The `ValueError` is raised if `low >= high`. The function will not emit `selection_changed` signal. Call `emit_selection_changed()` method to emit the signal. Parameters ---------- low, high: float or int lower and upper boundaries of the full range Returns ------- True - selected range was changed when full range was changed, False otherwise. """ low, high = self._convert_type(low), self._convert_type(high) # Check range if low >= high: raise ValueError( f"RangeManager.set_range(): incorrect range: low > high ({low} > {high})" ) def _compute_new_value(val_old): """ Adjust the current value so that the selected range covers the same fraction of the total range. """ range_old = self._range_high - self._range_low range_new = high - low return (val_old - self._range_low) / range_old * range_new + low new_value_low = _compute_new_value(self._value_low) new_value_high = _compute_new_value(self._value_high) self._range_high = high self._range_low = low self._value_per_step = (self._range_high - self._range_low) / (self.sld_n_steps - 1) self._range_min_diff = (self._range_high - self._range_low) * self._selection_to_range_min return self.set_selection(value_low=new_value_low, value_high=new_value_high) def set_selection(self, *, value_low=None, value_high=None): """ Set the selected range. The function may be used to set only lower or upper boundary. The function will not emit `selection_changed` signal. Call `emit_selection_changed()` method to emit the signal. Parameters ---------- value_low: float, int or None lower boundary of the selected range. If `None`, then the lower boundary is not changed. If `value_low` is outside the full range, then it is clipped. value_high: float, int or None upper boundary of the selected range. If `None`, then the upper boundary is not changed. If `value_low` is outside the full range, then it is clipped. Returns ------- True - selected range changed, False - selected range stayed the same """ old_low, old_high = self._value_low, self._value_high if value_low is not None or value_high is not None: self._adjust_min_diff() if value_low is not None: value_low = self._convert_type(value_low) value_low = max(min(value_low, self._range_high), self._range_low) self._value_low = value_low if value_high is not None: value_high = self._convert_type(value_high) value_high = max(min(value_high, self._range_high), self._range_low) self._value_high = value_high # Exceptional case when the selection is smaller than the minimum selected # range (or negative). Adjust the range: start at the specified 'low' value # and cover the minimum selectable range; if 'high' value exceeds the top # of the full range, then shift the selected range downwards to fit within # the full range if self._value_high < self._value_low + self._range_min_diff: self._value_high = self._value_low + self._range_min_diff if self._value_high > self._range_high: self._value_high = self._range_high self._value_low = self._range_high - self._range_min_diff self._adjust_validators() if value_low is not None: self.sld_min_value.setValue(self.sld_n_steps - self._value_to_slider(self._value_low)) self.le_min_value.setText(self._format_value(self._value_low)) if value_high is not None: self.sld_max_value.setValue(self._value_to_slider( self._value_high)) self.le_max_value.setText(self._format_value(self._value_high)) # Return True if selection changed return (old_low != self._value_low) or (old_high != self._value_high) def reset(self): """ Reset the selected range to full range of the RangeManager widget. The method will not emit `selection_changed` signal. Call `emit_selection_changed()` to emit the signal. Returns ------- True - selected range changed, False - selected range stayed the same """ return self.set_selection(value_low=self._range_low, value_high=self._range_high) def get_range(self): """ Get the full range Returns ------- tuple `(range_low, range_high)`, the values of `range_low` and `range_high` may be `int` or `float` type depending on the type set by `set_value_type()` method. """ return self._range_low, self._range_high def get_selection(self): """ Get the selected range Returns ------- tuple `(v_low, v_high)`, the values of `v_low` and `v_high` may be `int` or `float` type depending on the type set by `set_value_type()` method. """ return self._value_low, self._value_high def emit_selection_changed(self): """ Emit `selection_changed` signal that passes the selected range as parameters. Note, that the parameters of the signal are ALWAYS `float`. """ v_low = self._convert_type(self._value_low) v_high = self._convert_type(self._value_high) logger.debug( f"RangeManager ({self._name}): Emitting the signal 'selection_changed'. " f"Selection: ({v_low}, {v_high})") self.selection_changed.emit(v_low, v_high, self._name)