flutter_preview_context_menu 0.4.0 copy "flutter_preview_context_menu: ^0.4.0" to clipboard
flutter_preview_context_menu: ^0.4.0 copied to clipboard

A flexible WhatsApp-style context menu for Flutter with custom previews, top content, actions, animation, and viewport-aware overflow handling.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_preview_context_menu/flutter_preview_context_menu.dart';

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

class _ExampleApp extends StatelessWidget {
  const _ExampleApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF087E8B),
          brightness: Brightness.light,
        ),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF40C9A2),
          brightness: Brightness.dark,
        ),
      ),
      home: const _ConversationExamplePage(),
    );
  }
}

class _ConversationExamplePage extends StatelessWidget {
  const _ConversationExamplePage();

  static const _messages = [
    _Message(
      text: 'The context menu now works with touch, mouse, and trackpad.',
      time: '10:24',
      isMine: false,
    ),
    _Message(
      text: 'The preview can stay aligned with bubbles of different widths.',
      time: '10:25',
      isMine: true,
    ),
    _Message(
      text: 'Actions and top content remain fully customizable.',
      time: '10:26',
      isMine: false,
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Preview context menu')),
      body: ListView.separated(
        padding: EdgeInsets.fromLTRB(
          16,
          24,
          16,
          32 + MediaQuery.paddingOf(context).bottom,
        ),
        itemCount: _messages.length,
        separatorBuilder: (_, _) => const SizedBox(height: 14),
        itemBuilder: (context, index) {
          final message = _messages[index];
          return Align(
            alignment: message.isMine
                ? Alignment.centerRight
                : Alignment.centerLeft,
            child: ContextMenuRegion(
              topContent: const _ReactionBar(),
              preview: _MessageBubble(message: message, isPreview: true),
              actions: [
                ContextMenuActionData(
                  label: 'Reply',
                  description: 'Quote this message in your response',
                  icon: const Icon(Icons.reply_rounded),
                  onPressed: () => _showResult(context, 'Reply'),
                ),
                ContextMenuActionData(
                  label: 'Copy',
                  description: 'Copy the complete message text',
                  icon: const Icon(Icons.content_copy_rounded),
                  onPressed: () async {
                    await Clipboard.setData(ClipboardData(text: message.text));
                    if (context.mounted) _showResult(context, 'Copied');
                  },
                ),
                ContextMenuActionData(
                  label: 'Forward',
                  description: 'Choose another conversation',
                  icon: const Icon(Icons.forward_rounded),
                  onPressed: () => _showResult(context, 'Forward'),
                ),
                ContextMenuActionData(
                  label: 'Select',
                  description: 'Include this message in a selection',
                  icon: const Icon(Icons.check_circle_outline_rounded),
                  onPressed: () => _showResult(context, 'Select'),
                ),
                ContextMenuActionData(
                  label: 'Info',
                  description: 'View delivery details and message time',
                  icon: const Icon(Icons.info_outline_rounded),
                  onPressed: () => _showResult(context, 'Info'),
                ),
                if (message.isMine)
                  ContextMenuActionData(
                    label: 'Delete',
                    icon: const Icon(Icons.delete_outline_rounded),
                    destructive: true,
                    separatedBefore: true,
                    onPressed: () => _showResult(context, 'Delete'),
                  ),
              ],
              child: _MessageBubble(message: message),
            ),
          );
        },
      ),
    );
  }

  void _showResult(BuildContext context, String action) {
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(action)));
  }
}

class _MessageBubble extends StatelessWidget {
  const _MessageBubble({required this.message, this.isPreview = false});

  final _Message message;
  final bool isPreview;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    final background = message.isMine
        ? colors.primaryContainer
        : colors.surfaceContainerHigh;
    final foreground = message.isMine
        ? colors.onPrimaryContainer
        : colors.onSurface;

    return ConstrainedBox(
      constraints: BoxConstraints(maxWidth: isPreview ? 300 : 340),
      child: DecoratedBox(
        decoration: BoxDecoration(
          color: background,
          borderRadius: BorderRadius.circular(18),
          border: isPreview ? Border.all(color: colors.outlineVariant) : null,
        ),
        child: Padding(
          padding: const EdgeInsets.fromLTRB(14, 10, 12, 8),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(message.text, style: TextStyle(color: foreground)),
              const SizedBox(height: 4),
              Text(
                message.time,
                style: Theme.of(context).textTheme.labelSmall?.copyWith(
                  color: foreground.withValues(alpha: 0.68),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _ReactionBar extends StatelessWidget {
  const _ReactionBar();

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    return Material(
      color: colors.surfaceContainerHighest,
      shape: const StadiumBorder(),
      child: const Padding(
        padding: EdgeInsets.symmetric(horizontal: 14, vertical: 9),
        child: Text('👍   ❤️   😂   😮   😢'),
      ),
    );
  }
}

class _Message {
  const _Message({
    required this.text,
    required this.time,
    required this.isMine,
  });

  final String text;
  final String time;
  final bool isMine;
}
1
likes
160
points
410
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A flexible WhatsApp-style context menu for Flutter with custom previews, top content, actions, animation, and viewport-aware overflow handling.

Homepage
Repository (GitHub)
View/report issues

Topics

#context-menu #long-press #overlay #desktop #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_preview_context_menu