Tabelwerkbalk op desktop: rij toevoegen laat de werkbalk onder de cursor verdwijnen — focus en soms de actie gaan verloren #2140

Closed
opened 2026-09-20 21:19:54 +00:00 by brenno · 0 comments
Owner

Symptoom

In de documenteditor springt de focus weg zodra je via de tabelwerkbalk een rij toevoegt ("Rij eronder"/"Rij erboven"; dezelfde race zit in de kolomknoppen). Op desktop (macOS) komt bij een normale muisklik de rij vaak niet eens aan: de werkbalk verdwijnt onder de cursor vóór pointer-up, waardoor onPressed nooit vuurt. Bij een snelle klik kan de actie nét binnenkomen — dan komt de rij er wél maar landt de focus nergens, want de retentie is dan al opgeruimd. Beide varianten lezen als "de focus springt weg van waar ik stond".

Oorzaak — een frame-race in de actieve-cel-retentie

TableEditScaffold toont de werkbalk alleen als editor.activeCell != null (table_edit_scaffold.dart ~L112). De verdediging tegen werkbalk-taps bestaat uit twee post-frame-callbacks wiens volgorde ongunstig uitpakt:

  1. Pointer-down op een werkbalkknop → Listener.onPointerDown → holdActiveCell() (table_edit_controller.dart ~L92): zet _holdActiveCell = true en plant de reset als post-frame-callback.
  2. Dezelfde pointer-down unfocust de cel — op desktop doet EditableText.onTapOutside dat (mobiel niet!). setActiveCell(focused: false) (~L99-120) plant zijn eigen uitgestelde check.
  3. Aan het einde van het down-frame runnen de post-frames in registratievolgorde: eerst de reset (_holdActiveCell = false), dan de blur-check — die ziet nu _holdActiveCell == false, _pendingFocus == null, _focusScheduled == false, geen cel-focus → _activeCell = null.
  4. Pointer-up komt vrijwel altijd één of meer frames later → de werkbalk is weg → onPressed vuurt niet → niets gebeurt, focus staat op de FocusScope.

En zelfs als onPressed nog meekomt: insertRowAt (~L429) leest _activeCell — is die al gewist, dan slaat _retainActiveAfterStructure over en wordt er geen _focusAfterRebuild gepland → geen refocus → focus weg.

Waarom Visueel en Enter/Tab niet stuk zijn: in de visuele stand valt de werkbalk binnen Quill's EditableText-TapRegion, dus geldt de tap als "inside" en blurt de cel niet (gemeten: celFocus blijft true na pointer-down). Enter/Tab op de laatste rij roepen _focusAfterRebuild onvoorwaardelijk aan, zonder _activeCell nodig te hebben — die werken. Het Bron-preview-/documentoppervlak en TableEditor (dia's) hebben geen omhullende EditableText en zijn wél kwetsbaar.

Waarom de tests dit niet vingen: widget-tests draaien als Android, waar onTapOutside nooit unfocust — het blurpad wordt dus nooit betreden. De bestaande retentietests roepen focusNode.unfocus() en daarna synchroon de actie aan; de echte race zit tussen de frames van één muisklik.

Reproductie

Widget-test op DocumentEditorScreen in Bron-stand, met debugDefaultTargetPlatformOverride = TargetPlatform.macOS en echte pointer-timing:

final cell = find.descendant(
  of: find.byType(Table),
  matching: find.widgetWithText(TextField, 'Alfa'),
);
await tester.showKeyboard(cell);
await tester.pump(); await tester.pump();

final gesture = await tester.startGesture(
  tester.getCenter(find.byTooltip('Rij eronder')),
);
await tester.pump(); await tester.pump();   // ← hier is de toolbar al weg
await gesture.up();
await tester.pump(); await tester.pump();

// Gemeten: toolbar=false na pointer-down, geen rij toegevoegd,
// primaryFocus = FocusScope (nirgends), geen cel gefocust.

Oplossingsrichting

Maak de retentie niet afhankelijk van post-frame-volgorde, maar van de interactie zelf:

  • holdActiveCell moet de hold vasthouden tot de pointer-interactie eindigt, niet één frame: onPointerUp/onPointerCancel (of onTapCancel) op de Listener laten de reset pas ná onPressed lopen. Dan staat _activeCell gegarandeerd nog bij onPressed, en insertRowAt → _retainActiveAfterStructure → _focusAfterRebuild → de bestaande refocusketen (_pendingFocus → takePendingFocus → focusCell) werkt zoals ontworpen.
  • Alternatief dat net zo klein is: een _heldCell-veld dat bij pointer-down de activeCell vastlegt en pas bij pointer-up opruimt, en de blur-check laat _heldCell meewegen.
  • Regressietest toevoegen die draait met TargetPlatform.macOS en startGesture met frames tussen down en up — dat is de enige as die deze race zichtbaar maakt. Ideaal als één test die beweert: na "Rij eronder" staat er een rij meer in de bron én heeft een tabelcel de focus.

Betreffende plekken: lib/widgets/reader/table_edit_controller.dart (holdActiveCell, setActiveCell, insertRowAt, _retainActiveAfterStructure), lib/widgets/reader/table_edit_scaffold.dart (toolbar-Listener, active == null → werkbalk weg).

## Symptoom In de documenteditor springt de focus weg zodra je via de tabelwerkbalk een rij toevoegt ("Rij eronder"/"Rij erboven"; dezelfde race zit in de kolomknoppen). Op desktop (macOS) komt bij een normale muisklik de rij vaak niet eens aan: de werkbalk verdwijnt onder de cursor vóór pointer-up, waardoor `onPressed` nooit vuurt. Bij een snelle klik kan de actie nét binnenkomen — dan komt de rij er wél maar landt de focus nergens, want de retentie is dan al opgeruimd. Beide varianten lezen als "de focus springt weg van waar ik stond". ## Oorzaak — een frame-race in de actieve-cel-retentie `TableEditScaffold` toont de werkbalk alleen als `editor.activeCell != null` (`table_edit_scaffold.dart` ~L112). De verdediging tegen werkbalk-taps bestaat uit twee post-frame-callbacks wiens volgorde ongunstig uitpakt: 1. Pointer-down op een werkbalkknop → `Listener.onPointerDown` → `holdActiveCell()` (`table_edit_controller.dart` ~L92): zet `_holdActiveCell = true` en plant de reset als post-frame-callback. 2. Dezelfde pointer-down unfocust de cel — op desktop doet `EditableText.onTapOutside` dat (mobiel niet!). `setActiveCell(focused: false)` (~L99-120) plant zijn eigen uitgestelde check. 3. Aan het einde van het down-frame runnen de post-frames **in registratievolgorde**: eerst de reset (`_holdActiveCell = false`), dan de blur-check — die ziet nu `_holdActiveCell == false`, `_pendingFocus == null`, `_focusScheduled == false`, geen cel-focus → **`_activeCell = null`**. 4. Pointer-up komt vrijwel altijd één of meer frames later → de werkbalk is weg → `onPressed` vuurt niet → niets gebeurt, focus staat op de FocusScope. En zelfs als `onPressed` nog meekomt: `insertRowAt` (~L429) leest `_activeCell` — is die al gewist, dan slaat `_retainActiveAfterStructure` over en wordt er geen `_focusAfterRebuild` gepland → geen refocus → focus weg. **Waarom Visueel en Enter/Tab niet stuk zijn:** in de visuele stand valt de werkbalk binnen Quill's `EditableText`-`TapRegion`, dus geldt de tap als "inside" en blurt de cel niet (gemeten: celFocus blijft `true` na pointer-down). Enter/Tab op de laatste rij roepen `_focusAfterRebuild` onvoorwaardelijk aan, zonder `_activeCell` nodig te hebben — die werken. Het Bron-preview-/documentoppervlak en `TableEditor` (dia's) hebben geen omhullende EditableText en zijn wél kwetsbaar. **Waarom de tests dit niet vingen:** widget-tests draaien als Android, waar `onTapOutside` nooit unfocust — het blurpad wordt dus nooit betreden. De bestaande retentietests roepen `focusNode.unfocus()` en daarna synchroon de actie aan; de echte race zit tussen de frames van één muisklik. ## Reproductie Widget-test op `DocumentEditorScreen` in Bron-stand, met `debugDefaultTargetPlatformOverride = TargetPlatform.macOS` en echte pointer-timing: ```dart final cell = find.descendant( of: find.byType(Table), matching: find.widgetWithText(TextField, 'Alfa'), ); await tester.showKeyboard(cell); await tester.pump(); await tester.pump(); final gesture = await tester.startGesture( tester.getCenter(find.byTooltip('Rij eronder')), ); await tester.pump(); await tester.pump(); // ← hier is de toolbar al weg await gesture.up(); await tester.pump(); await tester.pump(); // Gemeten: toolbar=false na pointer-down, geen rij toegevoegd, // primaryFocus = FocusScope (nirgends), geen cel gefocust. ``` ## Oplossingsrichting Maak de retentie niet afhankelijk van post-frame-volgorde, maar van de interactie zelf: - `holdActiveCell` moet de hold vasthouden **tot de pointer-interactie eindigt**, niet één frame: `onPointerUp`/`onPointerCancel` (of `onTapCancel`) op de `Listener` laten de reset pas ná `onPressed` lopen. Dan staat `_activeCell` gegarandeerd nog bij `onPressed`, en `insertRowAt` → `_retainActiveAfterStructure` → `_focusAfterRebuild` → de bestaande refocusketen (`_pendingFocus` → `takePendingFocus` → `focusCell`) werkt zoals ontworpen. - Alternatief dat net zo klein is: een `_heldCell`-veld dat bij pointer-down de `activeCell` vastlegt en pas bij pointer-up opruimt, en de blur-check laat `_heldCell` meewegen. - Regressietest toevoegen die draait met `TargetPlatform.macOS` en `startGesture` met frames tussen down en up — dat is de enige as die deze race zichtbaar maakt. Ideaal als één test die beweert: na "Rij eronder" staat er een rij meer in de bron én heeft een tabelcel de focus. Betreffende plekken: `lib/widgets/reader/table_edit_controller.dart` (`holdActiveCell`, `setActiveCell`, `insertRowAt`, `_retainActiveAfterStructure`), `lib/widgets/reader/table_edit_scaffold.dart` (toolbar-`Listener`, `active == null` → werkbalk weg).
brenno 2026-09-20 23:36:23 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#2140
No description provided.