class SPipelineListWidget(QWidget): """Widget that contains a list of widgets to build a pipeline""" def __init__(self): super().__init__() self.input_layer_name = '' self._count = 0 list_widget = QWidget() list_widget.setAttribute(QtCore.Qt.WA_StyledBackground, True) scroll_widget = QScrollArea() scroll_widget.setWidgetResizable(True) scroll_widget.setWidget(list_widget) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(scroll_widget) self.setLayout(layout) self.layout = QVBoxLayout() self.layout.addWidget(QWidget(), 1, QtCore.Qt.AlignTop) list_widget.setLayout(self.layout) def add_widget(self, name, widget): """add a widget to the pipeline""" self._count += 1 widget_ = SProcessInListWidget(str(self._count), name, widget) widget_.remove.connect(self._on_remove_widget) index = self.layout.count()-1 self.layout.insertWidget(index, widget_, 0, QtCore.Qt.AlignTop) def _on_remove_widget(self, uuid): """Remove a filter Parameters ---------- uuid: int Unique id of the filter to remove """ # remove from widget list for i in range(self.layout.count()): item = self.layout.itemAt(i) widget = item.widget() if widget: if hasattr(widget, 'uuid'): if widget.uuid == uuid: item_d = self.layout.takeAt(i) item_d.widget().deleteLater() break def widgets(self): """Returns all the widgets in the pipeline""" widgets_ = list() for i in range(self.layout.count()): item = self.layout.itemAt(i) widget = item.widget() if widget: if hasattr(widget, 'uuid'): widgets_.append(widget) #widgets_.append(widget.process_widget) return widgets_
class TestViewManager: def setup_method(self): self._base_layout = QVBoxLayout() self._view_manager = ViewManager(self._base_layout) def test_current_view(self): # Arrange view1: MagicMock = MockView() view2: MagicMock = MockView() # Act / Assert self._view_manager.load_view(view1) assert self._view_manager.current_view == view1 self._view_manager.load_view(view2) assert self._view_manager.current_view == view2 def test_load_view_no_template(self): # Arrange view = MockView() # Act self._view_manager.load_view(view) # Assert assert self._base_layout.itemAt(0).widget() == view assert view.load_called == True def test_load_view_with_template(self): # Arrange view = MockView(template_class=MockViewTemplate1) # Act self._view_manager.load_view(view) # Assert assert self._view_manager.current_view == view.template assert self._base_layout.itemAt(0).widget() == view.template assert view.template.get_container().layout().itemAt( 0).widget() == view assert view.template.load_called == True assert view.load_called == True def test_load_view_with_nested_templates(self): # Arrange view = MockView(template_class=MockViewTemplate2) # Act self._view_manager.load_view(view) # Assert assert self._view_manager.current_view == view.template.template assert self._base_layout.itemAt(0).widget() == view.template.template assert view.template.load_called == True assert view.template.template.load_called == True assert view.load_called == True def test_load_view_null_view(self): # Assert with pytest.raises(ValueError): self._view_manager.load_view(None)
class SimpleMeasurements(QWidget): def __init__(self, settings: StackSettings, parent=None): super().__init__(parent) self.settings = settings self.calculate_btn = QPushButton("Calculate") self.calculate_btn.clicked.connect(self.calculate) self.result_view = QTableWidget() self.channel_select = ChannelComboBox() self.units_select = EnumComboBox(Units) self.units_select.set_value(self.settings.get("simple_measurements.units", Units.nm)) self.units_select.currentIndexChanged.connect(self.change_units) layout = QHBoxLayout() self.measurement_layout = QVBoxLayout() l1 = QHBoxLayout() l1.addWidget(QLabel("Units")) l1.addWidget(self.units_select) self.measurement_layout.addLayout(l1) l2 = QHBoxLayout() l2.addWidget(QLabel("Channel")) l2.addWidget(self.channel_select) self.measurement_layout.addLayout(l2) layout.addLayout(self.measurement_layout) result_layout = QVBoxLayout() result_layout.addWidget(self.result_view) result_layout.addWidget(self.calculate_btn) layout.addLayout(result_layout) self.setLayout(layout) self.setWindowTitle("Measurement") if self.window() == self: try: geometry = self.settings.get_from_profile("simple_measurement_window_geometry") self.restoreGeometry(QByteArray.fromHex(bytes(geometry, "ascii"))) except KeyError: pass def closeEvent(self, event: QCloseEvent) -> None: """ Save geometry if widget is used as standalone window. """ if self.window() == self: self.settings.set_in_profile( "simple_measurement_window_geometry", self.saveGeometry().toHex().data().decode("ascii") ) super().closeEvent(event) def calculate(self): if self.settings.segmentation is None: QMessageBox.warning(self, "No segmentation", "need segmentation to work") return to_calculate = [] for i in range(2, self.measurement_layout.count()): # noinspection PyTypeChecker chk: QCheckBox = self.measurement_layout.itemAt(i).widget() if chk.isChecked(): leaf: Leaf = MEASUREMENT_DICT[chk.text()].get_starting_leaf() to_calculate.append(leaf.replace_(per_component=PerComponent.Yes, area=AreaType.ROI)) if not to_calculate: QMessageBox.warning(self, "No measurement", "Select at least one measurement") return profile = MeasurementProfile("", [MeasurementEntry(x.name, x) for x in to_calculate]) dial = ExecuteFunctionDialog( profile.calculate, kwargs={ "channel": self.settings.image.get_channel(self.channel_select.get_value()), "segmentation": self.settings.segmentation, "mask": None, "voxel_size": self.settings.image.spacing, "result_units": self.units_select.get_value(), }, ) dial.exec() result: MeasurementResult = dial.get_result() values = result.get_separated() labels = result.get_labels() self.result_view.clear() self.result_view.setColumnCount(len(values) + 1) self.result_view.setRowCount(len(labels)) for i, val in enumerate(labels): self.result_view.setItem(i, 0, QTableWidgetItem(val)) for j, values_list in enumerate(values): for i, val in enumerate(values_list): self.result_view.setItem(i, j + 1, QTableWidgetItem(str(val))) def _clean_measurements(self): selected = set() for _ in range(self.measurement_layout.count() - 2): # noinspection PyTypeChecker chk: QCheckBox = self.measurement_layout.takeAt(2).widget() if chk.isChecked(): selected.add(chk.text()) chk.deleteLater() return selected def event(self, event: QEvent) -> bool: if event.type() == QEvent.WindowActivate: self.channel_select.change_channels_num(self.settings.image.channels) selected = self._clean_measurements() for val in MEASUREMENT_DICT.values(): area = val.get_starting_leaf().area pc = val.get_starting_leaf().per_component if ( val.get_fields() or (area is not None and area != AreaType.ROI) or (pc is not None and pc != PerComponent.Yes) ): continue text = val.get_name() chk = QCheckBox(text) if text in selected: chk.setChecked(True) self.measurement_layout.addWidget(chk) return super().event(event) def change_units(self): self.settings.set("simple_measurements.units", self.units_select.get_value())
class Gui(QWidget): """This Gui takes a napari as parameter and infiltrates it. It adds some buttons for categories of operations. """ # I don't like global variables. # But that's what it is. # It's a global variable. # haesleinhuepf global_last_filter_applied = None def __init__(self, viewer): super().__init__() self.viewer = viewer self.items = [] self.layout = QVBoxLayout() self._init_gui() self.setLayout(self.layout) self.dock_widget = None def _init_gui(self, main_menu: bool = True): """Switches the GUI internally between a main menu where you can select categories and a sub menu where you can keep results or cancel processing. """ # remove all buttons first for i in reversed(range(self.layout.count())): self.layout.itemAt(i).widget().setParent(None) if main_menu: self._add_button("Filter", self._add_filter_clicked) self._add_button("Binarize", self._add_binarize_clicked) self._add_button("Combine", self._add_combine_clicked) self._add_button("Label", self._add_label_clicked) self._add_button("Measure", self._measure_clicked) else: self._add_button("Done", self._done_clicked) self._add_button("Cancel", self._cancel_clicked) self.setLayout(self.layout) def _add_button(self, title: str, handler: callable): btn = QPushButton(title, self) btn.clicked.connect(handler) self.layout.addWidget(btn) self.items.append(btn) def _add_filter_clicked(self): self._activate(filter) def _add_binarize_clicked(self): self._activate(binarize) def _add_combine_clicked(self): self._activate(combine) def _add_label_clicked(self): self._activate(label) def _measure_clicked(self): self._activate(measure) def _activate(self, magicgui): for layer in viewer.layers: layer.visible = False Gui.global_last_filter_applied = None self.filter_gui = magicgui.Gui() self.dock_widget = viewer.window.add_dock_widget(self.filter_gui, area='right') self._init_gui(False) def _done_clicked(self): # magicqui somehow internally keeps the layer. # Thus, we need to destroy magicguis layer and add it again if Gui.global_last_filter_applied is not None: data = viewer.layers.selected[0].data viewer.layers.remove_selected() if isinstance(Gui.global_last_filter_applied, Label): viewer.add_labels(data, name=str(Gui.global_last_filter_applied)) else: viewer.add_image(data, name=str(Gui.global_last_filter_applied)) self.viewer.window.remove_dock_widget(self.dock_widget) self._init_gui(True) def _cancel_clicked(self): if Gui.global_last_filter_applied is not None: viewer.layers.remove_selected() print("Main menu") self.viewer.window.remove_dock_widget(self.dock_widget) self._init_gui(True)
class QtLayerList(QScrollArea): def __init__(self, layers): super().__init__() self.layers = layers self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollWidget = QWidget() self.setWidget(scrollWidget) self.vbox_layout = QVBoxLayout(scrollWidget) self.vbox_layout.addWidget(QtDivider()) self.vbox_layout.addStretch(1) self.vbox_layout.setContentsMargins(0, 0, 0, 0) self.vbox_layout.setSpacing(2) self.centers = [] # Create a timer to be used for autoscrolling the layers list up and # down when dragging a layer near the end of the displayed area self.dragTimer = QTimer() self.dragTimer.setSingleShot(False) self.dragTimer.setInterval(20) self.dragTimer.timeout.connect(self._force_scroll) self._scroll_up = True self._min_scroll_region = 24 self.setAcceptDrops(True) self.setToolTip('Layer list') self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.layers.events.added.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(self._reorder) self.drag_start_position = np.zeros(2) self.drag_name = None def _add(self, event): """Insert widget for layer `event.item` at index `event.index`.""" layer = event.item total = len(self.layers) index = 2 * (total - event.index) - 1 widget = QtLayerWidget(layer) self.vbox_layout.insertWidget(index, widget) self.vbox_layout.insertWidget(index + 1, QtDivider()) layer.events.select.connect(self._scroll_on_select) def _remove(self, event): """Remove widget for layer at index `event.index`.""" layer_index = event.index total = len(self.layers) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) + 1 widget = self.vbox_layout.itemAt(index).widget() divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(widget) widget.deleteLater() self.vbox_layout.removeWidget(divider) divider.deleteLater() def _reorder(self, event=None): """Reorders list of layer widgets by looping through all widgets in list sequentially removing them and inserting them into the correct place in final list. """ total = len(self.layers) # Create list of the current property and divider widgets widgets = [ self.vbox_layout.itemAt(i + 1).widget() for i in range(2 * total) ] # Take every other widget to ignore the dividers and get just the # property widgets indices = [ self.layers.index(w.layer) for i, w in enumerate(widgets) if i % 2 == 0 ] # Move through the layers in order for i in range(total): # Find index of property widget in list of the current layer index = 2 * indices.index(i) widget = widgets[index] divider = widgets[index + 1] # Check if current index does not match new index index_current = self.vbox_layout.indexOf(widget) index_new = 2 * (total - i) - 1 if index_current != index_new: # Remove that property widget and divider self.vbox_layout.removeWidget(widget) self.vbox_layout.removeWidget(divider) # Insert the property widget and divider into new location self.vbox_layout.insertWidget(index_new, widget) self.vbox_layout.insertWidget(index_new + 1, divider) def _force_scroll(self): """Force the scroll bar to automattically scroll either up or down.""" cur_value = self.verticalScrollBar().value() if self._scroll_up: new_value = cur_value - self.verticalScrollBar().singleStep() / 4 if new_value < 0: new_value = 0 self.verticalScrollBar().setValue(new_value) else: new_value = cur_value + self.verticalScrollBar().singleStep() / 4 if new_value > self.verticalScrollBar().maximum(): new_value = self.verticalScrollBar().maximum() self.verticalScrollBar().setValue(new_value) def _scroll_on_select(self, event): """Scroll to ensure that the currently selected layer is visible.""" layer = event.source self._ensure_visible(layer) def _ensure_visible(self, layer): """Ensure layer widget for at particular layer is visible.""" total = len(self.layers) layer_index = self.layers.index(layer) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) - 1 widget = self.vbox_layout.itemAt(index).widget() self.ensureWidgetVisible(widget) def keyPressEvent(self, event): event.ignore() def keyReleaseEvent(self, event): event.ignore() def mousePressEvent(self, event): # Check if mouse press happens on a layer properties widget or # a child of such a widget. If not, the press has happended on the # Layers Widget itself and should be ignored. widget = self.childAt(event.pos()) layer = ( getattr(widget, 'layer', None) or getattr(widget.parentWidget(), 'layer', None) or getattr(widget.parentWidget().parentWidget(), 'layer', None) ) if layer is not None: self.drag_start_position = np.array( [event.pos().x(), event.pos().y()] ) self.drag_name = layer.name else: self.drag_name = None def mouseReleaseEvent(self, event): if self.drag_name is None: # Unselect all the layers if not dragging a layer self.layers.unselect_all() return modifiers = event.modifiers() layer = self.layers[self.drag_name] if modifiers == Qt.ShiftModifier: # If shift select all layers in between currently selected one and # clicked one index = self.layers.index(layer) lastSelected = None for i in range(len(self.layers)): if self.layers[i].selected: lastSelected = i r = [index, lastSelected] r.sort() for i in range(r[0], r[1] + 1): self.layers[i].selected = True elif modifiers == Qt.ControlModifier: # If control click toggle selected state layer.selected = not layer.selected else: # If otherwise unselect all and leave clicked one selected self.layers.unselect_all(ignore=layer) layer.selected = True def mouseMoveEvent(self, event): position = np.array([event.pos().x(), event.pos().y()]) distance = np.linalg.norm(position - self.drag_start_position) if ( distance < QApplication.startDragDistance() or self.drag_name is None ): return mimeData = QMimeData() mimeData.setText(self.drag_name) drag = QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) drag.exec_() if self.drag_name is not None: index = self.layers.index(self.drag_name) layer = self.layers[index] self._ensure_visible(layer) def dragLeaveEvent(self, event): """Unselects layer dividers.""" event.ignore() self.dragTimer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): if event.source() == self: event.accept() divs = [] for i in range(0, self.vbox_layout.count(), 2): widget = self.vbox_layout.itemAt(i).widget() divs.append(widget.y() + widget.frameGeometry().height() / 2) self.centers = [ (divs[i + 1] + divs[i]) / 2 for i in range(len(divs) - 1) ] else: event.ignore() def dragMoveEvent(self, event): """Set the appropriate layers list divider to be highlighted when dragging a layer to a new position in the layers list. """ max_height = self.frameGeometry().height() if ( event.pos().y() < self._min_scroll_region and not self.dragTimer.isActive() ): self._scroll_up = True self.dragTimer.start() elif ( event.pos().y() > max_height - self._min_scroll_region and not self.dragTimer.isActive() ): self._scroll_up = False self.dragTimer.start() elif ( self.dragTimer.isActive() and event.pos().y() >= self._min_scroll_region and event.pos().y() <= max_height - self._min_scroll_region ): self.dragTimer.stop() # Determine which widget center is the mouse currently closed to cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) # Determine the current location of the widget being dragged total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self.drag_name) # If the widget being dragged hasn't moved above or below any other # widgets then don't highlight any dividers selected = not (insert == index) and not (insert - 1 == index) # Set the selected state of all the dividers for i in range(0, self.vbox_layout.count(), 2): if i == 2 * divider_index: self.vbox_layout.itemAt(i).widget().setSelected(selected) else: self.vbox_layout.itemAt(i).widget().setSelected(False) def dropEvent(self, event): if self.dragTimer.isActive(): self.dragTimer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self.drag_name) if index != insert and index + 1 != insert: if insert >= index: insert -= 1 self.layers.move_selected(index, insert) event.accept()
class QtLayerList(QScrollArea): """Widget storing a list of all the layers present in the current window. Parameters ---------- layers : napari.components.LayerList The layer list to track and display. Attributes ---------- centers : list List of layer widgets center coordinates. layers : napari.components.LayerList The layer list to track and display. vbox_layout : QVBoxLayout The layout instance in which the layouts appear. """ def __init__(self, layers: 'LayerList'): super().__init__() self.layers = layers self.setAttribute(Qt.WA_DeleteOnClose) self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollWidget = QWidget() self.setWidget(scrollWidget) self.vbox_layout = QVBoxLayout(scrollWidget) self.vbox_layout.addWidget(QtDivider()) self.vbox_layout.addStretch(1) self.vbox_layout.setContentsMargins(0, 0, 0, 0) self.vbox_layout.setSpacing(2) self.centers = [] # Create a timer to be used for autoscrolling the layers list up and # down when dragging a layer near the end of the displayed area self._drag_timer = QTimer() self._drag_timer.setSingleShot(False) self._drag_timer.setInterval(20) self._drag_timer.timeout.connect(self._force_scroll) self._scroll_up = True self._min_scroll_region = 24 self.setAcceptDrops(True) self.setToolTip(trans._('Layer list')) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) self.layers.events.inserted.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(self._reorder) self.layers.selection.events.changed.connect(self._on_selection_change) self._drag_start_position = np.zeros(2) self._drag_name = None self.chunk_receiver = _create_chunk_receiver(self) def _on_selection_change(self, event): for layer in event.added: w = self._find_widget(layer) if w: w.setSelected(True) for layer in event.removed: w = self._find_widget(layer) if w: w.setSelected(False) if event.added: self._ensure_visible(list(event.added)[-1]) def _find_widget(self, layer): for i in range(self.vbox_layout.count()): w = self.vbox_layout.itemAt(i).widget() if getattr(w, 'layer', None) == layer: return w def _add(self, event): """Insert widget for layer `event.value` at index `event.index`. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ layer = event.value total = len(self.layers) index = 2 * (total - event.index) - 1 widget = QtLayerWidget(layer, selected=layer in self.layers.selection) self.vbox_layout.insertWidget(index, widget) self.vbox_layout.insertWidget(index + 1, QtDivider()) def _remove(self, event): """Remove widget for layer at index `event.index`. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ layer_index = event.index total = len(self.layers) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) + 1 widget = self.vbox_layout.itemAt(index).widget() divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(widget) disconnect_events(widget.layer.events, self) widget.close() self.vbox_layout.removeWidget(divider) divider.deleteLater() def _reorder(self, event=None): """Reorder list of layer widgets. Loops through all widgets in list, sequentially removing them and inserting them into the correct place in the final list. Parameters ---------- event : napari.utils.event.Event, optional The napari event that triggered this method. """ total = len(self.layers) # Create list of the current property and divider widgets widgets = [ self.vbox_layout.itemAt(i + 1).widget() for i in range(2 * total) ] # Take every other widget to ignore the dividers and get just the # property widgets indices = [ self.layers.index(w.layer) for i, w in enumerate(widgets) if i % 2 == 0 ] # Move through the layers in order for i in range(total): # Find index of property widget in list of the current layer index = 2 * indices.index(i) widget = widgets[index] divider = widgets[index + 1] # Check if current index does not match new index index_current = self.vbox_layout.indexOf(widget) index_new = 2 * (total - i) - 1 if index_current != index_new: # Remove that property widget and divider self.vbox_layout.removeWidget(widget) self.vbox_layout.removeWidget(divider) # Insert the property widget and divider into new location self.vbox_layout.insertWidget(index_new, widget) self.vbox_layout.insertWidget(index_new + 1, divider) def _force_scroll(self): """Force the scroll bar to automattically scroll either up or down.""" cur_value = self.verticalScrollBar().value() if self._scroll_up: new_value = cur_value - self.verticalScrollBar().singleStep() / 4 if new_value < 0: new_value = 0 self.verticalScrollBar().setValue(new_value) else: new_value = cur_value + self.verticalScrollBar().singleStep() / 4 if new_value > self.verticalScrollBar().maximum(): new_value = self.verticalScrollBar().maximum() self.verticalScrollBar().setValue(new_value) def _ensure_visible(self, layer): """Ensure layer widget for at particular layer is visible. Parameters ---------- layer : napari.layers.Layer An instance of a napari layer. """ total = len(self.layers) layer_index = self.layers.index(layer) # Find property widget and divider for layer to be removed index = 2 * (total - layer_index) - 1 widget = self.vbox_layout.itemAt(index).widget() self.ensureWidgetVisible(widget) def keyPressEvent(self, event): """Ignore a key press event. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() def keyReleaseEvent(self, event): """Ignore key release event. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() def mousePressEvent(self, event): """Register mouse click if it happens on a layer widget. Checks if mouse press happens on a layer properties widget or a child of such a widget. If not, the press has happened on the Layers Widget itself and should be ignored. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ widget = self.childAt(event.pos()) layer = ( getattr(widget, 'layer', None) or getattr(widget.parentWidget(), 'layer', None) or getattr(widget.parentWidget().parentWidget(), 'layer', None) ) if layer is not None: self._drag_start_position = np.array( [event.pos().x(), event.pos().y()] ) self._drag_name = layer.name else: self._drag_name = None def mouseReleaseEvent(self, event): """Select layer using mouse click. Key modifiers: Shift - If the Shift button is pressed, select all layers in between currently selected one and the clicked one. Control - If the Control button is pressed, mouse click will toggle selected state of the layer. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self._drag_name is None: # Unselect all the layers if not dragging a layer self.layers.selection.active = None return modifiers = event.modifiers() clicked_layer = self.layers[self._drag_name] if modifiers == Qt.ShiftModifier and self.layers.selection._current: # shift-click: select all layers between current and clicked clicked = self.layers.index(clicked_layer) current = self.layers.index(self.layers.selection._current) from_, to_ = sorted([clicked, current]) _to_select = self.layers[slice(from_, to_ + 1)] # inclusive range self.layers.selection.update(_to_select) self.layers.selection._current = clicked_layer elif modifiers == Qt.ControlModifier: # If control click toggle selected state of clicked layer self.layers.selection.toggle(clicked_layer) else: # If otherwise unselect all and leave clicked one selected self.layers.selection.active = clicked_layer def mouseMoveEvent(self, event): """Drag and drop layer with mouse movement. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ position = np.array([event.pos().x(), event.pos().y()]) distance = np.linalg.norm(position - self._drag_start_position) if ( distance < QApplication.startDragDistance() or self._drag_name is None ): return mimeData = QMimeData() mimeData.setText(self._drag_name) drag = QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) drag.exec_() # Check if dragged layer still exists or was deleted during drag names = [layer.name for layer in self.layers] dragged_layer_exists = self._drag_name in names if self._drag_name is not None and dragged_layer_exists: index = self.layers.index(self._drag_name) layer = self.layers[index] self._ensure_visible(layer) def dragLeaveEvent(self, event): """Unselects layer dividers. Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ event.ignore() self._drag_timer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): """Update divider position before dragging layer widget to new position Allows the event to pass through a parent widget to its child widget without doing anything. If we did not use event.ignore() then the parent widget would catch the event and not pass it on to the child. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.source() == self: event.accept() divs = [] for i in range(0, self.vbox_layout.count(), 2): widget = self.vbox_layout.itemAt(i).widget() divs.append(widget.y() + widget.frameGeometry().height() / 2) self.centers = [ (divs[i + 1] + divs[i]) / 2 for i in range(len(divs) - 1) ] else: event.ignore() def dragMoveEvent(self, event): """Highlight appriate divider when dragging layer to new position. Sets the appropriate layers list divider to be highlighted when dragging a layer to a new position in the layers list. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ max_height = self.frameGeometry().height() if ( event.pos().y() < self._min_scroll_region and not self._drag_timer.isActive() ): self._scroll_up = True self._drag_timer.start() elif ( event.pos().y() > max_height - self._min_scroll_region and not self._drag_timer.isActive() ): self._scroll_up = False self._drag_timer.start() elif ( self._drag_timer.isActive() and event.pos().y() >= self._min_scroll_region and event.pos().y() <= max_height - self._min_scroll_region ): self._drag_timer.stop() # Determine which widget center is the mouse currently closed to cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) # Determine the current location of the widget being dragged total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self._drag_name) # If the widget being dragged hasn't moved above or below any other # widgets then don't highlight any dividers selected = not (insert == index) and not (insert - 1 == index) # Set the selected state of all the dividers for i in range(0, self.vbox_layout.count(), 2): if i == 2 * divider_index: self.vbox_layout.itemAt(i).widget().setSelected(selected) else: self.vbox_layout.itemAt(i).widget().setSelected(False) def dropEvent(self, event): """Drop dragged layer widget into new position in the list of layers. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self._drag_timer.isActive(): self._drag_timer.stop() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) cord = event.pos().y() + self.verticalScrollBar().value() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) total = self.vbox_layout.count() // 2 - 1 insert = total - divider_index index = self.layers.index(self._drag_name) if index != insert and index + 1 != insert: if insert >= index: insert -= 1 self.layers.move_selected(index, insert) event.accept()
class QtLayers(QScrollArea): def __init__(self, layers): super().__init__() self.layers = layers self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollWidget = QWidget() self.setWidget(scrollWidget) self.vbox_layout = QVBoxLayout(scrollWidget) self.vbox_layout.addWidget(QtDivider()) self.vbox_layout.addStretch(1) self.vbox_layout.setContentsMargins(0, 0, 0, 0) self.centers = [] self.setAcceptDrops(True) self.setToolTip('Layer list') self.layers.events.added.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(self._reorder) def _add(self, event): """Insert `event.widget` at index `event.index`.""" layer = event.item index = event.index total = len(self.layers) if layer._qt_properties is not None: self.vbox_layout.insertWidget(2 * (total - index) - 1, layer._qt_properties) self.vbox_layout.insertWidget(2 * (total - index), QtDivider()) def _remove(self, event): """Remove layer widget at index `event.index`.""" layer = event.item if layer._qt_properties is not None: index = self.vbox_layout.indexOf(layer._qt_properties) divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(layer._qt_properties) layer._qt_properties.deleteLater() layer._qt_properties = None self.vbox_layout.removeWidget(divider) divider.deleteLater() divider = None def _reorder(self, event): """Reorders list of layer widgets by looping through all widgets in list sequentially removing them and inserting them into the correct place in final list. """ total = len(self.layers) for i in range(total): layer = self.layers[i] if layer._qt_properties is not None: index = self.vbox_layout.indexOf(layer._qt_properties) divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(layer._qt_properties) self.vbox_layout.removeWidget(divider) self.vbox_layout.insertWidget(2 * (total - i) - 1, layer._qt_properties) self.vbox_layout.insertWidget(2 * (total - i), divider) def mouseReleaseEvent(self, event): """Unselects all layer widgets.""" self.layers.unselect_all() def dragLeaveEvent(self, event): """Unselects layer dividers.""" event.ignore() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): event.accept() divs = [] for i in range(0, self.vbox_layout.count(), 2): widget = self.vbox_layout.itemAt(i).widget() divs.append(widget.y() + widget.frameGeometry().height() / 2) self.centers = [(divs[i + 1] + divs[i]) / 2 for i in range(len(divs) - 1)] def dragMoveEvent(self, event): """Set the appropriate layers list divider to be highlighted when dragging a layer to a new position in the layers list. """ # Determine which widget center is the mouse currently closed to cord = event.pos().y() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) # Determine the current location of the widget being dragged layerWidget = event.source() total = self.vbox_layout.count() // 2 - 1 index = total - self.vbox_layout.indexOf(layerWidget) // 2 - 1 insert = total - divider_index # If the widget being dragged hasn't moved above or below any other # widgets then don't highlight any dividers selected = (not (insert == index) and not (insert - 1 == index)) # Set the selected state of all the dividers for i in range(0, self.vbox_layout.count(), 2): if i == 2 * divider_index: self.vbox_layout.itemAt(i).widget().setSelected(selected) else: self.vbox_layout.itemAt(i).widget().setSelected(False) def dropEvent(self, event): for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) cord = event.pos().y() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) layerWidget = event.source() total = self.vbox_layout.count() // 2 - 1 index = total - self.vbox_layout.indexOf(layerWidget) // 2 - 1 insert = total - divider_index if index != insert and index + 1 != insert: if not self.layers[index].selected: self.layers.unselect_all() self.layers[index].selected = True self.layers._move_layers(index, insert) event.accept()
class SimpleMeasurements(QWidget): def __init__(self, settings: StackSettings, parent=None): super().__init__(parent) self.settings = settings self.calculate_btn = QPushButton("Calculate") self.calculate_btn.clicked.connect(self.calculate) self.result_view = QTableWidget() self.channel_select = ChannelComboBox() self.units_select = QEnumComboBox(enum_class=Units) self.units_select.setCurrentEnum( self.settings.get("simple_measurements.units", Units.nm)) self.units_select.currentIndexChanged.connect(self.change_units) self._shift = 2 layout = QHBoxLayout() self.measurement_layout = QVBoxLayout() l1 = QHBoxLayout() l1.addWidget(QLabel("Units")) l1.addWidget(self.units_select) self.measurement_layout.addLayout(l1) l2 = QHBoxLayout() l2.addWidget(QLabel("Channel")) l2.addWidget(self.channel_select) self.measurement_layout.addLayout(l2) layout.addLayout(self.measurement_layout) result_layout = QVBoxLayout() result_layout.addWidget(self.result_view) result_layout.addWidget(self.calculate_btn) layout.addLayout(result_layout) self.setLayout(layout) self.setWindowTitle("Measurement") if self.window() == self: with suppress(KeyError): geometry = self.settings.get_from_profile( "simple_measurement_window_geometry") self.restoreGeometry( QByteArray.fromHex(bytes(geometry, "ascii"))) def closeEvent(self, event: QCloseEvent) -> None: """ Save geometry if widget is used as standalone window. """ if self.window() == self: self.settings.set_in_profile( "simple_measurement_window_geometry", self.saveGeometry().toHex().data().decode("ascii")) super().closeEvent(event) def calculate(self): if self.settings.roi is None: QMessageBox.warning(self, "No segmentation", "need segmentation to work") return to_calculate = [] for i in range(self._shift, self.measurement_layout.count()): # noinspection PyTypeChecker chk: QCheckBox = self.measurement_layout.itemAt(i).widget() if chk.isChecked(): leaf: Leaf = MEASUREMENT_DICT[chk.text()].get_starting_leaf() to_calculate.append( leaf.replace_(per_component=PerComponent.Yes, area=AreaType.ROI)) if not to_calculate: QMessageBox.warning(self, "No measurement", "Select at least one measurement") return profile = MeasurementProfile( "", [MeasurementEntry(x.name, x) for x in to_calculate]) dial = ExecuteFunctionDialog( profile.calculate, kwargs={ "image": self.settings.image, "channel_num": self.channel_select.get_value(), "roi": self.settings.roi_info, "result_units": self.units_select.currentEnum(), }, ) dial.exec_() result: MeasurementResult = dial.get_result() values = result.get_separated() labels = result.get_labels() self.result_view.clear() self.result_view.setColumnCount(len(values) + 1) self.result_view.setRowCount(len(labels)) for i, val in enumerate(labels): self.result_view.setItem(i, 0, QTableWidgetItem(val)) for j, values_list in enumerate(values): for i, val in enumerate(values_list): self.result_view.setItem(i, j + 1, QTableWidgetItem(str(val))) def _clean_measurements(self): selected = set() for _ in range(self.measurement_layout.count() - self._shift): # noinspection PyTypeChecker chk: QCheckBox = self.measurement_layout.takeAt( self._shift).widget() if chk.isChecked(): selected.add(chk.text()) chk.deleteLater() return selected def refresh_measurements(self): selected = self._clean_measurements() for val in MEASUREMENT_DICT.values(): area = val.get_starting_leaf().area pc = val.get_starting_leaf().per_component if (val.get_fields() or (area is not None and area != AreaType.ROI) or (pc is not None and pc != PerComponent.Yes)): continue text = val.get_name() chk = QCheckBox(text) if text in selected: chk.setChecked(True) self.measurement_layout.addWidget(chk) def keyPressEvent(self, e: QKeyEvent): if not e.modifiers() & Qt.ControlModifier: return selected = self.result_view.selectedRanges() if e.key() == Qt.Key_C: # copy s = "" for r in range(selected[0].topRow(), selected[0].bottomRow() + 1): for c in range(selected[0].leftColumn(), selected[0].rightColumn() + 1): try: s += str(self.result_view.item(r, c).text()) + "\t" except AttributeError: s += "\t" s = s[:-1] + "\n" # eliminate last '\t' QApplication.clipboard().setText(s) def event(self, event: QEvent) -> bool: if event.type() == QEvent.WindowActivate: if self.settings.image is not None: self.channel_select.change_channels_num( self.settings.image.channels) self.refresh_measurements() return super().event(event) def change_units(self): self.settings.set("simple_measurements.units", self.units_select.currentEnum())
class MultiView(QWidget): signalSyncSlices = Signal(int) signalAxisChanged = Signal(int) def __init__(self, views=None, data=None, labels=None, layout="horizontal", parent=None, **kwargs): super(MultiView, self).__init__(parent, **kwargs) self.parent = parent if layout == "horizontal": self.layout = QHBoxLayout(self) elif layout == "vertical": self.layout = QVBoxLayout(self) else: raise ValueError("layout must be 'horizontal' or 'vertical'.") self.views = [] if views is not None: for view in views: self.addView(view) if data is not None: if data.ndim not in (3, 4): raise IndexError("Dimensionality of data must be 3 or 4.") if data.ndim == 3: data = data[np.newaxis, ...] for ax in range(data.shape[0]): current_label = labels[ax] if labels is not None else None self.addView( InteractionVolumeView(data=data[ax], label=current_label)) def addView(self, view): self.views.append(view) self.layout.addWidget(view) if hasattr(view, "signalAxisChanged"): view.signalAxisChanged.connect(self.on_signalAxisChanged) if hasattr(view, "signalSyncSlices"): view.signalSyncSlices.connect(self.on_signalSyncSlices) def removeView(self, view): if isinstance(view, int): self.layout.itemAt(view).widget().setParent(None) self.views[view].deleteLater() else: for i, oldView in enumerate(self.views): if view == oldView: self.removeView(i) break def setAxis(self, axis): for view in self.views: view.setAxis(axis) def setSlice(self, slice_): for view in self.views: view.setSlice(slice_) def __getitem__(self, idx): return self.views[idx] def on_signalAxisChanged(self, axis): self.setAxis(axis) self.signalAxisChanged.emit(axis) def on_signalSyncSlices(self, slice_): self.setSlice(slice_) self.signalSyncSlices.emit(slice_)
class QtLayersList(QScrollArea): def __init__(self, layers): super().__init__() self.layers = layers self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollWidget = QWidget() self.setWidget(scrollWidget) self.vbox_layout = QVBoxLayout(scrollWidget) self.vbox_layout.addWidget(QtDivider()) self.vbox_layout.addStretch(1) self.vbox_layout.setContentsMargins(0, 0, 0, 0) self.centers = [] self.setAcceptDrops(True) self.setToolTip('Layer list') self.layers.events.added.connect(self._add) self.layers.events.removed.connect(self._remove) self.layers.events.reordered.connect(self._reorder) def _add(self, event): """Inserts a layer widget at a specific index """ layer = event.item index = event.index total = len(self.layers) if layer._qt_properties is not None: self.vbox_layout.insertWidget(2 * (total - index) - 1, layer._qt_properties) self.vbox_layout.insertWidget(2 * (total - index), QtDivider()) def _remove(self, event): """Removes a layer widget """ layer = event.item if layer._qt_properties is not None: index = self.vbox_layout.indexOf(layer._qt_properties) divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(layer._qt_properties) layer._qt_properties.deleteLater() layer._qt_properties = None self.vbox_layout.removeWidget(divider) divider.deleteLater() divider = None def _reorder(self, event): """Reorders list of layer widgets by looping through all widgets in list sequentially removing them and inserting them into the correct place in final list. """ total = len(self.layers) for i in range(total): layer = self.layers[i] if layer._qt_properties is not None: index = self.vbox_layout.indexOf(layer._qt_properties) divider = self.vbox_layout.itemAt(index + 1).widget() self.vbox_layout.removeWidget(layer._qt_properties) self.vbox_layout.removeWidget(divider) self.vbox_layout.insertWidget(2 * (total - i) - 1, layer._qt_properties) self.vbox_layout.insertWidget(2 * (total - i), divider) def mouseReleaseEvent(self, event): """Unselects all layer widgets """ self.layers.unselect_all() def dragLeaveEvent(self, event): """Unselects layer dividers """ event.ignore() for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) def dragEnterEvent(self, event): event.accept() divs = [] for i in range(0, self.vbox_layout.count(), 2): widget = self.vbox_layout.itemAt(i).widget() divs.append(widget.y() + widget.frameGeometry().height() / 2) self.centers = [(divs[i + 1] + divs[i]) / 2 for i in range(len(divs) - 1)] def dragMoveEvent(self, event): cord = event.pos().y() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) layerWidget = event.source() total = self.vbox_layout.count() // 2 - 1 index = total - self.vbox_layout.indexOf(layerWidget) // 2 - 1 insert = total - divider_index if not (insert == index) and not (insert - 1 == index): state = True else: state = False for i in range(0, self.vbox_layout.count(), 2): if i == 2 * divider_index: self.vbox_layout.itemAt(i).widget().setSelected(state) else: self.vbox_layout.itemAt(i).widget().setSelected(False) def dropEvent(self, event): for i in range(0, self.vbox_layout.count(), 2): self.vbox_layout.itemAt(i).widget().setSelected(False) cord = event.pos().y() center_list = (i for i, x in enumerate(self.centers) if x > cord) divider_index = next(center_list, len(self.centers)) layerWidget = event.source() total = self.vbox_layout.count() // 2 - 1 index = total - self.vbox_layout.indexOf(layerWidget) // 2 - 1 insert = total - divider_index if index != insert and index + 1 != insert: if not self.layers[index].selected: self.layers.unselect_all() self.layers[index].selected = True self.layers._move_layers(index, insert) event.accept()