8

Like Google Drive, can I create custom menu in Flutter Web application?.

Subair K
  • 1,760
  • 1
  • 11
  • 31
  • 1
    There is an [open GitHub issue](https://github.com/flutter/flutter/issues/31955) to add that. So I don't think there's an easy way right now. – Lambda Fairy Jun 07 '20 at 11:30
  • 1
    `GestureDetector`'s `onSecondaryTapUp` event works as long as `document.onContextMenu.listen((event) => event.preventDefault());` is run. – nathanfranke Feb 03 '22 at 22:10

5 Answers5

18

Below the instruction how to implement working context menu called via mouse right button in flutter web app:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
    // Prevent default event handler
    document.onContextMenu.listen((event) => event.preventDefault());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: Center(
        child: Listener(
          child: Icon(
            Icons.ac_unit,
            size: 48.0,
          ),
          onPointerDown: _onPointerDown,
        ),
      ),
    );
  }

  /// Callback when mouse clicked on `Listener` wrapped widget.
  Future<void> _onPointerDown(PointerDownEvent event) async {
    // Check if right mouse button clicked
    if (event.kind == PointerDeviceKind.mouse &&
        event.buttons == kSecondaryMouseButton) {
      final overlay =
          Overlay.of(context).context.findRenderObject() as RenderBox;
      final menuItem = await showMenu<int>(
          context: context,
          items: [
            PopupMenuItem(child: Text('Copy'), value: 1),
            PopupMenuItem(child: Text('Cut'), value: 2),
          ],
          position: RelativeRect.fromSize(
              event.position & Size(48.0, 48.0), overlay.size));
      // Check if menu item clicked
      switch (menuItem) {
        case 1:
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(
            content: Text('Copy clicked'),
            behavior: SnackBarBehavior.floating,
          ));
          break;
        case 2:
          ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('Cut clicked'),
              behavior: SnackBarBehavior.floating));
          break;
        default:
      }
    }
  }
}

The only thing is to do is correct positioning of left top corner of context menu.

BambinoUA
  • 6,126
  • 5
  • 35
  • 51
  • Can you share a full example? There are some errors with this code – Subair K Feb 10 '21 at 03:50
  • But with this solution, we'll surely face problem while changing the screen size of the browser. The menu will be not be at it's correct position. How can we avoid that ? – Mayur Agarwal May 20 '21 at 15:56
  • @MayurAgarwal, did you test? I replaced `Center` with `Container(align: Alignment.bottomRight)` and it looks fine. – BambinoUA May 25 '21 at 11:01
4

Prevent default contextmenu

Add an oncontextmenu attribute to <html> tag in web/index.html:

<!DOCTYPE html>
<html oncontextmenu="event.preventDefault();">
<head>
...

See also: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#event_handler_attributes

This has the same effect as https://stackoverflow.com/a/64779321/16613821 (window.document is just the <html> tag), but without triggering "Avoid using web-only libraries outside Flutter web plugin packages." warning or using universal_html package.

NOTE: Hot reload won't work for this kind of change, but you can simply refresh(F5) browser.

Add your custom contextmenu

https://github.com/flutter/flutter/pull/74286 doesn't work well for your usecase

This should show up by default on desktop, but only when right clicking on EditableText-based widgets. Right clicking elsewhere does nothing, for now.

This is also purposely not customizable or reusable for now. It was a temporary solution that we plan to expand on.

In general, you can use GestureDetector.onSecondaryTap to detect user's right click.

YouJiacheng
  • 449
  • 3
  • 11
3

Until the open issue is resolved, you can do the following in your main():

import 'dart:html';

void main() {
  window.document.onContextMenu.listen((evt) => evt.preventDefault());
  // ...
}
2

Here is the open issue for it: https://github.com/flutter/flutter/issues/31955

You can disable it for a webpage like this: How do I disable right click on my web page?

You can also listen for Pointer Signal events and render the popup in Flutter: https://medium.com/@crizantlai/flutter-handling-mouse-events-241108731537

Basically on web for example you would disable the default context menu, and show an Overlay in flutter when you receive the right click pointer signal.

Rody Davis
  • 1,825
  • 13
  • 20
  • You can use this class: https://api.flutter.dev/flutter/gestures/PointerSignalEvent-class.html And use this widget Listener to have the callback onPointerSignal: https://api.flutter.dev/flutter/widgets/Listener-class.html – Rody Davis Sep 29 '20 at 20:54
  • Thanks doe the info, could u pls send a small example in which something gets printed if the use right clicks? – Maadhav Sharma Sep 30 '20 at 01:00
0

Thanks for the inspiration BambinoUA. I decided to make my own cross platform class for this.

Works on iOS/Android/Web/Windows/Mac & Linux. Tested.

import 'package:bap/components/splash_effect.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart' as html;

class CrossPlatformClick extends StatefulWidget {
  final Widget child;

  /**
   * Normal touch, tap, right click for platforms.
   */
  final Function()? onNormalTap;

  /**
   * A list of menu items for right click or long press.
   */
  final List<PopupMenuEntry<String>>? menuItems;
  final Function(String? itemValue)? onMenuItemTapped;

  const CrossPlatformClick({Key? key, required this.child, this.menuItems, this.onNormalTap, this.onMenuItemTapped}) : super(key: key);

  @override
  State<CrossPlatformClick> createState() => _CrossPlatformClickState();
}

class _CrossPlatformClickState extends State<CrossPlatformClick> {
  /**
   * We record this so that we can use long-press and location.
   */
  PointerDownEvent? _lastEvent;

  @override
  Widget build(BuildContext context) {
    final listener = Listener(
      child: widget.child,
      onPointerDown: (event) => _onPointerDown(context, event),
    );
    return SplashEffect(
      isDisabled: widget.onNormalTap == null,
      borderRadius: BorderRadius.zero,
      onTap: widget.onNormalTap!,
      child: listener,
      onLongPress: () {
        if (_lastEvent != null) {
          _openMenu(context, _lastEvent!);
          return;
        }
        if (kDebugMode) {
          print("Last event was null, cannot open menu");
        }
      },
    );
  }

  @override
  void initState() {
    super.initState();
    html.document.onContextMenu.listen((event) => event.preventDefault());
  }

  /// Callback when mouse clicked on `Listener` wrapped widget.
  Future<void> _onPointerDown(BuildContext context, PointerDownEvent event) async {
    _lastEvent = event;

    if (widget.menuItems == null) {
      return;
    }

    // Check if right mouse button clicked
    if (event.kind == PointerDeviceKind.mouse && event.buttons == kSecondaryMouseButton) {
      return await _openMenu(context, event);
    }
  }

  _openMenu(BuildContext context, PointerDownEvent event) async {
    final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox;
    final menuItem = await showMenu<String>(
      context: context,
      items: widget.menuItems ?? [],
      position: RelativeRect.fromSize(event.position & Size(48.0, 48.0), overlay.size),
    );
    widget.onMenuItemTapped!(menuItem);
  }
}

The class for standard splash effect touches

import 'package:flutter/material.dart';

class SplashEffect extends StatelessWidget {
  final Widget child;
  final Function() onTap;
  final Function()? onLongPress;
  final BorderRadius? borderRadius;
  final bool isDisabled;

  const SplashEffect({
    Key? key,
    required this.child,
    required this.onTap,
    this.isDisabled = false,
    this.onLongPress,
    this.borderRadius = const BorderRadius.all(Radius.circular(6)),
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (isDisabled) {
      return child;
    }
    return Material(
      type: MaterialType.transparency,
      child: InkWell(
        borderRadius: borderRadius,
        child: child,
        onTap: onTap,
        onLongPress: onLongPress,
      ),
    );
  }
}

And how to use it:

 return CrossPlatformClick(
      onNormalTap: onTapped,
      menuItems: [
        PopupMenuItem(child: Text('Copy Name', style: TextStyle(fontSize: 16)), value: "copied"),
      ],
      onMenuItemTapped: (item) {
        print("item tapped: " + (item ?? "-no-item"));
      },
      child: 
Oliver Dixon
  • 7,012
  • 5
  • 61
  • 95