Saltar al contenido principal

Una nueva forma de personalizar los menús contextuales

Varios parámetros embebidos (hard-coded) para personalizar los menús contextuales han sido reemplazados ahora por un generador de widgets genérico.

Resumen

#

Los menús contextuales, o barras de herramientas de selección de texto, son los menús que aparecen al presionar prolongadamente o hacer clic derecho sobre el texto en Flutter, y muestran opciones como Cortar, Copiar, Pegar y Seleccionar todo. Anteriormente, solo era posible personalizarlos de forma limitada utilizando ToolbarOptions y TextSelectionControls. Ahora, se han hecho componibles utilizando widgets, al igual que todo lo demás en Flutter, y los parámetros de configuración específicos han been descontinuados (deprecated).

Contexto

#

Anteriormente, era posible deshabilitar botones de los menús contextuales usando TextSelectionControls, pero cualquier personalización más allá de eso requería copiar y editar cientos de líneas de clases personalizadas en el framework. Ahora, todo esto ha sido reemplazado por una función generadora simple, contextMenuBuilder, que permite usar cualquier widget de Flutter como menú contextual.

Descripción del cambio

#

Los menús contextuales ahora se construyen a partir del parámetro contextMenuBuilder, que se ha agregado a todos los widgets de edición y selección de texto. Si no se proporciona uno, Flutter simplemente lo establece en un valor predeterminado que construye el menú contextual correcto para la plataforma dada. Todos estos widgets predeterminados están expuestos a los usuarios para su reutilización. Personalizar los menús contextuales ahora consiste en usar contextMenuBuilder para retornar cualquier widget que desees, lo que posiblemente incluya reutilizar los widgets de menú contextual integrados.

Aquí tienes un ejemplo que muestra cómo agregar un botón Enviar correo electrónico a los menús contextuales predeterminados cada vez que se selecciona una dirección de correo electrónico. El código completo se puede encontrar en el repositorio de ejemplos en email_button_page.dart en GitHub.

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    final TextEditingValue value = editableTextState.textEditingValue;
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    if (isValidEmail(value.selection.textInside(value.text))) {
      buttonItems.insert(
          0,
          ContextMenuButtonItem(
            label: 'Send email',
            onPressed: () {
              ContextMenuController.removeAny();
              Navigator.of(context).push(_showDialog(context));
            },
          ));
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

Una gran cantidad de ejemplos de diferentes menús contextuales personalizados están disponibles en el repositorio de muestras en GitHub.

Todas las características descontinuadas relacionadas fueron marcadas con la advertencia de descontinuación "Usa contextMenuBuilder en su lugar".

Guía de migración

#

En general, cualquier cambio anterior en los menús contextuales que haya sido descontinuado ahora requiere el uso del parámetro contextMenuBuilder en el widget de edición o selección de texto correspondiente ( en TextField, por ejemplo). Retorna un widget de menú contextual integrado como AdaptiveTextSelectionToolbar para usar los menús contextuales integrados de Flutter, o retorna tu propio widget para algo totalmente personalizado.

Para la transición a contextMenuBuilder, se han descontinuado los siguientes parámetros y clases.

Esta clase se utilizaba anteriormente para habilitar o deshabilitar explícitamente ciertos botones en un menú contextual. Antes de este cambio, es posible que la hayas pasado a TextField u otros widgets de esta manera:

dart
// Deprecated.
TextField(
  toolbarOptions: ToolbarOptions(
    copy: true,
  ),
)

Ahora, puedes lograr el mismo efecto ajustando los buttonItems pasados a AdaptiveTextSelectionToolbar. Por ejemplo, podrías asegurarte de que el botón Cortar nunca aparezca, pero los demás botones aparezcan como de costumbre:

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    buttonItems.removeWhere((ContextMenuButtonItem buttonItem) {
      return buttonItem.type == ContextMenuButtonType.cut;
    });
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

O bien, podrías asegurarte de que el botón Cortar aparezca exclusiva y siempre:

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: <ContextMenuButtonItem>[
        ContextMenuButtonItem(
          onPressed: () {
            editableTextState.cutSelection(SelectionChangedCause.toolbar);
          },
          type: ContextMenuButtonType.cut,
        ),
      ],
    );
  },
)

TextSelectionControls.canCut y otros booleanos de botón

#

Estos booleanos tenían anteriormente el mismo efecto de habilitar y deshabilitar ciertos botones que tenían ToolbarOptions.cut y similares. Antes de este cambio, es posible que ocultaras y mostraras botones sobreescribiendo TextSelectionControls y configurando estos booleanos de esta manera:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool canCut() => false,
}

Consulta la sección anterior sobre ToolbarOptions para saber cómo lograr un efecto similar con contextMenuBuilder.

TextSelectionControls.handleCut y otros callbacks de botón

#

Estas funciones permitían modificar el callback que se llamaba cuando se presionaban los botones. Antes de este cambio, es posible que modificaras los callbacks de los botones del menú contextual sobreescribiendo estos métodos manejadores de esta manera:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool handleCut() {
    // My custom cut implementation here.
  },
}

Esto todavía es posible usando contextMenuBuilder, incluyendo llamar a las acciones de los botones originales en el manejador personalizado, usando widgets de barra de herramientas como AdaptiveTextSelectionToolbar.buttonItems.

Este ejemplo muestra cómo modificar el botón Copiar para mostrar un diálogo además de realizar su lógica de copia habitual.

dart
TextField(
  contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    final int copyButtonIndex = buttonItems.indexWhere(
      (ContextMenuButtonItem buttonItem) {
        return buttonItem.type == ContextMenuButtonType.copy;
      },
    );
    if (copyButtonIndex >= 0) {
      final ContextMenuButtonItem copyButtonItem =
          buttonItems[copyButtonIndex];
      buttonItems[copyButtonIndex] = copyButtonItem.copyWith(
        onPressed: () {
          copyButtonItem.onPressed();
          Navigator.of(context).push(
            DialogRoute<void>(
              context: context,
              builder: (BuildContext context) =>
                const AlertDialog(
                  title: Text('Copied, but also showed this dialog.'),
                ),
            );
          )
        },
      );
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

Un ejemplo completo de cómo modificar una acción del menú contextual integrada se puede encontrar en el repositorio de ejemplos en modified_action_page.dart en GitHub.

Esta función generaba el widget del menú contextual de manera similar a contextMenuBuilder, pero requería más configuración para su uso. Antes de este cambio, es posible que hayas sobreescrito buildToolbar como parte de TextSelectionControls, así:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
    double textLineHeight,
    Offset selectionMidpoint,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
    ClipboardStatusNotifier clipboardStatus,
    Offset lastSecondaryTapDownPosition,
  ) {
    return _MyCustomToolbar();
  },
}

Ahora simplemente puedes usar contextMenuBuilder directamente como parámetro para TextField (y otros). La información proporcionada en los parámetros de buildToolbar se puede obtener del EditableTextState que se pasa a contextMenuBuilder.

El siguiente ejemplo muestra cómo construir una barra de herramientas completamente personalizada desde cero mientras se siguen utilizando los botones predeterminados.

dart
class _MyContextMenu extends StatelessWidget {
  const _MyContextMenu({
    required this.anchor,
    required this.children,
  });

  final Offset anchor;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: anchor.dy,
          left: anchor.dx,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.amberAccent,
            child: Column(
              children: children,
            ),
          ),
        ),
      ],
    );
  }
}

class _MyTextField extends StatelessWidget {
  const _MyTextField();

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      maxLines: 4,
      minLines: 2,
      contextMenuBuilder: (context, editableTextState) {
        return _MyContextMenu(
          anchor: editableTextState.contextMenuAnchors.primaryAnchor,
          children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
            context,
            editableTextState.contextMenuButtonItems,
          ).toList(),
        );
      },
    );
  }
}

Un ejemplo completo de cómo construir un menú contextual personalizado se puede encontrar en el repositorio de ejemplos en custom_menu_page.dart en GitHub.

Timeline

#

Introducido en la versión: 3.6.0-0.0.pre
En la versión estable: 3.7.0

Referencias

#

Documentación de la API:

Issues relevantes:

PRs relevantes: