Can I change right click action in flutter web application?

Viewed 7276

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

4 Answers

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.

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());
  // ...
}

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: 
Related