Skip to content

Fix TabView collapsing to zero height after model reset - #6295

Open
juliuspfadt wants to merge 1 commit into
jasp-stats:developmentfrom
juliuspfadt:fix/tabview-height-collapse
Open

Fix TabView collapsing to zero height after model reset#6295
juliuspfadt wants to merge 1 commit into
jasp-stats:developmentfrom
juliuspfadt:fix/tabview-height-collapse

Conversation

@juliuspfadt

Copy link
Copy Markdown
Contributor

Problem

TabViews with dynamic values/source intermittently collapse to zero height (tab bar visible, content folded to nothing) and stay collapsed. Most visible in jaspSem's Moderated Nonlinear Factor Analysis, which nests TabViews 3 levels deep with values: recomputed from checkbox states and factor lists.

Root cause

itemStack (the StackLayout holding tab content) got its height via:

onCurrentIndexChanged: height = Qt.binding( function() { return currentIndex >= 0 ? rep.itemAt(currentIndex).height : 0; });

Two defects:

  1. The binding is only installed once currentIndex changes. If it never does (single tab, or index staying at 0), no height binding exists at all.
  2. Repeater.itemAt() is a plain function — not notifiable. Any values/source change triggers a full model reset (ListModel::_initTermsbeginResetModel()), destroying and recreating all delegates. If currentIndex stays the same, the binding's only tracked dependencies are currentIndex and the destroyed wrapper's height — it never re-evaluates, keeps referencing the dead item, and the TabView collapses to 0. The collapse latches: StackLayout writes height 0 onto the current item, and since row controls are cached C++-side, nothing re-triggers the binding.

With nested TabViews, a collapse at any level zeroes the level above → the whole stack folds.

Fix

Push-based height tracking: each delegate pushes its height into itemStack.currentTabHeight whenever it is the current item (StackLayout.isCurrentItem attached property, plus Component.onCompleted for creation ordering). Freshly created delegates after a model reset always announce their height, so no stale references are possible. height on itemStack is now a plain declarative binding with guards for empty model / index −1.

No public API change (content, currentIndex alias, add/remove tabs untouched). SEM/PLSSEM content:-style usage unaffected — their initial-height case (defect 1) is fixed too.

Test checklist (jaspSem MNLFA, dataset with ≥6 scale variables)

  1. Moderation Options: check/uncheck the invariance checkboxes one by one, incl. unchecking a tab other than the selected one → no fold-up.
  2. Same-count resets: swap which checkboxes are checked → inner tabs never fold.
  3. Factors: add a 3rd factor while the "Factors" tab is selected (adds "Covariances" entry), remove back to 1, rename factors, add/remove indicators → outer TabViews keep height.
  4. Switch tabs at all 3 levels, incl. to a tab whose content grew while hidden.
  5. Collapse/re-expand both Sections; also trigger a reset while collapsed, then expand.
  6. Plots section: repeat 1–4; toggle interaction terms.
  7. Resize window, change interface scale → no fold-ups.
  8. Console: no TypeError: Cannot read property 'height' of null, no binding-loop warnings.
  9. Regression SEM/PLS-SEM: model TextArea tab has correct height at first open; add/remove/rename tabs; multi-line lavaan syntax grows the TabView.
  10. Save/reload a .jasp file with populated nested tabs → all levels render at full height.

🤖 Generated with Claude Code

itemStack height was bound via rep.itemAt(currentIndex).height, installed
only in onCurrentIndexChanged. itemAt() is not notifiable: after a model
reset (values/source change -> beginResetModel) delegates are destroyed and
recreated, but if currentIndex stays the same the binding keeps referencing
the destroyed wrapper -> height 0 -> TabView folds up and stays folded.
Frequent with nested TabViews driven by dynamic values (e.g. MNLFA in
jaspSem).

Replace with push-based tracking: each wrapper pushes its height when it is
the current StackLayout item (isCurrentItem attached prop), so freshly
created delegates always announce their height. Also fixes initial height
when currentIndex never changes from 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an intermittent UI layout failure where TabView content could collapse to zero height after the underlying tab model resets (common with dynamic/nested TabViews whose model is recomputed). The solution replaces a fragile pull-based height binding (dependent on Repeater.itemAt()) with push-based height tracking from the currently active delegate.

Changes:

  • Replace onCurrentIndexChanged-installed height binding with a persistent declarative height binding on the StackLayout.
  • Introduce currentTabHeight on the StackLayout, updated by delegates when they become current and when their height changes.
  • Add inline documentation explaining why Repeater.itemAt() cannot be safely used as a binding dependency across model resets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants