Skip to content

feat: Use QScrollArea for components selection#1328

Open
Czaki wants to merge 13 commits intodevelopfrom
scroll_area_select_components
Open

feat: Use QScrollArea for components selection#1328
Czaki wants to merge 13 commits intodevelopfrom
scroll_area_select_components

Conversation

@Czaki
Copy link
Collaborator

@Czaki Czaki commented Oct 22, 2025

Summary by Sourcery

Make component selection scrollable and update ROI handling hooks to keep chosen components in sync with ROI changes.

New Features:

  • Allow scrolling through the list of selectable components using a scrollable container.

Enhancements:

  • Ensure selected components are automatically brought into view when toggled or changed.
  • Separate initialization of component checkboxes from updating their checked state via a dedicated setter.
  • Adjust the main ROI mask layout to use a splitter between algorithm selection and component selection panels.
  • Introduce a post-ROI-set hook for subclasses to perform additional updates after ROI changes.
  • Tighten type annotations for napari layer addition utility.

Summary by CodeRabbit

  • New Features

    • Component list now supports vertical scrolling and auto-scrolls a toggled item into view
    • Component and algorithm selectors placed in a resizable split layout for better spacing
    • Better synchronization of component choices after ROI or image updates
  • Refactor

    • Component-selection APIs reshaped for clearer, more efficient updates and partial state changes

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 22, 2025

Reviewer's Guide

Wrap the components selection widget in a vertically scrollable QScrollArea, adjust its API to separate component list initialization from selection updates, and integrate it into the ROI/stack settings lifecycle while improving layout behavior and type hints.

Sequence diagram for ROI update and components list initialization

sequenceDiagram
    participant Client
    participant StackSettings
    participant BaseSettings
    participant ChosenComponents

    Client->>StackSettings: set_project_info(data)
    activate StackSettings
    StackSettings->>StackSettings: compute new_roi_info and selected_components
    StackSettings->>BaseSettings: roi = new_roi_info
    activate BaseSettings
    BaseSettings->>BaseSettings: set _roi_info
    BaseSettings->>BaseSettings: clear _additional_layers
    BaseSettings->>StackSettings: post_roi_set()
    deactivate BaseSettings
    activate StackSettings
    StackSettings->>ChosenComponents: set_chose(roi_info.bound_info.keys(), [])
    deactivate StackSettings
    Note over StackSettings,ChosenComponents: Later, when restoring selection
    StackSettings->>ChosenComponents: set_chosen(selected_components)
    deactivate StackSettings
Loading

Class diagram for updated ChosenComponents and settings integration

classDiagram
    class QScrollArea

    class ChosenComponents {
        +dict~int,ComponentCheckBox~ check_box
        +QPushButton check_all_btn
        +QPushButton uncheck_all_btn
        +FlowLayout check_layout
        +check_change_signal
        +mouse_enter
        +mouse_leave
        +ChosenComponents()
        +other_component_choose(num int)
        +check_all()
        +uncheck_all()
        +remove_components()
        +new_choose(num int, chosen_components Sequence_int_)
        +set_chose(components_index Sequence_int_, chosen_components Sequence_int_)
        +set_chosen(chosen_components Sequence_int_)
        +check_change()
        +change_state(num int, val bool)
        +get_state(num int) bool
        +get_chosen() list_int_
    }

    QScrollArea <|-- ChosenComponents

    class BaseSettings {
        -ROIInfo _roi_info
        -dict _additional_layers
        +roi ROIInfo
        +roi_changed
        +set_roi(val Union_np_ndarray_ROIInfo_)
        +post_roi_set()
    }

    class StackSettings {
        +ChosenComponents chosen_components_widget
        +set_project_info(data MaskProjectTuple_or_PointsInfo)
        +_set_roi_info(state, new_roi_info ROIInfo, segmentation_parameters dict, list_of_components list_int_, save_chosen bool)
        +post_roi_set()
    }

    BaseSettings <|-- StackSettings
    StackSettings --> ChosenComponents : manages_selection
Loading

File-Level Changes

Change Details Files
Make the components chooser a scrollable QScrollArea-based widget and adjust its internal layout/behavior.
  • Change ChosenComponents to inherit from QScrollArea instead of QWidget and embed an inner QWidget as its scrollable content.
  • Move the existing button and FlowLayout setup onto the inner widget’s layout and enable vertical-only scrolling with ScrollBarAsNeeded.
  • Automatically scroll to make a component’s checkbox visible when it is toggled via other_component_choose or change_state.
package/PartSeg/_roi_mask/main_window.py
Refine the ChosenComponents API to separate component list creation from selection updates and handle optional inputs safely.
  • Update set_chose to accept an optional chosen_components sequence, defaulting to an empty list when None is provided.
  • Add a new set_chosen method that only updates which components are checked without recreating the component list.
  • Use set_chosen wherever only selection needs updating, avoiding unnecessary recreation of checkboxes.
package/PartSeg/_roi_mask/main_window.py
package/PartSeg/_roi_mask/stack_settings.py
Integrate the scrollable chooser into the main UI layout using a splitter and hook chooser updates into the ROI lifecycle.
  • Replace direct stacking of algorithm_choose_widget and choose_components with a vertical QSplitter so both can share space and resize together.
  • Introduce post_roi_set in the settings base class and override it to repopulate the components chooser after ROI changes using set_chose.
  • Ensure ROI assignment calls post_roi_set after updating _roi_info so UI stays in sync.
package/PartSeg/_roi_mask/main_window.py
package/PartSeg/_roi_mask/stack_settings.py
package/PartSeg/common_backend/base_settings.py
Tighten typing for napari layer utility function.
  • Add explicit type annotations to _add_layer_util arguments and return type for better clarity and static checking.
package/PartSeg/common_gui/napari_image_view.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c237d4f-a4d9-42d1-9fe9-ecf983aa53e1

📥 Commits

Reviewing files that changed from the base of the PR and between 6b3bc30 and 590b71e.

📒 Files selected for processing (1)
  • package/PartSeg/_roi_mask/stack_settings.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • package/PartSeg/_roi_mask/stack_settings.py

📝 Walkthrough

Walkthrough

Converted the component-selection widget to a scrollable area, reshaped its selection API (renamed/added methods), added an ImageSettings.post_roi_set() hook invoked on ROI updates, and tightened a napari image view type signature. AlgorithmOptions layout now nests widgets in a vertical splitter.

Changes

Cohort / File(s) Summary
ChosenComponents UI & API
package/PartSeg/_roi_mask/main_window.py
ChosenComponents now inherits QScrollArea (was QWidget); vertical scrolling enabled, horizontal disabled, previous content hosted via self.widget(). Handlers call ensureWidgetVisible(...). API changes: set_chose(...)set_components(self, components_index, chosen_components: Union[Sequence[int], None] = None) and new set_chosen(self, chosen_components: Sequence[int]). new_choose(...) now uses set_components(...). AlgorithmOptions places widgets inside a vertical QSplitter.
StackSettings integration
package/PartSeg/_roi_mask/stack_settings.py
Replaced reconstruction-based set_chose(...) calls with set_chosen(...) using saved selected_components. set_project_info() and _set_roi_info() pass existing selected lists; removed key-derivation steps. Added post_roi_set(self) to synchronize component parameters via set_components(...) with signals blocked.
Settings base hook
package/PartSeg/common_backend/base_settings.py
Added ImageSettings.post_roi_set(self) -> None and invoke it whenever ROI is set/cleared (called before emitting roi_changed / roi_clean) so subclasses can synchronize state after ROI updates.
Type annotation refinements
package/PartSeg/common_gui/napari_image_view.py
Updated ImageView._add_layer_util signature to index: int, layer: _NapariImage, filters: list[tuple[NoiseFilterType, float]] -> None. No behavior changes.

Sequence Diagram(s)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nudged the scroll, the checkboxes glide,

selections settle where they hide,
a tiny hook wakes when ROIs arrive,
types speak clearly, layouts dive,
I hop — the interface feels spry.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: wrapping the components selection widget in a QScrollArea to enable scrolling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scroll_area_select_components
📝 Coding Plan
  • Generate coding plan for human review comments

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.16.4)
package/PartSeg/_roi_mask/stack_settings.py

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

@codecov
Copy link

codecov bot commented Oct 22, 2025

Codecov Report

❌ Patch coverage is 86.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.13%. Comparing base (979f440) to head (6278963).

Files with missing lines Patch % Lines
package/PartSeg/_roi_mask/main_window.py 85.71% 3 Missing ⚠️
package/PartSeg/_roi_mask/stack_settings.py 83.33% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1328   +/-   ##
========================================
  Coverage    93.12%   93.13%           
========================================
  Files          211      211           
  Lines        33272    33291   +19     
========================================
+ Hits         30983    31004   +21     
+ Misses        2289     2287    -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Czaki Czaki added the skip check PR title skip spellcheking PR title label Dec 7, 2025
@Czaki Czaki added this to the 0.16.5 milestone Dec 7, 2025
@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 7, 2025

@Czaki Czaki marked this pull request as ready for review March 20, 2026 11:08
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="package/PartSeg/_roi_mask/stack_settings.py" line_range="303-311" />
<code_context>
         if save_chosen:
</code_context>
<issue_to_address>
**question (bug_risk):** set_chosen no longer constrains components to the available ROI parameters list.

Previously, the `save_chosen` path constrained components via `set_chosen(sorted(state2.roi_extraction_parameters.keys()))`. Now it uses only `state2.selected_components` and relies on `post_roi_set` / `bound_info.keys()`, so the component list is no longer tied to `roi_extraction_parameters`. If `roi_extraction_parameters` and `bound_info` diverge (e.g. params exist for components no longer in the ROI), some components with saved parameters may become unselectable. If that’s unintended, consider deriving the component list from the parameter keys, or intersecting `roi_extraction_parameters` and `bound_info` explicitly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package/PartSeg/_roi_mask/main_window.py`:
- Around line 473-476: The loop in set_chosen causes many stateChanged signals
and repeated image refreshes; wrap per-checkbox updates with signal blocking and
then trigger a single update at the end: in set_chosen (method name) call
check.blockSignals(True) before check.setChecked(...) and
check.blockSignals(False) after the loop (or for each check), then explicitly
emit the existing check_change_signal or call image_view.refresh_selected() once
after the loop to perform a single refresh; this prevents per-checkbox
stateChanged handlers from firing during the batch update invoked by
StackSettings.

In `@package/PartSeg/_roi_mask/stack_settings.py`:
- Around line 313-315: post_roi_set may be called before the UI widget exists
(chosen_components_widget is initialized to None), causing an AttributeError;
update post_roi_set to guard the call by checking that
self.chosen_components_widget is truthy (or hasattr/set to non-None) before
calling set_chose(self.roi_info.bound_info.keys(), []), so headless or pre-UI
usage of post_roi_set/setting self.roi doesn't raise.

In `@package/PartSeg/common_backend/base_settings.py`:
- Around line 158-163: The post_roi_set() hook is only called in one branch, so
ensure it runs on every ROI update path: call self.post_roi_set() before any
early returns in the ROI setter (so it executes even when ROI is set to None)
and add a call to self.post_roi_set() inside
BaseSettings.set_segmentation_result (the method referenced as
set_segmentation_result) after the internal ROI/state is updated; keep the
existing self.roi_changed.emit(self._roi_info) behavior but ensure
post_roi_set() is invoked consistently before or right after emitting so
subclasses are always synchronized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 767af4e3-be73-433d-b152-e3afa0efc1f2

📥 Commits

Reviewing files that changed from the base of the PR and between 979f440 and 6278963.

📒 Files selected for processing (4)
  • package/PartSeg/_roi_mask/main_window.py
  • package/PartSeg/_roi_mask/stack_settings.py
  • package/PartSeg/common_backend/base_settings.py
  • package/PartSeg/common_gui/napari_image_view.py

Comment on lines +473 to +476
def set_chosen(self, chosen_components: Sequence[int]):
chosen_components = set(chosen_components)
for num, check in self.check_box.items():
check.setChecked(num in chosen_components)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Batch set_chosen() updates into a single refresh.

Each setChecked() here can emit stateChanged, which reaches check_change_signal and image_view.refresh_selected() via Line 544. Since StackSettings now calls set_chosen() after ROI loads at Line 174 and Line 306 in package/PartSeg/_roi_mask/stack_settings.py, loading a segmentation with many selected components will refresh the view once per component.

Proposed fix
     def set_chosen(self, chosen_components: Sequence[int]):
         chosen_components = set(chosen_components)
-        for num, check in self.check_box.items():
-            check.setChecked(num in chosen_components)
+        self.blockSignals(True)
+        try:
+            for num, check in self.check_box.items():
+                check.setChecked(num in chosen_components)
+        finally:
+            self.blockSignals(False)
+        self.check_change_signal.emit()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def set_chosen(self, chosen_components: Sequence[int]):
chosen_components = set(chosen_components)
for num, check in self.check_box.items():
check.setChecked(num in chosen_components)
def set_chosen(self, chosen_components: Sequence[int]):
chosen_components = set(chosen_components)
self.blockSignals(True)
try:
for num, check in self.check_box.items():
check.setChecked(num in chosen_components)
finally:
self.blockSignals(False)
self.check_change_signal.emit()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/PartSeg/_roi_mask/main_window.py` around lines 473 - 476, The loop in
set_chosen causes many stateChanged signals and repeated image refreshes; wrap
per-checkbox updates with signal blocking and then trigger a single update at
the end: in set_chosen (method name) call check.blockSignals(True) before
check.setChecked(...) and check.blockSignals(False) after the loop (or for each
check), then explicitly emit the existing check_change_signal or call
image_view.refresh_selected() once after the loop to perform a single refresh;
this prevents per-checkbox stateChanged handlers from firing during the batch
update invoked by StackSettings.

Comment on lines +158 to +163
self.post_roi_set()
self.roi_changed.emit(self._roi_info)

def post_roi_set(self) -> None:
"""called after roi is set, for subclasses to override"""

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make post_roi_set() run on every ROI update path.

Right now this hook only fires in the non-None branch here. The early return above and BaseSettings.set_segmentation_result() at Line 537 still bypass it, so subclasses overriding post_roi_set() will stay synchronized for some ROI updates but not others.

Proposed fix
     def roi(self, val: Union[np.ndarray, ROIInfo]):
         if val is None:
             self._roi_info = ROIInfo(val)
             self._additional_layers = {}
+            self.post_roi_set()
             self.roi_clean.emit()
             return
         try:
             if isinstance(val, np.ndarray):
                 self._roi_info = ROIInfo(self.image.fit_array_to_image(val))
@@
         if result.points is not None:
             self.points = result.points
         self._roi_info = roi_info
+        self.post_roi_set()
         self.roi_changed.emit(self._roi_info)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/PartSeg/common_backend/base_settings.py` around lines 158 - 163, The
post_roi_set() hook is only called in one branch, so ensure it runs on every ROI
update path: call self.post_roi_set() before any early returns in the ROI setter
(so it executes even when ROI is set to None) and add a call to
self.post_roi_set() inside BaseSettings.set_segmentation_result (the method
referenced as set_segmentation_result) after the internal ROI/state is updated;
keep the existing self.roi_changed.emit(self._roi_info) behavior but ensure
post_roi_set() is invoked consistently before or right after emitting so
subclasses are always synchronized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
package/PartSeg/_roi_mask/main_window.py (1)

473-477: ⚠️ Potential issue | 🟠 Major

Batch set_chosen() into one signal emission.

Because Line 545 wires check_change_signal to image_view.refresh_selected, each changed setChecked() here can still trigger a full refresh before the final explicit emit. On ROI loads with many selected components, that turns one sync into N+1 view refreshes.

💡 Proposed fix
     def set_chosen(self, chosen_components: Sequence[int]):
         chosen_components = set(chosen_components)
         for num, check in self.check_box.items():
-            check.setChecked(num in chosen_components)
+            check.blockSignals(True)
+            try:
+                check.setChecked(num in chosen_components)
+            finally:
+                check.blockSignals(False)
         self.check_change_signal.emit()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/PartSeg/_roi_mask/main_window.py` around lines 473 - 477, Batch the
per-checkbox updates in set_chosen by temporarily blocking signals on each
QCheckBox while calling setChecked so intermediate stateChanged signals don't
trigger image_view.refresh_selected; for example, in set_chosen iterate
self.check_box.items(), call check.blockSignals(True) (or use
QSignalBlocker(check)), call check.setChecked(...), then unblock
(blockSignals(False)) and after the loop emit self.check_change_signal.emit()
once. Ensure you reference the set_chosen method and self.check_box and leave
the final explicit emit as the single notification.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@package/PartSeg/_roi_mask/main_window.py`:
- Around line 473-477: Batch the per-checkbox updates in set_chosen by
temporarily blocking signals on each QCheckBox while calling setChecked so
intermediate stateChanged signals don't trigger image_view.refresh_selected; for
example, in set_chosen iterate self.check_box.items(), call
check.blockSignals(True) (or use QSignalBlocker(check)), call
check.setChecked(...), then unblock (blockSignals(False)) and after the loop
emit self.check_change_signal.emit() once. Ensure you reference the set_chosen
method and self.check_box and leave the final explicit emit as the single
notification.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b9be0c1-0617-4b8b-a660-58ac2fce6f04

📥 Commits

Reviewing files that changed from the base of the PR and between 6278963 and 6b3bc30.

📒 Files selected for processing (3)
  • package/PartSeg/_roi_mask/main_window.py
  • package/PartSeg/_roi_mask/stack_settings.py
  • package/PartSeg/common_backend/base_settings.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • package/PartSeg/common_backend/base_settings.py
  • package/PartSeg/_roi_mask/stack_settings.py

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip check PR title skip spellcheking PR title

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant