Skip to main content

xue_hua_navite_system_tray 1.0.9 copy "xue_hua_navite_system_tray: ^1.0.9" to clipboard
xue_hua_navite_system_tray: ^1.0.9 copied to clipboard

A full-featured native desktop system tray plugin for Flutter across macOS, Windows, and Linux.

example/lib/main.dart

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:material_ui/material_ui.dart';
import 'package:xue_hua_navite_system_tray/xue_hua_navite_system_tray.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const SystemTrayExampleApp());
}

/// Comprehensive desktop example application demonstrating all features of
/// `xue_hua_navite_system_tray` across macOS, Windows, and Linux.
class SystemTrayExampleApp extends StatelessWidget {
  const SystemTrayExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'XueHua Native System Tray Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF2563EB),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF3B82F6),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const SystemTrayDashboardPage(),
    );
  }
}

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

  @override
  State<SystemTrayDashboardPage> createState() =>
      _SystemTrayDashboardPageState();
}

class _SystemTrayDashboardPageState extends State<SystemTrayDashboardPage> {
  final XueHuaNaviteSystemTray _tray = XueHuaNaviteSystemTray.instance;
  StreamSubscription<TrayEvent>? _eventSubscription;
  Timer? _liveMonitorTimer;

  TrayCapabilities? _capabilities;
  TrayBounds? _lastBounds;
  bool _isTrayInitialized = false;
  bool _isTrayVisible = true;
  bool _isDockVisible = true;
  bool _autoSyncEnabled = true;
  bool _liveMetricsEnabled = false;
  String _selectedMode = 'balanced';
  int _syncCounter = 0;

  TrayMenuTrigger _menuTrigger = TrayMenuTrigger.rightClick;
  TrayImagePosition _imagePosition = TrayImagePosition.leading;

  final TextEditingController _titleController = TextEditingController(
    text: 'XueHua',
  );
  final TextEditingController _toolTipController = TextEditingController(
    text: 'XueHua Native System Tray • Ready',
  );
  final TextEditingController _notifTitleController = TextEditingController(
    text: 'XueHua System Tray',
  );
  final TextEditingController _notifBodyController = TextEditingController(
    text: 'Background workspace synchronization finished.',
  );
  TrayNotificationIconType _notifIconType = TrayNotificationIconType.info;

  final List<String> _eventLogs = <String>[];

  @override
  void initState() {
    super.initState();
    _eventSubscription = _tray.events.listen(_onTrayEvent);
    _initializeTrayAndCapabilities();
  }

  @override
  void dispose() {
    _liveMonitorTimer?.cancel();
    _eventSubscription?.cancel();
    _titleController.dispose();
    _toolTipController.dispose();
    _notifTitleController.dispose();
    _notifBodyController.dispose();
    super.dispose();
  }

  void _appendLog(String message) {
    if (!mounted) return;
    final String time = DateTime.now().toIso8601String().substring(11, 19);
    setState(() {
      _eventLogs.insert(0, '[$time] $message');
      if (_eventLogs.length > 80) {
        _eventLogs.removeLast();
      }
    });
  }

  void _onTrayEvent(TrayEvent event) {
    _appendLog(
      'Event: ${event.type.name} | button=${event.mouseButton.name} '
      '${event.menuItemId != null ? "| item=${event.menuItemId}" : ""} '
      '${event.checked != null ? "| checked=${event.checked}" : ""}',
    );
    if (event.bounds != null && mounted) {
      setState(() {
        _lastBounds = event.bounds;
      });
    }

    // Automatically toggle or restore window on left double-click.
    if (event.type == TrayEventType.leftDoubleClick) {
      _tray.showMainWindow(focus: true);
    }
  }

  TrayIconSource _defaultPlatformIcon() {
    if (Platform.isMacOS) {
      return const TrayIconSource.system('sparkles', isTemplate: true);
    } else if (Platform.isWindows) {
      return const TrayIconSource.system('application');
    } else {
      return const TrayIconSource.system('applications-system');
    }
  }

  List<TrayMenuItem> _buildDefaultContextMenu() {
    return <TrayMenuItem>[
      const TrayMenuItem.header(
        id: 'header_workspace',
        label: 'XueHua Workspace',
      ),
      TrayMenuItem.action(
        id: 'show_window',
        label: 'Show Main Window',
        toolTip: 'Bring the main application window to front',
        icon: Platform.isMacOS
            ? const TrayIconSource.system('macwindow', isTemplate: true)
            : Platform.isWindows
            ? const TrayIconSource.system('application')
            : null,
        shortcut: const TrayMenuShortcut('o', meta: true),
        onTap: () async {
          await _tray.showMainWindow(focus: true);
          _appendLog('Action callback: Show Main Window');
        },
      ),
      TrayMenuItem.action(
        id: 'hide_window',
        label: 'Minimize to System Tray',
        icon: Platform.isMacOS
            ? const TrayIconSource.system('eye.slash', isTemplate: true)
            : null,
        shortcut: const TrayMenuShortcut('h', meta: true),
        onTap: () async {
          await _tray.hideMainWindow();
          _appendLog('Action callback: Minimize to System Tray');
        },
      ),
      const TrayMenuItem.separator(),
      TrayMenuItem.action(
        id: 'asset_star_favorite',
        label: 'Starred Favorites (Asset Icon)',
        toolTip: 'Menu item using local Flutter asset image menu_star.png',
        icon: const TrayIconSource.asset(
          'assets/icons/menu_star.png',
          isTemplate: false,
          iconSize: 16.0,
        ),
        shortcut: const TrayMenuShortcut('f', meta: true),
        onTap: () {
          _appendLog('Clicked local asset menu item: Starred Favorites');
        },
      ),
      TrayMenuItem.action(
        id: 'asset_open_folder',
        label: 'Open Workspace Folder (Asset Icon)',
        toolTip: 'Menu item using local Flutter asset image menu_folder.png',
        icon: const TrayIconSource.asset(
          'assets/icons/menu_folder.png',
          isTemplate: false,
          iconSize: 16.0,
        ),
        shortcut: const TrayMenuShortcut('d', meta: true),
        onTap: () {
          _appendLog('Clicked local asset menu item: Open Workspace Folder');
        },
      ),
      const TrayMenuItem.separator(),
      TrayMenuItem.action(
        id: 'sync_now',
        label: 'Sync Now ($_syncCounter)',
        icon: Platform.isMacOS
            ? const TrayIconSource.system('arrow.clockwise', isTemplate: true)
            : Platform.isWindows
            ? const TrayIconSource.system('info')
            : null,
        shortcut: const TrayMenuShortcut('r', meta: true),
        onTap: _triggerManualSync,
      ),
      TrayMenuItem.checkbox(
        id: 'auto_sync',
        label: 'Auto-Sync in Background',
        checked: _autoSyncEnabled,
        onChanged: (bool checked) {
          setState(() {
            _autoSyncEnabled = checked;
          });
          _appendLog('Checkbox callback: Auto-Sync = $checked');
        },
      ),
      TrayMenuItem.submenu(
        id: 'submenu_mode',
        label: 'Performance Profile',
        icon: Platform.isMacOS
            ? const TrayIconSource.system('speedometer', isTemplate: true)
            : null,
        children: <TrayMenuItem>[
          TrayMenuItem.radio(
            id: 'mode_eco',
            groupId: 'perf_mode',
            label: 'Eco Mode (Low Power)',
            checked: _selectedMode == 'eco',
            onTap: () => _selectPerformanceMode('eco'),
          ),
          TrayMenuItem.radio(
            id: 'mode_balanced',
            groupId: 'perf_mode',
            label: 'Balanced Mode',
            checked: _selectedMode == 'balanced',
            onTap: () => _selectPerformanceMode('balanced'),
          ),
          TrayMenuItem.radio(
            id: 'mode_turbo',
            groupId: 'perf_mode',
            label: 'Turbo Mode (Max Throughput)',
            checked: _selectedMode == 'turbo',
            onTap: () => _selectPerformanceMode('turbo'),
          ),
        ],
      ),
      const TrayMenuItem.separator(),
      TrayMenuItem.action(
        id: 'send_notification',
        label: 'Send Test Notification',
        icon: Platform.isMacOS
            ? const TrayIconSource.system('bell', isTemplate: true)
            : Platform.isWindows
            ? const TrayIconSource.system('warning')
            : null,
        onTap: _sendTrayNotification,
      ),
      const TrayMenuItem.separator(),
      TrayMenuItem.action(
        id: 'destroy_tray',
        label: 'Remove System Tray Icon',
        icon: Platform.isMacOS
            ? const TrayIconSource.system('trash', isTemplate: true)
            : Platform.isWindows
            ? const TrayIconSource.system('error')
            : null,
        shortcut: const TrayMenuShortcut('q', meta: true, shift: true),
        onTap: _destroyTray,
      ),
    ];
  }

  Future<void> _initializeTrayAndCapabilities() async {
    try {
      final TrayCapabilities caps = await _tray.getCapabilities();
      final bool ok = await _tray.initSystemTray(
        TrayConfig(
          icon: _defaultPlatformIcon(),
          title: _titleController.text,
          toolTip: _toolTipController.text,
          visible: _isTrayVisible,
          menuTrigger: _menuTrigger,
          imagePosition: _imagePosition,
          menuItems: _buildDefaultContextMenu(),
        ),
      );
      final TrayBounds? bounds = await _tray.getBounds();

      if (!mounted) return;
      setState(() {
        _capabilities = caps;
        _isTrayInitialized = ok;
        _lastBounds = bounds;
      });
      _appendLog('System tray initialized (backend: ${caps.backendName})');
    } catch (e) {
      _appendLog('Initialization error: $e');
    }
  }

  Future<void> _triggerManualSync() async {
    setState(() {
      _syncCounter++;
    });
    await _tray.updateMenuItem('sync_now', label: 'Sync Now ($_syncCounter)');
    _appendLog('Updated menu item "sync_now" in-place (count: $_syncCounter)');
  }

  void _selectPerformanceMode(String mode) {
    setState(() {
      _selectedMode = mode;
    });
    _appendLog('Selected radio performance profile: $mode');
  }

  /// Generates a crisp 36x36 PNG badge icon in memory using `dart:ui` Canvas
  /// and applies it to the system tray via [TrayIconSource.bytes].
  Future<void> _applyGeneratedBadgeIcon(Color color, String badgeText) async {
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final Canvas canvas = Canvas(recorder);
    const double size = 36.0;

    final Paint bgPaint = Paint()
      ..color = color
      ..isAntiAlias = true;
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        const Rect.fromLTWH(2, 2, size - 4, size - 4),
        const Radius.circular(10),
      ),
      bgPaint,
    );

    final TextPainter tp = TextPainter(
      text: TextSpan(
        text: badgeText,
        style: const TextStyle(
          color: Colors.white,
          fontSize: 16,
          fontWeight: FontWeight.bold,
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    tp.paint(canvas, Offset((size - tp.width) / 2, (size - tp.height) / 2));

    final ui.Image img = await recorder.endRecording().toImage(
      size.toInt(),
      size.toInt(),
    );
    final ByteData? pngBytes = await img.toByteData(
      format: ui.ImageByteFormat.png,
    );
    if (pngBytes != null) {
      final Uint8List uint8List = pngBytes.buffer.asUint8List();
      await _tray.setIcon(
        TrayIconSource.bytes(uint8List, isTemplate: false, iconSize: 18.0),
      );
      _appendLog('Applied in-memory PNG icon ($badgeText)');
    }
  }

  Future<void> _toggleLiveMetrics(bool enabled) async {
    _liveMonitorTimer?.cancel();
    setState(() {
      _liveMetricsEnabled = enabled;
    });
    if (enabled) {
      int tick = 0;
      _liveMonitorTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
        tick++;
        final int cpu = 18 + (tick * 7) % 45;
        await _tray.setTitle('CPU $cpu%');
      });
      _appendLog('Started live inline title monitor');
    } else {
      await _tray.setTitle(_titleController.text);
      _appendLog('Stopped live inline title monitor');
    }
  }

  Future<void> _sendTrayNotification() async {
    final bool sent = await _tray.showNotification(
      TrayNotification(
        title: _notifTitleController.text,
        message: _notifBodyController.text,
        iconType: _notifIconType,
      ),
    );
    _appendLog('showNotification result: $sent');
  }

  Future<void> _refreshBounds() async {
    final TrayBounds? bounds = await _tray.getBounds();
    setState(() {
      _lastBounds = bounds;
    });
    _appendLog('Queried tray bounds: $bounds');
  }

  Future<void> _destroyTray() async {
    await _tray.destroy();
    if (!mounted) return;
    setState(() {
      _isTrayInitialized = false;
    });
    _appendLog('System tray destroyed');
  }

  @override
  Widget build(BuildContext context) {
    final ColorScheme colors = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('XueHua Native System Tray Workbench'),
        actions: <Widget>[
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: Chip(
              avatar: Icon(
                _isTrayInitialized ? Icons.check_circle : Icons.cancel,
                size: 18,
                color: _isTrayInitialized ? Colors.green : Colors.grey,
              ),
              label: Text(_isTrayInitialized ? 'Tray Active' : 'Tray Inactive'),
            ),
          ),
        ],
      ),
      body: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Expanded(
            flex: 3,
            child: ListView(
              padding: const EdgeInsets.all(16),
              children: <Widget>[
                _buildCapabilitiesCard(colors),
                const SizedBox(height: 16),
                _buildLifecycleAndAppearanceCard(),
                const SizedBox(height: 16),
                _buildTriggerAndMenuCard(),
                const SizedBox(height: 16),
                _buildWindowAndNotificationCard(),
              ],
            ),
          ),
          const VerticalDivider(width: 1),
          Expanded(flex: 2, child: _buildEventLogPanel(colors)),
        ],
      ),
    );
  }

  Widget _buildCapabilitiesCard(ColorScheme colors) {
    final TrayCapabilities? caps = _capabilities;
    return Card(
      elevation: 0,
      color: colors.surfaceContainerHighest.withValues(alpha: 0.45),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Row(
              children: <Widget>[
                Icon(Icons.layers_outlined, color: colors.primary),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    'Platform Backend & Runtime Capabilities',
                    style: Theme.of(context).textTheme.titleMedium
                        ?.copyWith(fontWeight: FontWeight.bold),
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                TextButton.icon(
                  onPressed: _refreshBounds,
                  icon: const Icon(Icons.my_location, size: 16),
                  label: const Text('Query Bounds'),
                ),
              ],
            ),
            const SizedBox(height: 8),
            if (caps == null)
              const Text('Loading platform capabilities...')
            else
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: <Widget>[
                  _capabilityBadge('OS: ${caps.platform}', true),
                  _capabilityBadge('Backend: ${caps.backendName}', true),
                  _capabilityBadge('Inline Title', caps.supportsInlineTitle),
                  _capabilityBadge('Template Icon', caps.supportsTemplateIcon),
                  _capabilityBadge('System Symbol', caps.supportsSystemSymbol),
                  _capabilityBadge('Icon Bounds', caps.supportsIconBounds),
                  _capabilityBadge('Double Click', caps.supportsDoubleClick),
                  _capabilityBadge('Middle Click', caps.supportsMiddleClick),
                  _capabilityBadge('Menu Icons', caps.supportsMenuIcons),
                  _capabilityBadge(
                    'Dock Toggle',
                    caps.supportsDockVisibilityToggle,
                  ),
                  _capabilityBadge(
                    'Notifications',
                    caps.supportsTrayNotification,
                  ),
                ],
              ),
            if (_lastBounds != null) ...<Widget>[
              const SizedBox(height: 10),
              Text(
                'Tray Icon Screen Bounds: $_lastBounds',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _capabilityBadge(String label, bool supported) {
    return Chip(
      visualDensity: VisualDensity.compact,
      avatar: Icon(supported ? Icons.check : Icons.info_outline, size: 15),
      label: Text(label, style: const TextStyle(fontSize: 12)),
    );
  }

  Widget _buildLifecycleAndAppearanceCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              '1. Lifecycle, Icons, Inline Title & Tooltip',
              style: Theme.of(context).textTheme.titleMedium
                  ?.copyWith(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                FilledButton.icon(
                  key: const ValueKey('btn_init_tray'),
                  onPressed: _initializeTrayAndCapabilities,
                  icon: const Icon(Icons.play_arrow),
                  label: const Text('Init / Reset Tray'),
                ),
                OutlinedButton.icon(
                  key: const ValueKey('btn_toggle_tray_visible'),
                  onPressed: () async {
                    final bool next = !_isTrayVisible;
                    await _tray.setVisible(next);
                    setState(() => _isTrayVisible = next);
                    _appendLog('setVisible($next)');
                  },
                  icon: Icon(
                    _isTrayVisible ? Icons.visibility_off : Icons.visibility,
                  ),
                  label: Text(_isTrayVisible ? 'Hide Tray' : 'Show Tray'),
                ),
                OutlinedButton.icon(
                  key: const ValueKey('btn_destroy_tray'),
                  onPressed: _destroyTray,
                  icon: const Icon(Icons.delete_outline),
                  label: const Text('Destroy Tray'),
                ),
              ],
            ),
            const Divider(height: 24),
            const Text(
              'Switch Icon Source (System Symbol, Local Asset & In-Memory PNG):',
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                ActionChip(
                  avatar: const Icon(Icons.auto_awesome, size: 16),
                  label: const Text('System Default Icon'),
                  onPressed: () async {
                    await _tray.setIcon(_defaultPlatformIcon());
                    _appendLog('Switched to platform system icon');
                  },
                ),
                ActionChip(
                  avatar: const Icon(Icons.image_outlined, size: 16),
                  label: const Text('Local Asset Icon (tray_logo.png)'),
                  onPressed: () async {
                    await _tray.setIcon(
                      const TrayIconSource.asset(
                        'assets/icons/tray_logo.png',
                        isTemplate: false,
                        iconSize: 18.0,
                      ),
                    );
                    _appendLog('Switched to local asset icon: tray_logo.png');
                  },
                ),
                ActionChip(
                  avatar: const Icon(Icons.bolt, size: 16),
                  label: const Text('System Alert / Bolt'),
                  onPressed: () async {
                    final TrayIconSource icon = Platform.isMacOS
                        ? const TrayIconSource.system(
                            'bolt.fill',
                            isTemplate: true,
                          )
                        : Platform.isWindows
                        ? const TrayIconSource.system('warning')
                        : const TrayIconSource.system('dialog-warning');
                    await _tray.setIcon(icon);
                    _appendLog('Switched to warning/bolt system icon');
                  },
                ),
                ActionChip(
                  avatar: const Icon(Icons.looks_one, size: 16),
                  label: const Text('In-Memory Badge (XH)'),
                  onPressed: () =>
                      _applyGeneratedBadgeIcon(const Color(0xFF2563EB), 'XH'),
                ),
                ActionChip(
                  avatar: const Icon(Icons.notifications_active, size: 16),
                  label: const Text('In-Memory Badge (9+)'),
                  onPressed: () =>
                      _applyGeneratedBadgeIcon(const Color(0xFFDC2626), '9+'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Row(
              children: <Widget>[
                Expanded(
                  child: TextField(
                    controller: _titleController,
                    decoration: const InputDecoration(
                      labelText: 'Inline Title (macOS & Linux AppIndicator)',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                FilledButton.tonal(
                  onPressed: () async {
                    await _tray.setTitle(_titleController.text);
                    _appendLog('setTitle("${_titleController.text}")');
                  },
                  child: const Text('Update Title'),
                ),
              ],
            ),
            const SizedBox(height: 8),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Live CPU Metric Ticker in Status Bar'),
              subtitle: const Text(
                'Updates inline status bar text every second',
              ),
              value: _liveMetricsEnabled,
              onChanged: _toggleLiveMetrics,
            ),
            const SizedBox(height: 8),
            Row(
              children: <Widget>[
                Expanded(
                  child: TextField(
                    controller: _toolTipController,
                    decoration: const InputDecoration(
                      labelText: 'Hover Tooltip Text',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                FilledButton.tonal(
                  onPressed: () async {
                    await _tray.setToolTip(_toolTipController.text);
                    _appendLog('setToolTip("${_toolTipController.text}")');
                  },
                  child: const Text('Update Tooltip'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildTriggerAndMenuCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              '2. Context Menu, Click Trigger & Dynamic Item Updates',
              style: Theme.of(context).textTheme.titleMedium
                  ?.copyWith(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            Row(
              children: <Widget>[
                Expanded(
                  child: DropdownButtonFormField<TrayMenuTrigger>(
                    initialValue: _menuTrigger,
                    decoration: const InputDecoration(
                      labelText: 'Menu Click Trigger Mode',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                    items: TrayMenuTrigger.values.map((TrayMenuTrigger t) {
                      return DropdownMenuItem<TrayMenuTrigger>(
                        value: t,
                        child: Text(t.name),
                      );
                    }).toList(),
                    onChanged: (TrayMenuTrigger? next) async {
                      if (next == null) return;
                      setState(() => _menuTrigger = next);
                      await _tray.setMenuTrigger(
                        next,
                        imagePosition: _imagePosition,
                      );
                      _appendLog('setMenuTrigger(${next.name})');
                    },
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: DropdownButtonFormField<TrayImagePosition>(
                    initialValue: _imagePosition,
                    decoration: const InputDecoration(
                      labelText: 'Icon Position (macOS)',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                    items: TrayImagePosition.values.map((TrayImagePosition p) {
                      return DropdownMenuItem<TrayImagePosition>(
                        value: p,
                        child: Text(p.name),
                      );
                    }).toList(),
                    onChanged: (TrayImagePosition? next) async {
                      if (next == null) return;
                      setState(() => _imagePosition = next);
                      await _tray.setMenuTrigger(
                        _menuTrigger,
                        imagePosition: next,
                      );
                      _appendLog('setImagePosition(${next.name})');
                    },
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                FilledButton.tonalIcon(
                  onPressed: () async {
                    await _tray.popUpContextMenu();
                    _appendLog('Called popUpContextMenu()');
                  },
                  icon: const Icon(Icons.menu_open),
                  label: const Text('Pop Up Menu Now'),
                ),
                OutlinedButton.icon(
                  onPressed: _triggerManualSync,
                  icon: const Icon(Icons.sync),
                  label: Text('Increment Sync Item ($_syncCounter)'),
                ),
                OutlinedButton.icon(
                  onPressed: () async {
                    await _tray.setContextMenu(_buildDefaultContextMenu());
                    _appendLog('Rebuilt full context menu tree');
                  },
                  icon: const Icon(Icons.refresh),
                  label: const Text('Rebuild Context Menu'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildWindowAndNotificationCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              '3. Window Control, Dock Visibility & Tray Notifications',
              style: Theme.of(context).textTheme.titleMedium
                  ?.copyWith(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                OutlinedButton.icon(
                  key: const ValueKey('btn_minimize_window'),
                  onPressed: () async {
                    await _tray.hideMainWindow();
                    _appendLog(
                      'Hid main window (use Tray Menu -> Show Main Window to restore)',
                    );
                  },
                  icon: const Icon(Icons.minimize),
                  label: const Text('Minimize Window to Tray'),
                ),
                OutlinedButton.icon(
                  key: const ValueKey('btn_show_window'),
                  onPressed: () async {
                    await _tray.showMainWindow(focus: true);
                    _appendLog('Restored and focused main window');
                  },
                  icon: const Icon(Icons.open_in_browser),
                  label: const Text('Show & Focus Main Window'),
                ),
                if (Platform.isMacOS)
                  FilterChip(
                    selected: _isDockVisible,
                    label: const Text('macOS Dock Icon Visible'),
                    onSelected: (bool value) async {
                      await _tray.setDockIconVisible(value);
                      setState(() => _isDockVisible = value);
                      _appendLog('setDockIconVisible($value)');
                    },
                  ),
              ],
            ),
            const Divider(height: 24),
            Row(
              children: <Widget>[
                Expanded(
                  child: TextField(
                    controller: _notifTitleController,
                    decoration: const InputDecoration(
                      labelText: 'Notification Title',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                DropdownButton<TrayNotificationIconType>(
                  value: _notifIconType,
                  items: TrayNotificationIconType.values.map((
                    TrayNotificationIconType type,
                  ) {
                    return DropdownMenuItem<TrayNotificationIconType>(
                      value: type,
                      child: Text(type.name),
                    );
                  }).toList(),
                  onChanged: (TrayNotificationIconType? val) {
                    if (val != null) {
                      setState(() => _notifIconType = val);
                    }
                  },
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: <Widget>[
                Expanded(
                  child: TextField(
                    controller: _notifBodyController,
                    decoration: const InputDecoration(
                      labelText: 'Notification Message',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                FilledButton.icon(
                  onPressed: _sendTrayNotification,
                  icon: const Icon(Icons.notifications_outlined),
                  label: const Text('Notify'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildEventLogPanel(ColorScheme colors) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          color: colors.surfaceContainerHighest.withValues(alpha: 0.35),
          child: Row(
            children: <Widget>[
              const Icon(Icons.terminal, size: 18),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  'Live Tray Event Stream (${_eventLogs.length})',
                  style: const TextStyle(fontWeight: FontWeight.bold),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              TextButton(
                onPressed: () => setState(_eventLogs.clear),
                child: const Text('Clear'),
              ),
            ],
          ),
        ),
        const Divider(height: 1),
        Expanded(
          child: _eventLogs.isEmpty
              ? const Center(
                  child: Text(
                    'Click the system tray icon or select menu items\nto inspect real-time native events here.',
                    textAlign: TextAlign.center,
                  ),
                )
              : ListView.separated(
                  padding: const EdgeInsets.all(12),
                  itemCount: _eventLogs.length,
                  separatorBuilder: (_, _) => const Divider(height: 12),
                  itemBuilder: (BuildContext context, int index) {
                    return Text(
                      _eventLogs[index],
                      style: const TextStyle(
                        fontFamily: 'monospace',
                        fontSize: 12.5,
                      ),
                    );
                  },
                ),
        ),
      ],
    );
  }
}
0
likes
160
points
378
downloads

Documentation

API reference

Publisher

verified publisherjsontodart.cn

Weekly Downloads

A full-featured native desktop system tray plugin for Flutter across macOS, Windows, and Linux.

Repository (GitHub)
View/report issues

Topics

#system-tray #status-bar #menu-bar #desktop #tray

License

Apache-2.0 (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on xue_hua_navite_system_tray

Packages that implement xue_hua_navite_system_tray