From 295f6bd619d3bb0047780cb32b36196c4cbf674f Mon Sep 17 00:00:00 2001 From: Markus Stabrin Date: Tue, 29 Oct 2024 18:11:05 +0100 Subject: [PATCH 1/3] Add selection toggle buttons --- .pre-commit-config.yaml | 14 ++++---- src/box_manager/_qt/SelectMetric.py | 50 +++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79f5047..a8198fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,34 +6,34 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.1 + rev: v2.7.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v3.19.0 hooks: - id: pyupgrade args: [--py38-plus, --keep-runtime-typing] - repo: https://github.com/myint/autoflake - rev: v1.4 + rev: v2.3.1 hooks: - id: autoflake args: ["--in-place", "--remove-all-unused-imports"] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.1.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports>=1.9.0] - repo: https://github.com/tlambert03/napari-plugin-checks - rev: v0.2.0 + rev: v0.3.0 hooks: - id: napari-plugin-checks # https://mypy.readthedocs.io/en/stable/introduction.html diff --git a/src/box_manager/_qt/SelectMetric.py b/src/box_manager/_qt/SelectMetric.py index a0c2e01..55c1914 100644 --- a/src/box_manager/_qt/SelectMetric.py +++ b/src/box_manager/_qt/SelectMetric.py @@ -33,6 +33,7 @@ QHeaderView, QLabel, QLineEdit, + QPushButton, QSlider, QStyle, QStyledItemDelegate, @@ -167,7 +168,7 @@ def update_model(self, layer_dict, value, col_idx): parent_item = self.item(parent_idx, 0) change_children = False - for row_idx in rows_idx: + for row_idx in set(rows_idx): child_item = parent_item.child(row_idx, col_idx) child_item.setText(str(value)) if change_children: @@ -427,9 +428,11 @@ def set_parent_only(self, value): def get_row_selection(self): prev_selection = { - self.model.get_value(-1, entry[1], "name") - if entry[0] == -1 - else self.model.get_value(-1, entry[0], "name") + ( + self.model.get_value(-1, entry[1], "name") + if entry[0] == -1 + else self.model.get_value(-1, entry[0], "name") + ) for entry in self.get_row_candidates(False) } return prev_selection @@ -542,9 +545,11 @@ def get_row_candidates(self, parent_only=None): parent_only = self.parent_only if parent_only: return { - (-1, entry.parent().row()) - if entry.parent().row() != -1 - else (entry.parent().row(), entry.row()) + ( + (-1, entry.parent().row()) + if entry.parent().row() != -1 + else (entry.parent().row(), entry.row()) + ) for entry in self.selectedIndexes() } else: @@ -642,6 +647,14 @@ def __init__(self, napari_viewer: "napari.Viewer"): ) self.metric_area = QVBoxLayout() + self.option_area2 = QHBoxLayout() + btn = QPushButton('"Write" all selected') + btn.clicked.connect(lambda: self._update_all_check_state(True)) + self.option_area2.addWidget(btn) + btn = QPushButton('Un-"Write" all selected') + btn.clicked.connect(lambda: self._update_all_check_state(False)) + self.option_area2.addWidget(btn) + self.option_area = QHBoxLayout() self.global_checkbox = QCheckBox( "Apply on layers, not on slices", self @@ -684,6 +697,7 @@ def __init__(self, napari_viewer: "napari.Viewer"): self.setLayout(QVBoxLayout()) self.layout().addLayout(self.settings_area, stretch=0) # type: ignore self.layout().addWidget(self.table_widget, stretch=1) + self.layout().addLayout(self.option_area2, stretch=0) # type: ignore self.layout().addLayout(self.option_area, stretch=0) # type: ignore self.layout().addLayout(self.metric_area, stretch=0) # type: ignore @@ -771,6 +785,24 @@ def _update_check_state(self, layer_name, slice_idx, attr_name, value): attr_name ] = value + @Slot(bool) + def _update_all_check_state(self, new_value): + cur_selection = self.table_widget.get_row_candidates(False) + layer_dict = {} + for parent_idx, row_idx in cur_selection: + if parent_idx == -1: + parent_item = self.table_widget.model.item(row_idx, 0) + rows = list(range(parent_item.rowCount())) + layer_dict.setdefault(row_idx, []).extend(rows) + else: + layer_dict.setdefault(parent_idx, []).append(row_idx) + + for parent_idx, rows in layer_dict.items(): + for row in set(rows): + self.table_widget.model.set_checkstate( + parent_idx, row, "write", new_value + ) + def _set_color(self): if self.napari_viewer.theme == "dark": icon = pathlib.Path(ICON_DIR, "checkmark_white.png") @@ -1065,9 +1097,9 @@ def _update_name(self, event): @Slot(object) def _update_on_data(self, event): - ''' + """ Is triggered when data is change in one layer. - ''' + """ if not self._plugin_view_update: layer = event.source try: From 7a607d78b515e4d745bb9ec6a010f7df0b00c7e3 Mon Sep 17 00:00:00 2001 From: Markus Stabrin Date: Wed, 30 Oct 2024 11:12:16 +0100 Subject: [PATCH 2/3] Use toggle button --- src/box_manager/_qt/SelectMetric.py | 35 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/box_manager/_qt/SelectMetric.py b/src/box_manager/_qt/SelectMetric.py index 55c1914..09b53d2 100644 --- a/src/box_manager/_qt/SelectMetric.py +++ b/src/box_manager/_qt/SelectMetric.py @@ -647,15 +647,9 @@ def __init__(self, napari_viewer: "napari.Viewer"): ) self.metric_area = QVBoxLayout() - self.option_area2 = QHBoxLayout() - btn = QPushButton('"Write" all selected') - btn.clicked.connect(lambda: self._update_all_check_state(True)) - self.option_area2.addWidget(btn) - btn = QPushButton('Un-"Write" all selected') - btn.clicked.connect(lambda: self._update_all_check_state(False)) - self.option_area2.addWidget(btn) - self.option_area = QHBoxLayout() + ### + ### self.global_checkbox = QCheckBox( "Apply on layers, not on slices", self ) @@ -693,11 +687,13 @@ def __init__(self, napari_viewer: "napari.Viewer"): self.settings_area.addWidget(self.hide_dim, stretch=1) self.settings_area.addWidget(QLabel("Slices:", self)) self.settings_area.addWidget(self.show_mode, stretch=1) + btn = QPushButton('Toggle "Write"') + btn.clicked.connect(self._update_all_check_state) + self.settings_area.addWidget(btn) self.setLayout(QVBoxLayout()) self.layout().addLayout(self.settings_area, stretch=0) # type: ignore self.layout().addWidget(self.table_widget, stretch=1) - self.layout().addLayout(self.option_area2, stretch=0) # type: ignore self.layout().addLayout(self.option_area, stretch=0) # type: ignore self.layout().addLayout(self.metric_area, stretch=0) # type: ignore @@ -786,21 +782,30 @@ def _update_check_state(self, layer_name, slice_idx, attr_name, value): ] = value @Slot(bool) - def _update_all_check_state(self, new_value): + def _update_all_check_state(self): cur_selection = self.table_widget.get_row_candidates(False) layer_dict = {} for parent_idx, row_idx in cur_selection: if parent_idx == -1: parent_item = self.table_widget.model.item(row_idx, 0) - rows = list(range(parent_item.rowCount())) - layer_dict.setdefault(row_idx, []).extend(rows) + parent_idx = row_idx + rows = set(range(parent_item.rowCount())) else: - layer_dict.setdefault(parent_idx, []).append(row_idx) + rows = {row_idx} + layer_dict.setdefault(parent_idx, set()).update(rows) + + item_values = [] + for parent_idx, rows in layer_dict.items(): + item_values.extend( + self.table_widget.model.get_checkstates( + parent_idx, rows, "write" + ) + ) for parent_idx, rows in layer_dict.items(): - for row in set(rows): + for row in rows: self.table_widget.model.set_checkstate( - parent_idx, row, "write", new_value + parent_idx, row, "write", not all(item_values) ) def _set_color(self): From 62f6944037e0fb32633fe64126cab9fc93503e4e Mon Sep 17 00:00:00 2001 From: Markus Stabrin Date: Wed, 30 Oct 2024 13:27:43 +0100 Subject: [PATCH 3/3] Fix np.bool_ type check --- src/box_manager/_qt/SelectMetric.py | 62 +++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/src/box_manager/_qt/SelectMetric.py b/src/box_manager/_qt/SelectMetric.py index 09b53d2..876f53b 100644 --- a/src/box_manager/_qt/SelectMetric.py +++ b/src/box_manager/_qt/SelectMetric.py @@ -59,6 +59,9 @@ # return inner +EMPTY_NAME = "-" + + def check_equal(layer, compare_data): if isinstance(layer, napari.layers.Points): return np.array_equal(layer.data, compare_data) @@ -357,7 +360,7 @@ def append_to_row(self, root_element, columns, row_idx, first_item=None): else: text = columns[cur_label] if cur_label in columns else "-" col_item = QStandardItem(text) - if isinstance(text, bool): + if isinstance(text, (bool, np.bool_)): combo_items.append((col_item, cur_label)) col_item.setEditable(True) col_item.setCheckable(True) @@ -783,6 +786,7 @@ def _update_check_state(self, layer_name, slice_idx, attr_name, value): @Slot(bool) def _update_all_check_state(self): + prev_status = self.table_widget.blockSignals(True) cur_selection = self.table_widget.get_row_candidates(False) layer_dict = {} for parent_idx, row_idx in cur_selection: @@ -802,11 +806,19 @@ def _update_all_check_state(self): ) ) + check_state = not all(item_values) for parent_idx, rows in layer_dict.items(): + layer_name = self.table_widget.model.get_value( + -1, parent_idx, "name" + ) for row in rows: self.table_widget.model.set_checkstate( - parent_idx, row, "write", not all(item_values) + parent_idx, row, "write", check_state ) + self.napari_viewer.layers[layer_name].metadata[row][ + "write" + ] = check_state + self.table_widget.blockSignals(prev_status) def _set_color(self): if self.napari_viewer.theme == "dark": @@ -1237,6 +1249,15 @@ def _prepare_entries(self, layer, *, name=None) -> list: for e1, e2 in features_copy.groupby("identifier", sort=False) } + try: + is_3d = layer.metadata["is_3d"] + except KeyError: + is_3d = False + + empty_name = None + if name is None and is_3d: + empty_name = EMPTY_NAME + try: max_slice = max(loop_var) except ValueError: @@ -1246,10 +1267,13 @@ def _prepare_entries(self, layer, *, name=None) -> list: ident_df = slice_dict[identifier] except KeyError: ident_df = pd.DataFrame(columns=features_copy.columns) + try: - cur_name = name or layer.metadata[identifier]["name"] + cur_name = name or ( + empty_name or layer.metadata[identifier]["name"] + ) except KeyError: - cur_name = "Manual" + cur_name = EMPTY_NAME output_list.append( self._prepare_columns( pd.DataFrame(get_size(layer), dtype=float), @@ -1266,9 +1290,11 @@ def _prepare_entries(self, layer, *, name=None) -> list: if not output_list and name is not None: identifier = "" if name is not None else 0 try: - cur_name = name or layer.metadata[identifier]["name"] + cur_name = name or ( + empty_name or layer.metadata[identifier]["name"] + ) except KeyError: - cur_name = "Manual" + cur_name = EMPTY_NAME features = pd.DataFrame(columns=["shown"]) output_list.append( self._prepare_columns( @@ -1314,14 +1340,15 @@ def _prepare_columns( if is_main_group: output_dict["write"] = "-" else: + # Cast explicitely as bool here, because it can be np.bool_ try: write_val = label_data.loc[slice_idx, "write"] if write_val is not None and not np.isnan(write_val): - output_dict["write"] = write_val + output_dict["write"] = bool(write_val) else: - output_dict["write"] = not features.empty + output_dict["write"] = bool(not features.empty) except KeyError: - output_dict["write"] = not features.empty + output_dict["write"] = bool(not features.empty) for col_name in features.columns: if col_name in self.ignore_idx: @@ -1447,10 +1474,19 @@ def _update_table(self, layer: napari.layers.Layer, current_slice): else: label_data = pd.DataFrame() range_list.extend(full_range) + try: - name = layer.metadata[current_slice]["name"] + is_3d = layer.metadata["is_3d"] except KeyError: - name = "Manual" + is_3d = False + + empty_name = None + if is_3d: + empty_name = EMPTY_NAME + try: + name = empty_name or layer.metadata[current_slice]["name"] + except KeyError: + name = EMPTY_NAME new_col_entry = self._prepare_columns( pd.DataFrame(get_size(layer), dtype=float), pd.DataFrame(columns=list(layer.features.columns) + ["shown"]), @@ -1470,7 +1506,9 @@ def _update_table(self, layer: napari.layers.Layer, current_slice): write_val = layer.metadata.setdefault(current_slice, {}).setdefault( "write", None ) - check_value = write_val if write_val is not None else bool(selected) + check_value = ( + bool(write_val) if write_val is not None else bool(selected) + ) if check_value != self.table_model.get_checkstate( parent_idx, child_idx, "write" ):