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.
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:
// 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:
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:
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:
// 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:
// 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.
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í:
// 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.
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:
- Barras de herramientas de selección de texto personalizadas simples
- Menú de clic derecho fuera de los campos de texto
- Edición de texto para escritorio - estable
- Capacidad de desactivar el menú contextual en TextFields
- APIs faltantes para el estilo de la barra de herramientas de selección de texto
- Habilitar la barra de herramientas de copia en todos los widgets
- Desactivar el menú contextual desde el navegador
- Los menús contextuales personalizados no aparecen para Flutter web
PRs relevantes:
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.