Skip to content

TextInputClient in macOS doesn't update the editingValue except for inserting characters  #82124

@osaxma

Description

@osaxma

When creating a custom TextInputClient, macOS does not update the editingValue for the following keystrokes:

  • all arrows
  • shift + an arrow
  • delete
  • cmd+a
  • cmd+c
  • cmd+x
  • cmd+v

The last three were not tested since I'm unable to make a selection with either shift + arrows or cmd+a.

Steps to Reproduce

using the code sample provided below, run the following sequence:

  1. type the sequence abcde
  2. press delete (nothing happen on macOS)
  3. type the sequence efg
  4. press left arrow key till baseOffset = 0 (nothing happen on macOS)
  5. press cmd + a (nothing happen on macOS)
  6. press cmd + c (nothing happen on macOS)
  7. press delete (nothing happen on macOS)

Expected results:
I expected the following result which was produced with the same steps on the web platform.

value => TextEditingValue(text: ┤├, selection: TextSelection(baseOffset: 0, extentOffset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤a├, selection: TextSelection(baseOffset: 1, extentOffset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤ab├, selection: TextSelection(baseOffset: 2, extentOffset: 2, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abc├, selection: TextSelection(baseOffset: 3, extentOffset: 3, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcd├, selection: TextSelection(baseOffset: 4, extentOffset: 4, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcde├, selection: TextSelection(baseOffset: 5, extentOffset: 5, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcd├, selection: TextSelection(baseOffset: 4, extentOffset: 4, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcde├, selection: TextSelection(baseOffset: 5, extentOffset: 5, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdef├, selection: TextSelection(baseOffset: 6, extentOffset: 6, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 7, extentOffset: 7, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 6, extentOffset: 6, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 5, extentOffset: 5, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 4, extentOffset: 4, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 3, extentOffset: 3, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 2, extentOffset: 2, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 1, extentOffset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 0, extentOffset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤abcdefg├, selection: TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
value => TextEditingValue(text: ┤├, selection: TextSelection(baseOffset: 0, extentOffset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
 

Actual results:

flutter: value => TextEditingValue(text: ┤a├, selection: TextSelection(baseOffset: 1, extentOffset: 1, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: value => TextEditingValue(text: ┤ab├, selection: TextSelection(baseOffset: 2, extentOffset: 2, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: value => TextEditingValue(text: ┤abc├, selection: TextSelection(baseOffset: 3, extentOffset: 3, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: value => TextEditingValue(text: ┤abcd├, selection: TextSelection(baseOffset: 4, extentOffset: 4, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: value => TextEditingValue(text: ┤abcde├, selection: TextSelection(baseOffset: 5, extentOffset: 5, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: value => TextEditingValue(text: ┤abcdee├, selection: TextSelection(baseOffset: 6, extentOffset: 6, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: value => TextEditingValue(text: ┤abcdeef├, selection: TextSelection(baseOffset: 7, extentOffset: 7, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
flutter: value => TextEditingValue(text: ┤abcdeefg├, selection: TextSelection(baseOffset: 8, extentOffset: 8, affinity: TextAffinity.upstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
Minimal Code Sample
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FocusNode focusNode;
  TextInputConnection textInputConnection;
  @override
  void initState() {
    super.initState();
    textInputConnection = TextInput.attach(
      CustomTextInputClient(),
      TextInputConfiguration(
          inputType: TextInputType.text,
          inputAction: TextInputAction.newline,
      ),
    );

    textInputConnection.show();
  }

  @override
  void dispose() {
    textInputConnection.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(),
    );
  }
}

class CustomTextInputClient extends TextInputClient {
  @override
  void connectionClosed() {
    // TODO: implement connectionClosed
  }

  @override
  // TODO: implement currentAutofillScope
  AutofillScope get currentAutofillScope {
    throw UnimplementedError();
  }

  @override
  // TODO: implement currentTextEditingValue
  TextEditingValue get currentTextEditingValue {
    throw UnimplementedError();
  }

  @override
  void performAction(TextInputAction action) {
    // TODO: implement performAction
  }

  @override
  void performPrivateCommand(String action, Map<String, dynamic> data) {
    // TODO: implement performPrivateCommand
  }

  @override
  void showAutocorrectionPromptRect(int start, int end) {
    // TODO: implement showAutocorrectionPromptRect
  }

  @override
  void updateEditingValue(TextEditingValue value) {
    // TODO: implement updateEditingValue
    print('value => $value');
  }

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {
    // TODO: implement updateFloatingCursor
  }
}
flutter doctor -v
[✓] Flutter (Channel stable, 2.0.6, on macOS 11.2.3 20D91 darwin-x64, locale en)
    • Flutter version 2.0.6 at /Users/osaxma/fvm/versions/stable
    • Framework revision 1d9032c7e1 (9 days ago), 2021-04-29 17:37:58 -0700
    • Engine revision 05e680e202
    • Dart version 2.12.3

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/osaxma/Library/Android/sdk
    • Platform android-30, build-tools 29.0.2
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.4, Build version 12D4e
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)

[✓] VS Code (version 1.56.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.22.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-x64     • macOS 11.2.3 20D91 darwin-x64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 90.0.4430.93

• No issues found!

side note:

I'm not sure if I'm missing something out since TextField works fine. I tried to look at the EditableText implementation to see if there's anything unique about macOS or desktop but I couldn't find such indication. Nevertheless, given that the same provided sample code works for web, I was expecting the same behavior on macOS. Please advise.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: desktopRunning on desktopa: text inputEntering text in a text field or keyboard related problemsa: typographyText rendering, possibly libtxtengineflutter/engine related. See also e: labels.found in release: 2.3Found to occur in 2.3has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-macBuilding on or for macOS specificallyteam-macosOwned by the macOS platform teamtriaged-macosTriaged by the macOS platform team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions