class SliderWidget(HLayoutMixin, LineEditWidget): """ IntItem with Slider """ DATA_TYPE = int def __init__(self, item, parent_layout): super(SliderWidget, self).__init__(item, parent_layout) self.slider = self.vmin = self.vmax = None if item.get_prop_value("display", "slider"): self.vmin = item.get_prop_value("data", "min") self.vmax = item.get_prop_value("data", "max") assert ( self.vmin is not None and self.vmax is not None ), "SliderWidget requires that item min/max have been defined" self.slider = QSlider() self.slider.setOrientation(Qt.Horizontal) self.setup_slider(item) self.slider.valueChanged.connect(self.value_changed) self.group.addWidget(self.slider) def value_to_slider(self, value): return value def slider_to_value(self, value): return value def setup_slider(self, item): self.slider.setRange(self.vmin, self.vmax) def update(self, value): """Reimplement LineEditWidget method""" LineEditWidget.update(self, value) if self.slider is not None and isinstance(value, self.DATA_TYPE): self.slider.blockSignals(True) self.slider.setValue(self.value_to_slider(value)) self.slider.blockSignals(False) def value_changed(self, ivalue): """Update the lineedit""" value = str(self.slider_to_value(ivalue)) self.edit.setText(value) self.update(value)
class ChannelControlsWidget(QWidget): def __init__(self, controller: 'IMCController', parent: Optional[QWidget] = None): super(ChannelControlsWidget, self).__init__(parent) self._controller = controller self._opacity_slider = QSlider(Qt.Horizontal, self) self._opacity_slider.setMinimum(0) self._opacity_slider.setMaximum(100) self._contrast_range_slider = QHRangeSlider(parent=self) self._gamma_slider = QSlider(Qt.Horizontal, self) self._gamma_slider.setMinimum(2) self._gamma_slider.setMaximum(200) self._color_picker = ColorPicker(self) self._blending_combo_box = QComboBox(self) self._blending_combo_box.addItems(Blending.keys()) self._interpolation_combo_box = QComboBox(self) self._interpolation_combo_box.addItems(Interpolation.keys()) layout = QFormLayout(self) layout.addRow('Opacity:', self._opacity_slider) layout.addRow('Contrast:', self._contrast_range_slider) layout.addRow('Gamma:', self._gamma_slider) layout.addRow('Color:', self._color_picker) layout.addRow('Blending:', self._blending_combo_box) layout.addRow('Interpolation:', self._interpolation_combo_box) self.setLayout(layout) # noinspection PyUnresolvedReferences @self._opacity_slider.valueChanged.connect def on_opacity_slider_value_changed(value: int): for channel in self._controller.selected_channels: channel.opacity = value / 100 # noinspection PyUnresolvedReferences @self._contrast_range_slider.valuesChanged.connect def on_contrast_range_slider_values_changed(values: Tuple[float, float]): for channel in self._controller.selected_channels: contrast_limits_range_min = min( (layer.contrast_limits_range[0] for layer in channel.shown_imc_file_acquisition_layers.values()), default=values[0]) contrast_limits_range_max = max( (layer.contrast_limits_range[1] for layer in channel.shown_imc_file_acquisition_layers.values()), default=values[1]) channel.contrast_limits = (max(values[0], contrast_limits_range_min), min(values[1], contrast_limits_range_max)) # noinspection PyUnresolvedReferences @self._gamma_slider.valueChanged.connect def on_gamma_slider_value_changed(value: int): for channel in self._controller.selected_channels: channel.gamma = value / 100 # noinspection PyUnresolvedReferences @self._color_picker.events.color_changed.connect def on_color_picker_color_changed(color: QColor): for channel in self._controller.selected_channels: channel.color = (color.red() / 255, color.green() / 255, color.blue() / 255, color.alpha() / 255) # noinspection PyUnresolvedReferences blending_combo_box_activated = self._blending_combo_box.activated[str] # noinspection PyUnresolvedReferences @blending_combo_box_activated.connect def on_blending_combo_box_activated(text: str): for channel in self._controller.selected_channels: channel.blending = text # noinspection PyUnresolvedReferences interpolation_combo_box_activated = self._interpolation_combo_box.activated[ str] # noinspection PyUnresolvedReferences @interpolation_combo_box_activated.connect def on_interpolation_combo_box_activated(text: str): for channel in self._controller.selected_channels: channel.interpolation = text self.refresh() def refresh(self): channels = self._controller.selected_channels if len(channels) > 0: mean_opacity = sum(channel.opacity for channel in channels) / len(channels) self._opacity_slider.blockSignals(True) self._opacity_slider.setValue(int(mean_opacity * 100)) self._opacity_slider.blockSignals(False) contrast_limits_channels = [ channel for channel in channels if channel.contrast_limits is not None ] if len(contrast_limits_channels) > 0: layers = [ layer for channel in channels for layer in channel.shown_imc_file_acquisition_layers.values() ] if len(layers) > 0: contrast_limits_min = min( channel.contrast_limits[0] for channel in contrast_limits_channels) contrast_limits_max = max( channel.contrast_limits[1] for channel in contrast_limits_channels) contrast_limits_range_min = min( layer.contrast_limits_range[0] for layer in layers) contrast_limits_range_max = max( layer.contrast_limits_range[1] for layer in layers) contrast_limits_min = max(contrast_limits_min, contrast_limits_range_min) contrast_limits_max = min(contrast_limits_max, contrast_limits_range_max) self._contrast_range_slider.blockSignals(True) self._contrast_range_slider.setRange( (contrast_limits_range_min, contrast_limits_range_max)) self._contrast_range_slider.setValues( (contrast_limits_min, contrast_limits_max)) self._contrast_range_slider.blockSignals(False) mean_gamma = sum(channel.gamma for channel in channels) / len(channels) self._gamma_slider.blockSignals(True) self._gamma_slider.setValue(int(mean_gamma * 100)) self._gamma_slider.blockSignals(False) color = tuple(int(x * 255) for x in channels[0].color) self._color_picker.blockSignals(True) self._color_picker.color = QColor(*color) self._color_picker.blockSignals(False) index = self._blending_combo_box.findText(channels[0].blending, Qt.MatchFixedString) self._blending_combo_box.blockSignals(True) self._blending_combo_box.setCurrentIndex(index) self._blending_combo_box.blockSignals(False) index = self._interpolation_combo_box.findText( channels[0].interpolation, Qt.MatchFixedString) self._interpolation_combo_box.blockSignals(True) self._interpolation_combo_box.setCurrentIndex(index) self._interpolation_combo_box.blockSignals(False)
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 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)