Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

- [平台支持](#%E5%B9%B3%E5%8F%B0%E6%94%AF%E6%8C%81)
- [截图](#%E6%88%AA%E5%9B%BE)
- [菜单项图标(macOS)](#%E8%8F%9C%E5%8D%95%E9%A1%B9%E5%9B%BE%E6%A0%87macos)
- [菜单项快捷键(macOS)](#%E8%8F%9C%E5%8D%95%E9%A1%B9%E5%BF%AB%E6%8D%B7%E9%94%AEmacos)
- [已知问题](#%E5%B7%B2%E7%9F%A5%E9%97%AE%E9%A2%98)
- [与 app_links 不兼容](#%E4%B8%8E-app_links-%E4%B8%8D%E5%85%BC%E5%AE%B9)
- [在 GNOME 中不显示](#%E5%9C%A8-gnome-%E4%B8%AD%E4%B8%8D%E6%98%BE%E7%A4%BA)
Expand All @@ -34,6 +36,8 @@
- [谁在用使用它?](#%E8%B0%81%E5%9C%A8%E7%94%A8%E4%BD%BF%E7%94%A8%E5%AE%83)
- [API](#api)
- [TrayManager](#traymanager)
- [菜单项图标(macOS)](#%E8%8F%9C%E5%8D%95%E9%A1%B9%E5%9B%BE%E6%A0%87macos-1)
- [菜单项快捷键(macOS)](#%E8%8F%9C%E5%8D%95%E9%A1%B9%E5%BF%AB%E6%8D%B7%E9%94%AEmacos-1)
- [许可证](#%E8%AE%B8%E5%8F%AF%E8%AF%81)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand All @@ -50,6 +54,36 @@
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos.png?raw=true) | ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/linux.png?raw=true) | ![image](https://github.com/leanflutter/tray_manager/blob/main/screenshots/windows.png?raw=true) |

### 菜单项图标(macOS)

通过 `MenuItem.icon` 渲染的菜单项图标:

![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon.png?raw=true)
![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon1.png?raw=true)

### 菜单项快捷键(macOS)

使用 `TrayMenuItem.shortcut` 渲染右侧对齐(浅灰色)的快捷键列:

```dart
import 'package:tray_manager/tray_manager.dart';

final menu = Menu(
items: [
TrayMenuItem(
key: 'assistant',
label: 'Assistant',
shortcut: '⌘⇧Space',
icon: 'images/tray_icon.png',
),
],
);

await trayManager.setContextMenu(menu);
```

![MenuItem shortcut on macOS](./screenshots/shortcut.png)

## 已知问题

### 与 app_links 不兼容
Expand Down Expand Up @@ -218,6 +252,51 @@ class _HomePageState extends State<HomePage> with TrayListener {
| popUpContextMenu | 弹出托盘图标的上下文菜单。 | ➖ | ✔️ | ✔️ |
| getBounds | 返回 `Rect` 这个托盘图标的边界。 | ➖ | ✔️ | ✔️ |

## 菜单项图标(macOS)

`MenuItem`(来自 `menu_base`)带有 `icon` 字段。在 **macOS** 上,如果你传入 Flutter assets 路径,就可以显示每个菜单项的图标:

```dart
Menu menu = Menu(
items: [
MenuItem(
key: 'show_window',
label: 'Show Window',
icon: 'images/tray_icon.png',
),
],
);
await trayManager.setContextMenu(menu);
```

说明:

- 插件会优先从 Flutter assets 加载图标,并以 base64 形式传给原生侧。
- 如果 assets 加载失败,会回退把 `icon` 当作绝对文件路径来尝试加载(best-effort)。
- 其他平台目前可能会忽略该字段。

## 菜单项快捷键(macOS)

`TrayMenuItem` 提供了 `shortcut` 字段。在 **macOS** 上该字段会被转换为原生 `NSMenuItem.keyEquivalent` 进行渲染,因此会显示为**右侧对齐、浅灰色**的系统快捷键列:

```dart
Menu menu = Menu(
items: [
TrayMenuItem(
key: 'assistant',
label: 'Assistant',
shortcut: '⌘⇧Space',
),
],
);
await trayManager.setContextMenu(menu);
```

说明:

- macOS 会按系统规则标准化显示修饰键顺序(例如 `⇧⌘ Space`),不保证与输入字符串顺序一致。
- 其他平台目前可能会忽略该字段。

## 许可证

[MIT](./LICENSE)
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
> **⚠️ Migration Notice**: This plugin is being migrated to [libnativeapi/nativeapi-flutter](https://github.com/libnativeapi/nativeapi-flutter)
>
> The new version is based on a unified C++ core library ([libnativeapi/nativeapi](https://github.com/libnativeapi/nativeapi)), providing more complete and consistent cross-platform native API support.
r

[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url] ![][visits-count-image]

Expand All @@ -22,6 +21,8 @@ English | [简体中文](./README-ZH.md)

- [Platform Support](#platform-support)
- [Screenshots](#screenshots)
- [MenuItem icon (macOS)](#menuitem-icon-macos)
- [MenuItem shortcut (macOS)](#menuitem-shortcut-macos)
- [Known Issues](#known-issues)
- [Not Working with app_links](#not-working-with-app_links)
- [Not Showing in GNOME](#not-showing-in-gnome)
Expand All @@ -33,6 +34,8 @@ English | [简体中文](./README-ZH.md)
- [Who's using it?](#whos-using-it)
- [API](#api)
- [TrayManager](#traymanager)
- [MenuItem icon (macOS)](#menuitem-icon-macos-1)
- [MenuItem shortcut (macOS)](#menuitem-shortcut-macos-1)
- [License](#license)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand All @@ -49,6 +52,36 @@ English | [简体中文](./README-ZH.md)
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos.png?raw=true) | ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/linux.png?raw=true) | ![image](https://github.com/leanflutter/tray_manager/blob/main/screenshots/windows.png?raw=true) |

### MenuItem icon (macOS)

Menu item icons rendered via `MenuItem.icon`:

![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon.png?raw=true)
![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon1.png?raw=true)

### MenuItem shortcut (macOS)

Render a right-aligned (light gray) shortcut column using `TrayMenuItem.shortcut`:

```dart
import 'package:tray_manager/tray_manager.dart';

final menu = Menu(
items: [
TrayMenuItem(
key: 'assistant',
label: 'Assistant',
shortcut: '⌘⇧Space',
icon: 'images/tray_icon.png',
),
],
);

await trayManager.setContextMenu(menu);
```

![MenuItem shortcut on macOS](./screenshots/shortcut.png)

## Known Issues

### Not Working with app_links
Expand Down Expand Up @@ -124,11 +157,13 @@ Menu menu = Menu(
MenuItem(
key: 'show_window',
label: 'Show Window',
icon: 'images/tray_icon.png',
),
MenuItem.separator(),
MenuItem(
key: 'exit_app',
label: 'Exit App',
icon: 'images/tray_icon.png',
),
],
);
Expand Down Expand Up @@ -217,6 +252,50 @@ class _HomePageState extends State<HomePage> with TrayListener {
| popUpContextMenu | Pops up the context menu of the tray icon. | ➖ | ✔️ | ✔️ |
| getBounds | Returns `Rect` The bounds of this tray icon. | ➖ | ✔️ | ✔️ |

## MenuItem icon (macOS)

`MenuItem` has an `icon` field (from `menu_base`). On **macOS**, per-menu-item icons are supported when you provide an asset path:

```dart
Menu menu = Menu(
items: [
MenuItem(
key: 'show_window',
label: 'Show Window',
icon: 'images/tray_icon.png',
),
],
);
await trayManager.setContextMenu(menu);
```

Notes:

- The plugin will try to load the icon from Flutter assets and pass it to native code as base64.
- If the icon asset can't be loaded, it will fall back to treating `icon` as an absolute file path (best-effort).
- Other platforms may ignore this field for now.

## MenuItem shortcut (macOS)

`TrayMenuItem` provides a `shortcut` field. On **macOS** this is converted into native `NSMenuItem.keyEquivalent` rendering, so it appears as a **right-aligned, light gray** shortcut column:

```dart
final menu = Menu(
items: [
TrayMenuItem(
key: 'assistant',
label: 'Assistant',
shortcut: '⌘⇧Space',
),
],
);
await trayManager.setContextMenu(menu);
```

Notes:
- macOS will standardize modifier display order (e.g. `⇧⌘ Space`), so the visual order may differ from the input string.
- Other platforms may ignore this field for now.

## License

[MIT](./LICENSE)
15 changes: 15 additions & 0 deletions packages/tray_manager/example/lib/pages/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,30 +150,44 @@ class _HomePageState extends State<HomePage> with TrayListener {
onTap: () async {
_menu ??= Menu(
items: [
if (Platform.isMacOS)
TrayMenuItem(
label: 'Shortcut',
// macOS only: renders as a right-aligned, light-gray shortcut column.
shortcut: '⌘⇧Space',
icon: 'images/tray_icon.png',
),
MenuItem(
label: 'Look Up "LeanFlutter"',
icon: 'images/tray_icon.png',
),
MenuItem(
label: 'Search with Google',
icon: 'images/tray_icon.png',
),
MenuItem.separator(),
MenuItem(
label: 'Cut',
icon: 'images/tray_icon.png',
),
MenuItem(
label: 'Copy',
icon: 'images/tray_icon.png',
),
MenuItem(
label: 'Paste',
disabled: true,
icon: 'images/tray_icon.png',
),
MenuItem.submenu(
label: 'Share',
icon: 'images/tray_icon.png',
submenu: Menu(
items: [
MenuItem.checkbox(
label: 'Item 1',
checked: true,
icon: 'images/tray_icon.png',
onClick: (menuItem) {
if (kDebugMode) {
print('click item 1');
Expand All @@ -184,6 +198,7 @@ class _HomePageState extends State<HomePage> with TrayListener {
MenuItem.checkbox(
label: 'Item 2',
checked: false,
icon: 'images/tray_icon.png',
onClick: (menuItem) {
if (kDebugMode) {
print('click item 2');
Expand Down
51 changes: 50 additions & 1 deletion packages/tray_manager/lib/src/tray_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,60 @@ class TrayManager {
Future<void> setContextMenu(Menu menu) async {
_menu = menu;
final Map<String, dynamic> arguments = {
'menu': menu.toJson(),
'menu': await _menuToJsonForPlatform(menu),
};
await _channel.invokeMethod('setContextMenu', arguments);
}

Future<Map<String, dynamic>> _menuToJsonForPlatform(Menu menu) async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return menu.toJson();
}
return _menuToJsonWithBase64Icons(menu, <String, String>{});
}

Future<Map<String, dynamic>> _menuToJsonWithBase64Icons(
Menu menu,
Map<String, String> iconBase64Cache,
) async {
final items = menu.items ?? const <MenuItem>[];
final jsonItems = <Map<String, dynamic>>[];

for (final item in items) {
final m = Map<String, dynamic>.from(item.toJson());

final iconPath = item.icon;
if (iconPath != null && iconPath.isNotEmpty) {
final cached = iconBase64Cache[iconPath];
if (cached != null) {
m['base64Icon'] = cached;
} else {
try {
final data = await rootBundle.load(iconPath);
final base64Icon = base64Encode(data.buffer.asUint8List());
iconBase64Cache[iconPath] = base64Icon;
m['base64Icon'] = base64Icon;
} catch (_) {
// Best-effort: if icon isn't a bundled asset or can't be loaded,
// leave it as-is (platform side may still support file paths).
}
}
}

final submenu = item.submenu;
if (submenu != null) {
m['submenu'] =
await _menuToJsonWithBase64Icons(submenu, iconBase64Cache);
}

jsonItems.add(m);
}

return <String, dynamic>{
'items': jsonItems,
}..removeWhere((key, value) => value == null);
}

/// Pops up the context menu of the tray icon.
///
/// [bringAppToFront] If true, the app will be brought to the front when the
Expand Down
37 changes: 37 additions & 0 deletions packages/tray_manager/lib/src/tray_menu_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:menu_base/menu_base.dart';

/// A tray menu item with an explicit shortcut/hotkey hint.
///
/// On macOS this is rendered using NSMenuItem.keyEquivalent so it appears
/// right-aligned and in the system default (light gray) style.
///
/// Other platforms may ignore this field.
class TrayMenuItem extends MenuItem {
/// Shortcut hint string (macOS style), e.g. "⌘⇧Space" or "⌘Q".
final String? shortcut;

TrayMenuItem({
super.key,
super.type = 'normal',
super.label,
super.toolTip,
super.icon,
super.checked,
super.disabled = false,
super.submenu,
super.onClick,
super.onHighlight,
super.onLoseHighlight,
this.shortcut,
});

@override
Map<String, dynamic> toJson() {
final m = super.toJson();
final s = shortcut;
if (s != null && s.isNotEmpty) {
m['shortcut'] = s;
}
return m;
}
}
1 change: 1 addition & 0 deletions packages/tray_manager/lib/tray_manager.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'package:menu_base/menu_base.dart';

export 'src/tray_listener.dart';
export 'src/tray_menu_item.dart';
export 'src/tray_manager.dart';
Loading