De niet-opgeslagen-stip verdwijnt weer als je opslaat #1611

Merged
brenno merged 2 commits from fix/stip-blijft-na-opslaan into main 2026-08-20 15:38:44 +00:00
Owner

De niet-opgeslagen-stip bleef staan nadat je had opgeslagen. Gemeld voor een presentatie én voor een document; de oorzaak ligt in de editors, niet in het opslaan.

Wat er misging

Een TextEditingController wekt zijn luisteraars bij élke wijziging van zijn waarde — en de cursorpositie zit in diezelfde waarde. Eén klik in een tekstveld is dus net zo goed een melding als een aanslag. De dia-editors lazen elke melding als een bewerking en schreven een ongewijzigde dia terug naar het deck.

Met de diagnose in de draaiende app, op een zojuist opgeslagen presentatie:

### SAVETOPATH ... userEdited=false
### DECK DIRTY->FALSE          ← opslaan maakte het deck schoon
### UNDO PUSH len=3
#3  _BulletsEditorState._emit  ← alleen in het veld geklikt, niets getypt
### DECK DIRTY->TRUE

Drie gevolgen, alle drie zichtbaar voor de gebruiker:

  1. Na het opslaan maakte de eerstvolgende klik het deck meteen weer 'gewijzigd'. In de praktijk verdween de stip dus nooit.
  2. Ongedaan maken kreeg een stap die niets terugdraait — één per klik in een veld.
  3. Een nog niet bewaarde presentatie opslaan ging zelfs zónder tussenklik mis. Het bewaarvenster haalt de focus weg en geeft hem terug terwijl er geschreven wordt; saveAs zag daardoor een gegroeide ongedaan-stapel, concludeerde met de heuristiek uit #1473 dat er "tijdens het schrijven was doorgetypt", en liet het deck bewust vuil. Bestand geschreven, stip blijft staan — en het herstelbestand bleef op schijf staan voor werk dat allang bewaard was. Zo gemeten in de app: SAVEAS deckChanged=true undoBefore=1 undoNow=2 userEdited=true.

Wat er verandert

Nieuw: EditorTextController (lib/widgets/editors/editor_text_controller.dart).

  • addListener blijft ongefilterd — het tekstveld zelf moet elke cursorwissel zien, anders beweegt de cursor niet mee op het scherm.
  • addTextListener meldt alleen een échte tekstwijziging.

Alle dia-editors, de notitievelden van het presentatiescherm en de tabelbewerker in de weergave gaan nu langs die tweede ingang. TextField(onChanged:) was al veilig — dat vuurt alleen op tekst — dus editors die dáármee werken blijven ongemoeid.

Aardig detail: in table_edit_controller.dart stond dit filter al met de hand geschreven, mét een commentaar dat het probleem precies beschrijft. Daar was het één keer lokaal opgelost; nu is het het algemene geval, en wat daar overblijft zijn de rij- en kolombewerkingen.

De heuristiek in saveAs (#1473) blijft zoals hij is: hij heeft nu geen valse trigger meer, en zonder aanleiding een tweede mechanisme naast een bestaande vangnet zetten maakt het geheel niet begrijpelijker.

Toetsen

test/caret_is_not_an_edit_test.dart, eerst rood tegen de onherstelde code (2 van de 3 klik-toetsen faalden met precies één spookbewerking):

  • klikken in een opsommingsveld, een titelveld en een codeveld werkt de dia niet bij;
  • typen werkt hem wél bij (anders bewijst het bovenstaande niets);
  • de controller zelf: een cursorwissel bereikt addListener maar niet addTextListener, en een afgemelde luisteraar hoort niets meer;
  • scherm-breed, zoals de klacht luidt: een schoon deck blijft schoon na een klik in de editor.

Het codeveld heeft een eigen toets omdat die editor zijn luisteraar als losse closure ophangt in plaats van als benoemde methode — de vorm die bij het omzetten het makkelijkst wordt overgeslagen.

make check-full groen: 10.191 tests, dekking 87,2%, per-bestand-vloer 0, gitleaks + trufflehog zonder bevindingen, semgrep 0 findings. DAST (ZAP) niet gedraaid — deze wijziging raakt het geserveerde weboppervlak niet.

Wat niet met eigen ogen is nagekeken: het opslaan-met-bewaarvenster in de draaiende app na de reparatie. De reproductie vóór de reparatie is wél live gedaan (daar komen de logregels hierboven vandaan); de nacontrole strandde op een vergrendeld scherm. De widgettest dekt het klik-pad volledig, maar kan het systeem-bewaarvenster niet nabootsen.

Bewaker: overgeslagen, met reden. Deze wijziging raakt het bestandsformaat niet, de opslag niet, geen afhankelijkheid, geen uitgaand verkeer en geen publieke belofte — het is een interne correctie in de editorlaag.

Meegeleverde refactor

fullscreen_presenter.dart stond exact op zijn basislijn, dus de ene importregel die de fix nodig heeft tripte de groottepoort. _replaceSlide — dat geen enkel veld van de schermtoestand raakt en alleen vanuit presenter_content en presenter_table wordt aangeroepen — verhuist naar presenter_support.dart, in een eigen commit. Het bestand zakt van 1043 naar 1038 regels en de basislijn zakt mee.

De niet-opgeslagen-stip bleef staan nadat je had opgeslagen. Gemeld voor een presentatie én voor een document; de oorzaak ligt in de editors, niet in het opslaan. ## Wat er misging Een `TextEditingController` wekt zijn luisteraars bij élke wijziging van zijn *waarde* — en de cursorpositie zit in diezelfde waarde. Eén klik in een tekstveld is dus net zo goed een melding als een aanslag. De dia-editors lazen elke melding als een bewerking en schreven een **ongewijzigde** dia terug naar het deck. Met de diagnose in de draaiende app, op een zojuist opgeslagen presentatie: ``` ### SAVETOPATH ... userEdited=false ### DECK DIRTY->FALSE ← opslaan maakte het deck schoon ### UNDO PUSH len=3 #3 _BulletsEditorState._emit ← alleen in het veld geklikt, niets getypt ### DECK DIRTY->TRUE ``` Drie gevolgen, alle drie zichtbaar voor de gebruiker: 1. **Na het opslaan maakte de eerstvolgende klik het deck meteen weer 'gewijzigd'.** In de praktijk verdween de stip dus nooit. 2. **Ongedaan maken kreeg een stap die niets terugdraait** — één per klik in een veld. 3. **Een nog niet bewaarde presentatie opslaan ging zelfs zónder tussenklik mis.** Het bewaarvenster haalt de focus weg en geeft hem terug terwijl er geschreven wordt; `saveAs` zag daardoor een gegroeide ongedaan-stapel, concludeerde met de heuristiek uit #1473 dat er "tijdens het schrijven was doorgetypt", en liet het deck bewust vuil. Bestand geschreven, stip blijft staan — en het herstelbestand bleef op schijf staan voor werk dat allang bewaard was. Zo gemeten in de app: `SAVEAS deckChanged=true undoBefore=1 undoNow=2 userEdited=true`. ## Wat er verandert Nieuw: `EditorTextController` (`lib/widgets/editors/editor_text_controller.dart`). - `addListener` blijft **ongefilterd** — het tekstveld zelf moet elke cursorwissel zien, anders beweegt de cursor niet mee op het scherm. - `addTextListener` meldt alleen een échte tekstwijziging. Alle dia-editors, de notitievelden van het presentatiescherm en de tabelbewerker in de weergave gaan nu langs die tweede ingang. `TextField(onChanged:)` was al veilig — dat vuurt alleen op tekst — dus editors die dáármee werken blijven ongemoeid. Aardig detail: in `table_edit_controller.dart` stond dit filter al met de hand geschreven, mét een commentaar dat het probleem precies beschrijft. Daar was het één keer lokaal opgelost; nu is het het algemene geval, en wat daar overblijft zijn de rij- en kolombewerkingen. De heuristiek in `saveAs` (#1473) blijft zoals hij is: hij heeft nu geen valse trigger meer, en zonder aanleiding een tweede mechanisme naast een bestaande vangnet zetten maakt het geheel niet begrijpelijker. ## Toetsen `test/caret_is_not_an_edit_test.dart`, eerst rood tegen de onherstelde code (2 van de 3 klik-toetsen faalden met precies één spookbewerking): - klikken in een opsommingsveld, een titelveld en een codeveld werkt de dia niet bij; - typen werkt hem wél bij (anders bewijst het bovenstaande niets); - de controller zelf: een cursorwissel bereikt `addListener` maar niet `addTextListener`, en een afgemelde luisteraar hoort niets meer; - scherm-breed, zoals de klacht luidt: een schoon deck blijft schoon na een klik in de editor. Het codeveld heeft een eigen toets omdat die editor zijn luisteraar als losse closure ophangt in plaats van als benoemde methode — de vorm die bij het omzetten het makkelijkst wordt overgeslagen. `make check-full` groen: 10.191 tests, dekking 87,2%, per-bestand-vloer 0, gitleaks + trufflehog zonder bevindingen, semgrep 0 findings. DAST (ZAP) niet gedraaid — deze wijziging raakt het geserveerde weboppervlak niet. **Wat niet met eigen ogen is nagekeken:** het opslaan-met-bewaarvenster in de draaiende app na de reparatie. De reproductie vóór de reparatie is wél live gedaan (daar komen de logregels hierboven vandaan); de nacontrole strandde op een vergrendeld scherm. De widgettest dekt het klik-pad volledig, maar kan het systeem-bewaarvenster niet nabootsen. **Bewaker:** overgeslagen, met reden. Deze wijziging raakt het bestandsformaat niet, de opslag niet, geen afhankelijkheid, geen uitgaand verkeer en geen publieke belofte — het is een interne correctie in de editorlaag. ## Meegeleverde refactor `fullscreen_presenter.dart` stond exact op zijn basislijn, dus de ene importregel die de fix nodig heeft tripte de groottepoort. `_replaceSlide` — dat geen enkel veld van de schermtoestand raakt en alleen vanuit `presenter_content` en `presenter_table` wordt aangeroepen — verhuist naar `presenter_support.dart`, in een eigen commit. Het bestand zakt van 1043 naar 1038 regels en de basislijn zakt mee.
`fullscreen_presenter.dart` stond exact op zijn basislijn, dus de eerste
regel die de volgende reparatie nodig heeft tript de groottepoort. De
helper die daarvoor moest wijken staat er ook het minst op zijn plaats:
`_replaceSlide` raakt geen enkel veld van de schermtoestand en wordt
alleen aangeroepen vanuit `presenter_content` en `presenter_table`.

Verhuisd naar `presenter_support.dart` — zelfde library, dus private
blijft private — mét de uitleg waaróm hij de gecachte splitsingsindeling
weggooit. Het hoofdbestand zakt van 1043 naar 1038 regels; de basislijn
zakt mee, want een gewonnen regel die je niet vastzet win je nog een keer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(editor): een cursor die verspringt is geen bewerking
All checks were successful
scans / scans (pull_request) Successful in 2m58s
static-gate / static-gate (pull_request) Successful in 8m32s
d784228f9b
Opslaan werkte, maar de niet-opgeslagen-stip bleef staan. De oorzaak zit
niet in het opslaan: een `TextEditingController` wekt zijn luisteraars
óók wanneer alleen de cursor of de selectie verschuift — die positie zit
in dezelfde waarde als de tekst. De dia-editors lazen élke melding als
een bewerking en schreven een *ongewijzigde* dia terug naar het deck.

Drie gevolgen, en de gebruiker ziet ze alle drie. Na het opslaan maakte
de eerstvolgende klik in een tekstveld de presentatie meteen weer
'gewijzigd'. Ongedaan maken kreeg een stap die niets terugdraait. En bij
het opslaan van een nóg niet bewaarde presentatie ging het zelfs zonder
tussenklik mis: het bewaarvenster haalt de focus weg en geeft hem terug
terwijl er geschreven wordt, waarop `saveAs` aan de gegroeide
ongedaan-stapel meende dat er tijdens het schrijven was doorgetypt
(#1473) en het deck bewust vuil liet. De stip verscheen dan meteen ná het
schrijven weer, en verdween nooit — met een herstelbestand dat bleef
staan voor werk dat allang op schijf stond.

Nieuw is `EditorTextController`. `addListener` blijft ongefilterd, want
het tekstveld zelf moet elke cursorwissel zien — anders beweegt de cursor
niet mee op het scherm. Wie `addTextListener` gebruikt krijgt alleen
echte tekstwijzigingen. Alle dia-editors, de notitievelden van het
presentatiescherm en de tabelbewerker in de weergave gaan nu langs die
tweede ingang. In `table_edit_controller.dart` stond hier al een
handgeschreven filter tegen; dat is nu het algemene geval, en wat daar
overblijft zijn de rij- en kolombewerkingen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
brenno merged commit 9e909da74a into main 2026-08-20 15:38:44 +00:00
Sign in to join this conversation.
No description provided.