Skip to content

[NRT-790] Fix clipped last line of long tag checkboxes in suggestion dialog#1309

Open
RisingOrange wants to merge 6 commits into
mainfrom
NRT-790-fix-suggestion-dialog-tag-clipping
Open

[NRT-790] Fix clipped last line of long tag checkboxes in suggestion dialog#1309
RisingOrange wants to merge 6 commits into
mainfrom
NRT-790-fix-suggestion-dialog-tag-clipping

Conversation

@RisingOrange

@RisingOrange RisingOrange commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Related issues

Proposed changes

1. Fix clipped last line of long tag checkboxes

In the note suggestion dialog ("Include in suggestion" panel, behind auto_protect_fields_when_edited), long tag checkboxes had their last line clipped:

  • A deep hierarchical tag like #AK_MCAT_v2::MileDown::Behavioral::Cognition lost its bottom segment ("Cognition") — the row wrapped to one more line than its reserved height. The clip appeared/disappeared at boundary widths when resizing the dialog.
  • An elided tag whose leaf has no break points overflowed horizontally off the right edge.

More noticeable with larger UI fonts.

Root cause: _WrappingCheckBox.heightForWidth measured text wrapping against a wider width than paintEvent actually drew into. The style content-rect metrics were cached in __init__, before the widget was polished — and the dialog's stylesheet activates QStyleSheetStyle, which pads the checkbox content rect once polished, so the cached width was ~6px too wide and heightForWidth reserved one line too few. Wrapping was also measured with QFontMetrics.boundingRect, a different code path than the painter, which disagrees at wrap boundaries. This is why it only reproduced in the real dialog, not in an isolated widget.

Fix:

  • Compute the label content width fresh per call (the same SE_CheckBoxContents the painter uses) instead of caching a pre-polish value.
  • Share one QTextLayout between heightForWidth and paintEvent so the reserved height matches the painted line count exactly.
  • Wrap at word boundary or anywhere so an over-long unbreakable leaf wraps instead of overflowing horizontally.

2. Scale both columns proportionally when resizing (review feedback)

Previously, enlarging the dialog gave almost all extra width to the right (Change Type) column: the "Include in suggestion" panel was capped at 360px, even though it's the column that benefits most from extra room (long tags). The columns already had 2:3 stretch factors — the cap just kept them from applying. Dropping the cap lets both columns grow proportionally (~40/60). Long content still can't push the dialog wide: section titles and tag rows elide (Ignored size policy) and the rest sits inside a scroll area.

How to reproduce

  1. Enable the auto_protect_fields_when_edited feature flag.
  2. On a note in an AnkiHub deck, remove a deep tag such as #AK_MCAT_v2::MileDown::Behavioral::Cognition (and/or add a tag with a very long final segment).
  3. Open the suggestion dialog and look at the "Removed Tags" / "Added Tags" checkboxes; resize the dialog.
  4. Before this fix, the last tag segment is cut off (and reappears only at certain widths). After, every wrapped line stays fully visible.
  5. Enlarge the dialog window: before, the left panel stopped growing at 360px; after, it keeps ~40% of the width and long tags become fully readable.

Screenshots and videos

Suggestion dialog with a deep tag and a long-leaf tag, before and after the clipping fix:

Before
Before: last tag segment clipped
After
After: tags wrap fully

Proportional column scaling at a wide window size (1500px):

Before: left panel capped at 360px
Before: left panel capped at 360px at wide window sizes
After: columns scale proportionally
After: both columns scale proportionally

Further comments

  • Tests: added TestFieldsToSuggestFilters::test_long_tag_checkbox_is_not_clipped, a dialog-level regression test that reproduces both the vertical (deep tag) and horizontal (long leaf) clipping in the real dialog and asserts no clipping across a range of widths. Verified it fails on the buggy version and passes on the fix. mypy clean.
  • The per-call style query + QTextLayout build replace a cache that was the source of the bug; reviewers (Codex + cleanup pass) confirmed the cost is negligible at realistic tag counts, so no caching was reintroduced.
  • Verified on both Linux and macOS (native style differs there, and the fix depends on the native checkbox content-rect metrics) — tag checkboxes render without clipping across dialog widths.
  • The proportional-scaling change came from review feedback on this PR; the suggestion/dialog integration tests (68) and the wrapping/clipping unit tests (39) pass with it.

…dialog

_WrappingCheckBox.heightForWidth measured wrapping against a wider width
than paintEvent drew into: the style content-rect metrics were cached in
__init__ before the widget was polished, and the dialog's stylesheet pads
the checkbox content rect once QStyleSheetStyle is active. The cached value
was too wide, so heightForWidth under-counted wrapped lines and reserved one
line too few, clipping the last segment of deep tags. boundingRect was also a
different wrapping engine than the painter, disagreeing at wrap boundaries.

- Compute the label content width fresh per call (same SE_CheckBoxContents
  the painter uses) instead of caching a pre-polish value.
- Share one QTextLayout between heightForWidth and paintEvent so reserved
  height matches the painted line count exactly.
- Wrap at word boundary or anywhere so an over-long unbreakable leaf wraps
  instead of overflowing horizontally.

Adds a dialog-level regression test covering both vertical (deep tag) and
horizontal (long leaf) clipping in the real dialog.
- Measure painted extents through production _lay_out/_content_width instead of
  re-implementing the QTextLayout loop, so the test asserts the real paint output
  fits rather than a copy of it.
- Restore the app font in a finally block so the larger-font setup doesn't leak
  into other tests in the process.
- Wait for layout to settle via qtbot.waitUntil instead of a fixed 10ms timer.
- Account for the paint top offset in the vertical-clip assertion.
- Drop now-redundant inline imports (SuggestionDialog, aqt.qt).
…ped helper

The tag-box dicts are typed Dict[str, QCheckBox], so calling _vertical_padding()
on a loop var tripped mypy. Compute the text top offset inside painted_extents
(where cb is untyped) and return it.
The bug is a pre-polish vs post-polish discrepancy in the checkbox's content-rect
metrics, which reproduces on a bare _WrappingCheckBox once it is shown — it does
not need the full SuggestionDialog or an Anki collection. Replace the heavy
profile_loaded() integration test with a fast widget test that shows the checkbox
(to polish it) and asserts heightForWidth reserves enough height for the painted
text across widths and font sizes. Verified it fails on the cached-pre-polish bug
and passes on the fix.
@RisingOrange RisingOrange marked this pull request as ready for review June 8, 2026 15:23
@RisingOrange RisingOrange requested a review from a team June 8, 2026 15:23

@ddevdan ddevdan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM but we need to create the addon for Maryna/Daemon to do QA

Drop the 360px cap on the Include-in-suggestion panel so the existing
2:3 stretch factors keep both columns growing with the window. The cap
predates the elision/wrapping work; long titles and tags can no longer
push the dialog wide.
@RisingOrange RisingOrange requested a review from ddevdan June 10, 2026 19:15

@ddevdan ddevdan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM
But, just a heads up
Next steps before merging:

  1. Create PR env
  2. Set the feature flag there
  3. Create addon pointing to the PR env
  4. Move to Ready for QA

@ddevdan

ddevdan commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

I was wondering if we could fix this, but I think it's more of a bug on Anki's side than in the add-on.

image

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