Like Google Drive, can I create custom menu in Flutter Web application?.
Like Google Drive, can I create custom menu in Flutter Web application?.
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.
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.
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: