diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 2f7bab367..b9866942d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -291,7 +291,7 @@ A scene is a group of frames in one roll shot in the same light. It has its own ## 3. Analysis readout (always visible) -The readout sits above the Roll and Frame tabs and describes the current frame as you edit. Top to bottom: +The readout sits above the Roll and Frame tabs and describes the current frame as you edit. Each tab remembers whether the readout is collapsed. Top to bottom: #### Photometric curve diff --git a/negpy/desktop/view/sidebar/right_panel.py b/negpy/desktop/view/sidebar/right_panel.py index ce1bb4f16..5d8b53c86 100644 --- a/negpy/desktop/view/sidebar/right_panel.py +++ b/negpy/desktop/view/sidebar/right_panel.py @@ -176,17 +176,17 @@ def wrap_scroll(widget: QWidget) -> QScrollArea: repo = self.controller.session.repo saved_sizes = repo.get_global_setting("analysis_splitter_sizes") if isinstance(saved_sizes, list) and len(saved_sizes) == 2: - self.splitter.setSizes([int(s) for s in saved_sizes]) + sizes = [int(s) for s in saved_sizes] else: - self.splitter.setSizes(default_analysis_split(QApplication.primaryScreen())) - self.splitter.splitterMoved.connect(lambda *_: repo.save_global_setting("analysis_splitter_sizes", self.splitter.sizes())) + sizes = default_analysis_split(QApplication.primaryScreen()) + self.splitter.setSizes(sizes) + self.splitter.splitterMoved.connect(self._on_analysis_splitter_moved) # Collapsing the Analysis section hands its splitter space back to the tabs below and pins # the header at the top, instead of leaving a large empty pane. - self._analysis_expanded_size = self.splitter.sizes()[0] + self._analysis_expanded_size = sizes[0] + self._analysis_split_shown = False self.analysis_section.expanded_changed.connect(self._resize_splitter_for_analysis) - if not self.analysis_section.toggle_button.isChecked(): - self._resize_splitter_for_analysis(False) layout.addWidget(self.group_switcher) layout.addWidget(self.splitter, 1) @@ -267,9 +267,9 @@ def _build_frame_page(self, wrap_scroll) -> QWidget: analysis_layout.addWidget(self.stats_widget, 0) repo = self.controller.session.repo - self.analysis_section = make_section( - repo, "Analysis", "analysis", analysis_content, "fa5s.chart-bar", THEME.sidebar_expanded_defaults["analysis"] - ) + # Roll and Frame each keep their own expanded state; _switch_group restores it. + self.analysis_section = make_section(repo, "Analysis", "analysis", analysis_content, "fa5s.chart-bar", persist=False) + self.analysis_section.expanded_changed.connect(lambda checked: repo.save_global_setting(self._analysis_expanded_setting(), checked)) self.clipping_btn = self.analysis_section.add_header_toggle("fa5s.adjust", "Clipping Overlay") self.clipping_btn.setChecked(self.controller.state.clipping_overlay) self.clipping_btn.toggled.connect(self.controller.toggle_clipping_overlay) @@ -504,17 +504,32 @@ def show_analysis_help(self) -> None: SectionHelpDialog("analysis", "Analysis", self, repo=self.controller.session.repo).exec() + def _analysis_expanded_setting(self) -> str: + return "section_expanded_analysis_roll" if self._group_keys[self._active_group] == "roll" else "section_expanded_analysis" + + def showEvent(self, event) -> None: + super().showEvent(event) + # Before the first show the splitter has no real height, so a split set then is rescaled. + if not self._analysis_split_shown: + self._analysis_split_shown = True + QTimer.singleShot(0, lambda: self._resize_splitter_for_analysis(self.analysis_section.toggle_button.isChecked())) + + def _on_analysis_splitter_moved(self, *_) -> None: + if not self.analysis_section.toggle_button.isChecked(): + return + sizes = self.splitter.sizes() + self._analysis_expanded_size = sizes[0] + self.controller.session.repo.save_global_setting("analysis_splitter_sizes", sizes) + def _resize_splitter_for_analysis(self, expanded: bool) -> None: """Pin the collapsed Analysis header at the top: shrink pane 0 to the header and - give the rest to the tabs; restore the prior size when re-expanded.""" - sizes = self.splitter.sizes() - total = sum(sizes) - if total <= 0: + give the rest to the tabs; restore the dragged size when re-expanded.""" + total = sum(self.splitter.sizes()) + if total <= 0 or not self._analysis_split_shown: return if expanded: top = min(max(self._analysis_expanded_size, 120), max(120, total - 120)) else: - self._analysis_expanded_size = sizes[0] top = max(1, self.analysis_section.sizeHint().height()) self.splitter.setSizes([top, max(0, total - top)]) @@ -677,7 +692,11 @@ def _switch_group(self, index: int) -> None: self._active_group = index self.controller.session.repo.save_global_setting("right_panel_group", index) self.group_stack.setCurrentIndex(index) - self.analysis_section.setVisible(self._group_keys[index] in ("roll", "frame")) + analysis_shown = self._group_keys[index] in ("roll", "frame") + self.analysis_section.setVisible(analysis_shown) + if analysis_shown: + saved = self.controller.session.repo.get_global_setting(self._analysis_expanded_setting()) + self.analysis_section.set_expanded(THEME.sidebar_expanded_defaults["analysis"] if saved is None else bool(saved)) for i, btn in enumerate(self._group_buttons): btn.setChecked(i == index) self.group_switcher.set_pinned(index) diff --git a/negpy/desktop/view/widgets/collapsible.py b/negpy/desktop/view/widgets/collapsible.py index 7d91bbf90..0a776ce83 100644 --- a/negpy/desktop/view/widgets/collapsible.py +++ b/negpy/desktop/view/widgets/collapsible.py @@ -336,15 +336,17 @@ def make_section( default_expanded: bool = False, background_widget: Optional[QWidget] = None, collapsible: bool = True, + *, + persist: bool = True, ) -> CollapsibleSection: """The one way a sidebar builds a section: persisted under section_expanded_{key}, and the ⓘ guide present exactly when docs/USER_GUIDE.md carries a `panel:{key}` marker. The help dialog is parented to the section, so it centres on the window the section is in. collapsible=False always expands and never reads or writes the persisted setting; repo=None - keeps the section but not its expanded state.""" + or persist=False keeps the section but not its expanded state.""" from negpy.desktop.view.widgets.section_help_dialog import SectionHelpDialog, has_guide - persist = collapsible and repo is not None + persist = persist and collapsible and repo is not None if collapsible: setting = f"section_expanded_{key}" persisted = repo.get_global_setting(setting) if persist else None diff --git a/tests/test_right_panel_wiring.py b/tests/test_right_panel_wiring.py index 2b96a531c..85a6a2dd8 100644 --- a/tests/test_right_panel_wiring.py +++ b/tests/test_right_panel_wiring.py @@ -86,6 +86,49 @@ def test_analysis_shows_on_the_roll_and_frame_tabs_only(): panel.analysis_section.setVisible.assert_called_once_with(key in ("roll", "frame")) +def test_roll_and_frame_each_restore_their_own_analysis_expanded_state(): + panel = _group_panel_stub() + panel._analysis_expanded_setting = lambda: RightPanel._analysis_expanded_setting(panel) + saved = {"section_expanded_analysis_roll": False, "section_expanded_analysis": True} + panel.controller.session.repo.get_global_setting.side_effect = saved.get + + RightPanel._switch_group(panel, 0) + panel.analysis_section.set_expanded.assert_called_with(False) + RightPanel._switch_group(panel, 1) + panel.analysis_section.set_expanded.assert_called_with(True) + + panel.analysis_section.reset_mock() + RightPanel._switch_group(panel, 2) + panel.analysis_section.set_expanded.assert_not_called() + + +def test_only_an_expanded_analysis_drag_sets_the_saved_height(): + panel = MagicMock() + panel._analysis_expanded_size = 300 + panel.splitter.sizes.return_value = [40, 860] + panel.analysis_section.toggle_button.isChecked.return_value = False + + RightPanel._on_analysis_splitter_moved(panel) + assert panel._analysis_expanded_size == 300 + panel.controller.session.repo.save_global_setting.assert_not_called() + + panel.splitter.sizes.return_value = [500, 400] + panel.analysis_section.toggle_button.isChecked.return_value = True + RightPanel._on_analysis_splitter_moved(panel) + assert panel._analysis_expanded_size == 500 + panel.controller.session.repo.save_global_setting.assert_called_once_with("analysis_splitter_sizes", [500, 400]) + + +def test_analysis_split_waits_for_the_first_show(): + panel = MagicMock() + panel.splitter.sizes.return_value = [189, 287] + panel._analysis_split_shown = False + + RightPanel._resize_splitter_for_analysis(panel, True) + + panel.splitter.setSizes.assert_not_called() + + def test_switch_group_activates_the_chosen_scanner_only_on_the_scan_tab(): panel = _group_panel_stub(scan_index=5)