🙆‍♀️

contextMenuBuilderなるものを使ってみた

2025/02/06に公開

Note(ノート)

入力をしたテキストをコピーしたりカットすることはcontextMenuBuilderというWidgetを使用すれば実現できるらしい。しかしいつの間にか、標準機能で搭載されていないか?

main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: FormWidget(),
    );
  }
}

class FormWidget extends StatefulWidget {
  const FormWidget({super.key});

  
  State<FormWidget> createState() => _FormWidgetState();
}

class _FormWidgetState extends State<FormWidget> {
  final TextEditingController _controller = TextEditingController();

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Form'),
        ),
        body: Column(
          children: [
            TextField(
              controller: _controller,
            )
          ],
        ));
  }
}

いや見た目が違うかも?
カスタマイズできるのだろうか....

example
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: EditableTextToolbarBuilderExampleApp(),
    );
  }
}

class EditableTextToolbarBuilderExampleApp extends StatefulWidget {
  const EditableTextToolbarBuilderExampleApp({super.key});

  
  State<EditableTextToolbarBuilderExampleApp> createState() =>
      _EditableTextToolbarBuilderExampleAppState();
}

class _EditableTextToolbarBuilderExampleAppState
    extends State<EditableTextToolbarBuilderExampleApp> {
  final TextEditingController _controller = TextEditingController();

  
  void initState() {
    super.initState();
    // ブラウザのコンテキストメニューを無効にする
    if (kIsWeb) {
      BrowserContextMenu.disableContextMenu();
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Custom button appearance'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            const SizedBox(height: 20.0),
            TextField(
              controller: _controller,
              contextMenuBuilder:
                  (BuildContext context, EditableTextState editableTextState) {
                return CustomContextMenu(
                  anchors: editableTextState.contextMenuAnchors,
                  allButtons: editableTextState.contextMenuButtonItems,
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

class CustomContextMenu extends StatefulWidget {
  final TextSelectionToolbarAnchors anchors;
  final List<ContextMenuButtonItem> allButtons;

  const CustomContextMenu({
    super.key,
    required this.anchors,
    required this.allButtons,
  });

  
  State<CustomContextMenu> createState() => _CustomContextMenuState();
}

class _CustomContextMenuState extends State<CustomContextMenu> {
  // 初期は2個表示
  int visibleCount = 2;

  
  void deactivate() {
    // ウィジェットが非表示になるときに初期状態に戻す
    visibleCount = 2;
    super.deactivate();
  }

  
  Widget build(BuildContext context) {
    final int total = widget.allButtons.length;

    // visibleCount が超えないように調整
    final List<ContextMenuButtonItem> visibleButtons =
        widget.allButtons.take(visibleCount).toList();

    // 表示件数が全件未満の場合は「もっと」ボタンを追加
    final bool showMoreButton = visibleCount < total;

    return AdaptiveTextSelectionToolbar(
      anchors: widget.anchors,
      children: [
        Container(
          constraints: const BoxConstraints(maxWidth: 300),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(8.0),
            boxShadow: const [
              BoxShadow(
                color: Colors.black26,
                blurRadius: 4.0,
              ),
            ],
          ),
          padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
          child: SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                ...visibleButtons.map((buttonItem) {
                  return Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 4.0),
                    child: CupertinoButton(
                      color: Colors.transparent,
                      padding: EdgeInsets.zero,
                      onPressed: buttonItem.onPressed,
                      child: Text(
                        CupertinoTextSelectionToolbarButton.getButtonLabel(
                            context, buttonItem),
                        style: const TextStyle(color: Colors.black),
                      ),
                    ),
                  );
                }),
                if (showMoreButton)
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 4.0),
                    child: CupertinoButton(
                      color: Colors.transparent,
                      padding: EdgeInsets.zero,
                      onPressed: () {
                        setState(() {
                          visibleCount = (visibleCount + 2).clamp(0, total);
                        });
                      },
                      child: const Text(
                        'more',
                        style: TextStyle(color: Colors.black),
                      ),
                    ),
                  ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

https://youtube.com/shorts/xNCyEtQXp6M

Que(きっかけ)

contextMenuBuilderの名前は今日思い出したのだが、以前ルーカスさんに教えていただきました。古いFlutterだけなのかもしれないがコピーしかできない。しかしテキストをカットしたり全部選択するメニューを表示できるようだ。Swiftだと標準でついているらしい。自分の作ったSwiftUIのアプリではできていたので、以前のFlutterにはなかったのでしょうね。

いつの間に標準機能になっていたのか?
 
https://tech.youtrust.co.jp/?page=1736838057

Summary(要約)

contextMenuBuilderを使用することで、Flutterアプリのテキスト操作をより直感的で使いやすいものにカスタマイズできます。ユーザーの操作性を向上させながら、アプリのブランディングに合わせたUIを実現できる強力な機能です。

Discussion