Editeurs de traduction : le curseur ne saute plus a chaque frappe

`QuillEditor.basic` fabrique un `FocusNode` et un `ScrollController` neufs a
chaque construction quand on ne lui en passe pas. Or chaque frappe reconstruit
ces widgets — compteur de caracteres, pastilles du rail de langues — donc
l'editeur perdait le focus des le premier caractere saisi.

Les deux conteneurs creent desormais leurs noeuds une fois, dans `initState`,
un par langue, et les liberent au `dispose`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-09-09 16:54:56 +02:00
parent 0df5dc03fa
commit 83eba7ff54
2 changed files with 45 additions and 0 deletions

View File

@ -41,6 +41,12 @@ class TranslationInputAndResourceContainer extends StatefulWidget {
class _TranslationInputAndResourceContainerState class _TranslationInputAndResourceContainerState
extends State<TranslationInputAndResourceContainer> { extends State<TranslationInputAndResourceContainer> {
late Map<String, QuillController> _controllers; late Map<String, QuillController> _controllers;
/// Meme piege que dans `translation_input_container` : sans `focusNode` ni
/// `scrollController`, `QuillEditor.basic` en cree de neufs a chaque
/// construction et le champ perd le curseur des qu'un rebuild survient.
late Map<String, FocusNode> _focusNodes;
late Map<String, ScrollController> _editorScrollControllers;
int _selected = 0; int _selected = 0;
bool _isEnforcingLimit = false; bool _isEnforcingLimit = false;
bool _isTranslating = false; bool _isTranslating = false;
@ -50,6 +56,14 @@ class _TranslationInputAndResourceContainerState
void initState() { void initState() {
super.initState(); super.initState();
_controllers = _buildControllers(); _controllers = _buildControllers();
_focusNodes = {
for (final translation in widget.newValues)
translation.language!: FocusNode(),
};
_editorScrollControllers = {
for (final translation in widget.newValues)
translation.language!: ScrollController(),
};
} }
static const _emptyDelta = [{'insert': '\n'}]; static const _emptyDelta = [{'insert': '\n'}];
@ -172,6 +186,12 @@ class _TranslationInputAndResourceContainerState
for (final c in _controllers.values) { for (final c in _controllers.values) {
c.dispose(); c.dispose();
} }
for (final node in _focusNodes.values) {
node.dispose();
}
for (final c in _editorScrollControllers.values) {
c.dispose();
}
super.dispose(); super.dispose();
} }
@ -396,6 +416,8 @@ class _TranslationInputAndResourceContainerState
), ),
child: QuillEditor.basic( child: QuillEditor.basic(
controller: controller, controller: controller,
focusNode: _focusNodes[lang],
scrollController: _editorScrollControllers[lang],
config: const QuillEditorConfig( config: const QuillEditorConfig(
scrollable: true, scrollable: true,
expands: false, expands: false,

View File

@ -47,6 +47,13 @@ class TranslationInputContainer extends StatefulWidget {
class _TranslationInputContainerState extends State<TranslationInputContainer> { class _TranslationInputContainerState extends State<TranslationInputContainer> {
late Map<String, QuillController> _controllers; late Map<String, QuillController> _controllers;
late Map<String, TextEditingController> _textControllers; late Map<String, TextEditingController> _textControllers;
/// `QuillEditor.basic` fabrique un `FocusNode` et un `ScrollController` neufs
/// a chaque construction quand on ne lui en passe pas : le curseur sautait de
/// l'editeur des la premiere frappe, puisque chaque frappe reconstruit ce
/// widget (compteur de caracteres, pastilles du rail).
late Map<String, FocusNode> _focusNodes;
late Map<String, ScrollController> _editorScrollControllers;
int _selected = 0; int _selected = 0;
bool _isEnforcingLimit = false; bool _isEnforcingLimit = false;
bool _isTranslating = false; bool _isTranslating = false;
@ -57,6 +64,14 @@ class _TranslationInputContainerState extends State<TranslationInputContainer> {
super.initState(); super.initState();
_controllers = widget.isHTML ? _buildControllers() : {}; _controllers = widget.isHTML ? _buildControllers() : {};
_textControllers = widget.isHTML ? {} : _buildTextControllers(); _textControllers = widget.isHTML ? {} : _buildTextControllers();
_focusNodes = {
for (final translation in widget.newValues)
translation.language!: FocusNode(),
};
_editorScrollControllers = {
for (final translation in widget.newValues)
translation.language!: ScrollController(),
};
} }
Map<String, TextEditingController> _buildTextControllers() { Map<String, TextEditingController> _buildTextControllers() {
@ -207,6 +222,12 @@ class _TranslationInputContainerState extends State<TranslationInputContainer> {
for (final c in _textControllers.values) { for (final c in _textControllers.values) {
c.dispose(); c.dispose();
} }
for (final node in _focusNodes.values) {
node.dispose();
}
for (final c in _editorScrollControllers.values) {
c.dispose();
}
super.dispose(); super.dispose();
} }
@ -467,6 +488,8 @@ class _TranslationInputContainerState extends State<TranslationInputContainer> {
), ),
child: QuillEditor.basic( child: QuillEditor.basic(
controller: controller, controller: controller,
focusNode: _focusNodes[lang],
scrollController: _editorScrollControllers[lang],
config: const QuillEditorConfig( config: const QuillEditorConfig(
scrollable: true, scrollable: true,
expands: false, expands: false,