Skip to content

[proposal][desktop] Tab character (\t) should be displayed as indentation in TextField #77512

Open

Description

@aldrinmathew

Tab Character \t should be displayed as indentation in EditableText, TextField and all implementations of Text widgets in Desktop

This is a common behaviour in many desktop applications

Flutter Channel: master
Platform: Desktop, Linux
Version: 2.1.0-11.0.pre.138

When Tab key is pressed while the TextField is focused, in a Flutter Desktop Application, by default, the focused TextField is unfocused and the focus shifts to another widget. That is a problem in my case as I want the user to be able to enter tab characters. But I partially solved it by using a RawKeyboardListener to detect when the user presses Tab key, and then inserted a \t to the TextField at the current cursor position through its controller.
The problem is that, the \t character is displayed as a small space, almost half as wide as whitespace, which is counterproductive.
There should be an option to configure how \t is displayed, as it can be of multiple number of spaces' width, and it should be customisable.

I believe this is not exactly a bug. For mobile devices, the Tab key is irrelevant. For web, pressing Tab key is helpful in shifting focus between UI elements.
But for desktop, it makes sense to have an option to customise how a Tab character is displayed in a TextField.

Suggestion

Add an option to disable focus shifting on Tab press on Desktop and add Tab character to the TextField. Provide a way to customise Tab character display, by equating it to a specific number of whitespaces that can be changed if required.

Activity

  1. pedromassangocode commented on Mar 8, 2021

    @pedromassangocode

    Yes this is definitely a proposal. Just tested on Slack MacOS app and Tab button move focus to another component.

  2. changed the title [-]\t Tab character display in TextField[/-] [+][proposal][desktop] Tab character (\t) to add spaces in TextField[/+] on Mar 8, 2021
  3. added
    a: text inputEntering text in a text field or keyboard related problems
    frameworkflutter/packages/flutter repository. See also f: labels.
    c: proposalA detailed proposal for a change to Flutter
    c: new featureNothing broken; request for a new capability
    and removed
    in triagePresently being triaged by the triage team
    on Mar 8, 2021
  4. TimWhiting commented on Mar 11, 2021

    @TimWhiting
    Contributor

    Found a way to accomplish this:

    But I agree that there should be two properties on EditableText and TextField called something like:
    bool acceptTabs
    int tabIndentation
    And TextField or Editable text should handle wrapping in the Action/ Shortcuts widget.

    code sample
    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.dark(),
          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 InsertTabIntent extends Intent {
      const InsertTabIntent(this.numSpaces, this.textController);
      final int numSpaces;
      final TextEditingController textController;
    }
    
    class InsertTabAction extends Action {
      @override
      Object invoke(covariant Intent intent) {
        if (intent is InsertTabIntent) {
          final oldValue = intent.textController.value;
          final newComposing = TextRange.collapsed(oldValue.composing.start);
          final newSelection = TextSelection.collapsed(
              offset: oldValue.selection.start + intent.numSpaces);
    
          final newText = StringBuffer(oldValue.selection.isValid
              ? oldValue.selection.textBefore(oldValue.text)
              : oldValue.text);
          for (var i = 0; i < intent.numSpaces; i++) {
            newText.write(' ');
          }
          newText.write(oldValue.selection.isValid
              ? oldValue.selection.textAfter(oldValue.text)
              : '');
          intent.textController.value = intent.textController.value.copyWith(
            composing: newComposing,
            text: newText.toString(),
            selection: newSelection,
          );
        }
        return '';
      }
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      TextEditingController textController;
      @override
      void initState() {
        super.initState();
        textController = TextEditingController();
      }
    
      @override
      void dispose() {
        textController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Actions(
                  actions: {InsertTabIntent: InsertTabAction()},
                  child: Shortcuts(
                    shortcuts: {
                      LogicalKeySet(LogicalKeyboardKey.tab):
                          InsertTabIntent(2, textController)
                    },
                    child: TextField(
                      controller: textController,
                      textInputAction: TextInputAction.newline,
                      maxLines: 30,
                      keyboardType: TextInputType.multiline,
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
  5. aldrinmathew commented on Mar 11, 2021

    @aldrinmathew
    Author

    @TimWhiting @pedromassangocode

    I tested the implementation by Tim Whiting and it works well. 👍

    However, I reviewed the code and found that it is adding the appropriate number of spaces whenever the user presses Tab key. I have already implemented this in the application that I am developing. Is there anyway that we can add the '\t' character in the text and make it look like multiple spaces? I mean, without replacing the tab with spaces?
    In theory, we should be able to configure and customise the display of certain characters in the text field. Especially when we consider the possibility that desktop applications developed by flutter might not always be mere "applications", they might be complex enough to be called "software".

    Use case

    I want the user to be able to add the raw tab character (\t) to the text field. And I want to customise the display of the tab character to resemble the equivalent number of spaces chosen by the user.
    I am developing a text/code editor. I think you get where I am going with this. If a user/programmer presses tab to indent their content/code, and if I write code to replace the indentation with actual whitespace, someone's definitely getting angry. I can easily provide them with an option to replace '\t' with spaces, but that shouldn't be the default behaviour in this case.

  6. changed the title [-][proposal][desktop] Tab character (\t) to add spaces in TextField[/-] [+][proposal][desktop] Tab character (\t) should be displayed as indentation in TextField[/+] on Mar 11, 2021
  7. aldrinmathew commented on Mar 11, 2021

    @aldrinmathew
    Author

    @TimWhiting

    This is the modified code. This won't run as there is no property called indentSpaceCount for TextField.

    code sample
    import 'dart:io';
    
    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.dark(),
          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 InsertTabIntent extends Intent {
      const InsertTabIntent(this.textController);
      final TextEditingController textController;
    }
    
    class InsertTabAction extends Action {
      @override
      Object invoke(covariant Intent intent) {
        if (intent is InsertTabIntent) {
          final oldValue = intent.textController.value;
          final newComposing = TextRange.collapsed(oldValue.composing.start);
          final newSelection = TextSelection.collapsed(
              offset: oldValue.selection.start + 1);
    
          final newText = StringBuffer(oldValue.selection.isValid
              ? oldValue.selection.textBefore(oldValue.text)
              : oldValue.text);
          newText.write('\t');
          newText.write(oldValue.selection.isValid
              ? oldValue.selection.textAfter(oldValue.text)
              : '');
          intent.textController.value = intent.textController.value.copyWith(
            composing: newComposing,
            text: newText.toString(),
            selection: newSelection,
          );
        }
        return '';
      }
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      TextEditingController textController;
      @override
      void initState() {
        super.initState();
        textController = TextEditingController();
      }
    
      @override
      void dispose() {
        textController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Actions(
                  actions: {InsertTabIntent: InsertTabAction()},
                  child: Shortcuts(
                    shortcuts: {
                      LogicalKeySet(LogicalKeyboardKey.tab):
                          InsertTabIntent(textController)
                    },
                    child: TextField(
                      controller: textController,
                      textInputAction: TextInputAction.newline,
                      maxLines: 30,
                      keyboardType: TextInputType.multiline,
                      indentSpaceCount: 4,
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
  8. TimWhiting commented on Mar 11, 2021

    @TimWhiting
    Contributor

    Yeah, I agree, the properties should be more like:

    • bool acceptTabs (with this property set to false which should be the default pressing tab should change focus)
    • int indentSpaceCount (this property controls the visual spacing of a literal \t, default should probably be 4)
    • bool replaceTabsWithSpaces (this property controls whether to replace a literal \t with indentSpaceCount of spaces, default should be false) or there should be an InputFormatter provided by Flutter to do the same

    As a design note, for accessibility there needs to be a way to unfocus the field that accepts tabs to a larger focus scope to allow moving to a different field. I believe this is typically done by the escape key, but I could be wrong.

  9. aldrinmathew commented on Mar 11, 2021

    @aldrinmathew
    Author

    @TimWhiting

    Exactly, this model looks perfect. And yes, Escape key is usually the way to do it.

  10. 17 remaining items

  11. justinmc commented on Jul 26, 2021

    @justinmc
    Contributor

    Sorry I thought I had already replied here. I will see if I can get this in as part of my current text editing work. Thanks for the great design doc and good luck on your thesis!

  12. added
    P2Important issues not at the top of the work list
    on May 19, 2022
  13. flutter-triage-bot commented on Jun 7, 2024

    @flutter-triage-bot

    The triaged-desktop label is irrelevant if there is no team-desktop label or fyi-desktop label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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 problemsc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterframeworkflutter/packages/flutter repository. See also f: labels.team-text-inputOwned by Text Input teamtriaged-text-inputTriaged by Text Input team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions