Skip to content

Commit 9eb075f

Browse files
committed
Add context menu trigger support for tray icons
Introduces the ContextMenuTrigger enum and related logic to control how tray icon context menus are triggered (manual, left click, right click, double click). Updates bindings and UI to allow configuration and propagation of the trigger mode, improving flexibility for different desktop environments.
1 parent 4d3fd41 commit 9eb075f

4 files changed

Lines changed: 270 additions & 1 deletion

File tree

examples/tray_icon_example/lib/main.dart

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class TrayIconData {
1616
bool isVisible;
1717
String title;
1818
String tooltip;
19+
ContextMenuTrigger contextMenuTrigger;
1920
AnimatedIconGenerator? animatedIconGenerator;
2021

2122
TrayIconData({
@@ -28,6 +29,7 @@ class TrayIconData {
2829
this.isVisible = true,
2930
this.title = '',
3031
this.tooltip = '',
32+
this.contextMenuTrigger = ContextMenuTrigger.none,
3133
});
3234

3335
void dispose() {
@@ -149,6 +151,7 @@ class _TrayIconExamplePageState extends State<TrayIconExamplePage> {
149151
// trayIcon.title = trayIconData.title; // Set via UI
150152
// trayIcon.tooltip = trayIconData.tooltip; // Set via UI
151153
trayIcon.isVisible = trayIconData.isVisible;
154+
trayIcon.contextMenuTrigger = trayIconData.contextMenuTrigger;
152155

153156
_trayIcons.add(trayIconData);
154157

@@ -300,6 +303,30 @@ class _TrayIconExamplePageState extends State<TrayIconExamplePage> {
300303
_addToHistory('Tooltip updated for tray icon $id: $tooltip');
301304
}
302305

306+
void _updateTrayIconContextMenuTrigger(int id, ContextMenuTrigger trigger) {
307+
final trayIconData = _trayIcons.firstWhere(
308+
(trayIconData) => trayIconData.id == id,
309+
orElse: () => throw Exception('Tray icon not found'),
310+
);
311+
trayIconData.trayIcon.contextMenuTrigger = trigger;
312+
trayIconData.contextMenuTrigger = trigger;
313+
final triggerName = _getTriggerName(trigger);
314+
_addToHistory('Context menu trigger updated for tray icon $id: $triggerName');
315+
}
316+
317+
String _getTriggerName(ContextMenuTrigger trigger) {
318+
switch (trigger) {
319+
case ContextMenuTrigger.none:
320+
return 'None (Manual)';
321+
case ContextMenuTrigger.clicked:
322+
return 'Left Click';
323+
case ContextMenuTrigger.rightClicked:
324+
return 'Right Click';
325+
case ContextMenuTrigger.doubleClicked:
326+
return 'Double Click';
327+
}
328+
}
329+
303330
void _toggleTrayIconVisibility(int id) {
304331
final trayIconData = _trayIcons.firstWhere(
305332
(trayIconData) => trayIconData.id == id,
@@ -834,6 +861,73 @@ class _TrayIconExamplePageState extends State<TrayIconExamplePage> {
834861
),
835862
const SizedBox(height: 12),
836863

864+
// Context Menu Trigger Section
865+
const Text(
866+
'Context Menu Trigger',
867+
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.grey),
868+
),
869+
const SizedBox(height: 8),
870+
Container(
871+
decoration: BoxDecoration(
872+
border: Border.all(color: Colors.grey.shade400),
873+
borderRadius: BorderRadius.circular(4),
874+
),
875+
padding: const EdgeInsets.symmetric(horizontal: 12),
876+
child: DropdownButton<ContextMenuTrigger>(
877+
value: trayIconData.contextMenuTrigger,
878+
isExpanded: true,
879+
underline: const SizedBox(),
880+
items: [
881+
DropdownMenuItem(
882+
value: ContextMenuTrigger.none,
883+
child: Row(
884+
children: [
885+
const Icon(Icons.touch_app, size: 16),
886+
const SizedBox(width: 8),
887+
const Text('None (Manual)'),
888+
],
889+
),
890+
),
891+
DropdownMenuItem(
892+
value: ContextMenuTrigger.clicked,
893+
child: Row(
894+
children: [
895+
const Icon(Icons.mouse, size: 16),
896+
const SizedBox(width: 8),
897+
const Text('Left Click'),
898+
],
899+
),
900+
),
901+
DropdownMenuItem(
902+
value: ContextMenuTrigger.rightClicked,
903+
child: Row(
904+
children: [
905+
const Icon(Icons.mouse_outlined, size: 16),
906+
const SizedBox(width: 8),
907+
const Text('Right Click'),
908+
],
909+
),
910+
),
911+
DropdownMenuItem(
912+
value: ContextMenuTrigger.doubleClicked,
913+
child: Row(
914+
children: [
915+
const Icon(Icons.double_arrow, size: 16),
916+
const SizedBox(width: 8),
917+
const Text('Double Click'),
918+
],
919+
),
920+
),
921+
],
922+
onChanged: (value) {
923+
if (value != null) {
924+
_updateTrayIconContextMenuTrigger(trayIconData.id, value);
925+
}
926+
},
927+
),
928+
),
929+
const SizedBox(height: 12),
930+
837931
// Icon Management Section
838932
const Text(
839933
'Icon Management',

packages/cnativeapi/lib/src/bindings_generated.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3128,6 +3128,45 @@ class CNativeApiBindings {
31283128
_native_tray_icon_get_context_menuPtr
31293129
.asFunction<native_menu_t Function(native_tray_icon_t)>();
31303130

3131+
/// Set the context menu trigger behavior
3132+
/// @param tray_icon The tray icon
3133+
/// @param trigger The desired trigger behavior
3134+
void native_tray_icon_set_context_menu_trigger(
3135+
native_tray_icon_t tray_icon,
3136+
native_context_menu_trigger_t trigger,
3137+
) {
3138+
return _native_tray_icon_set_context_menu_trigger(tray_icon, trigger.value);
3139+
}
3140+
3141+
late final _native_tray_icon_set_context_menu_triggerPtr =
3142+
_lookup<
3143+
ffi.NativeFunction<
3144+
ffi.Void Function(native_tray_icon_t, ffi.UnsignedInt)
3145+
>
3146+
>('native_tray_icon_set_context_menu_trigger');
3147+
late final _native_tray_icon_set_context_menu_trigger =
3148+
_native_tray_icon_set_context_menu_triggerPtr
3149+
.asFunction<void Function(native_tray_icon_t, int)>();
3150+
3151+
/// Get the current context menu trigger behavior
3152+
/// @param tray_icon The tray icon
3153+
/// @return The current trigger behavior
3154+
native_context_menu_trigger_t native_tray_icon_get_context_menu_trigger(
3155+
native_tray_icon_t tray_icon,
3156+
) {
3157+
return native_context_menu_trigger_t.fromValue(
3158+
_native_tray_icon_get_context_menu_trigger(tray_icon),
3159+
);
3160+
}
3161+
3162+
late final _native_tray_icon_get_context_menu_triggerPtr =
3163+
_lookup<ffi.NativeFunction<ffi.UnsignedInt Function(native_tray_icon_t)>>(
3164+
'native_tray_icon_get_context_menu_trigger',
3165+
);
3166+
late final _native_tray_icon_get_context_menu_trigger =
3167+
_native_tray_icon_get_context_menu_triggerPtr
3168+
.asFunction<int Function(native_tray_icon_t)>();
3169+
31313170
/// Get the screen bounds of the tray icon
31323171
/// @param tray_icon The tray icon
31333172
/// @param bounds Pointer to store the bounds (caller allocated)
@@ -3913,6 +3952,35 @@ enum native_tray_icon_event_type_t {
39133952
};
39143953
}
39153954

3955+
/// Context menu trigger modes
3956+
/// Defines how the context menu is triggered for a tray icon
3957+
enum native_context_menu_trigger_t {
3958+
/// Manual control only
3959+
NATIVE_CONTEXT_MENU_TRIGGER_NONE(0),
3960+
3961+
/// Left click triggers menu
3962+
NATIVE_CONTEXT_MENU_TRIGGER_CLICKED(1),
3963+
3964+
/// Right click triggers menu
3965+
NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED(2),
3966+
3967+
/// Double click triggers menu
3968+
NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED(3);
3969+
3970+
final int value;
3971+
const native_context_menu_trigger_t(this.value);
3972+
3973+
static native_context_menu_trigger_t fromValue(int value) => switch (value) {
3974+
0 => NATIVE_CONTEXT_MENU_TRIGGER_NONE,
3975+
1 => NATIVE_CONTEXT_MENU_TRIGGER_CLICKED,
3976+
2 => NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED,
3977+
3 => NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED,
3978+
_ => throw ArgumentError(
3979+
"Unknown value for native_context_menu_trigger_t: $value",
3980+
),
3981+
};
3982+
}
3983+
39163984
/// Opaque handle for tray icon objects
39173985
typedef native_tray_icon_t = ffi.Pointer<ffi.Void>;
39183986

packages/nativeapi/lib/src/tray_icon.dart

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,71 @@ import 'package:nativeapi/src/image.dart';
99
import 'package:nativeapi/src/menu.dart';
1010
import 'package:nativeapi/src/tray_icon_event.dart';
1111

12+
/// Defines how the context menu is triggered for a tray icon.
13+
///
14+
/// This enum specifies which mouse interactions should display the tray icon's
15+
/// context menu. The values align with tray icon event types for consistency.
16+
enum ContextMenuTrigger {
17+
/// Context menu is not automatically triggered by mouse events.
18+
///
19+
/// The application must call [TrayIcon.openContextMenu] explicitly to display
20+
/// the menu. Use this when you want full control over when the menu appears.
21+
none,
22+
23+
/// Context menu is triggered on [TrayIconClickedEvent].
24+
///
25+
/// Automatically opens the context menu when the tray icon is left-clicked.
26+
/// This is common on some Linux desktop environments.
27+
clicked,
28+
29+
/// Context menu is triggered on [TrayIconRightClickedEvent].
30+
///
31+
/// Automatically opens the context menu when the tray icon is right-clicked.
32+
/// This follows the convention on Windows and most desktop environments.
33+
rightClicked,
34+
35+
/// Context menu is triggered on [TrayIconDoubleClickedEvent].
36+
///
37+
/// Automatically opens the context menu when the tray icon is double-clicked.
38+
/// Less common but useful for applications that use single-click for another action.
39+
doubleClicked,
40+
}
41+
42+
/// Extension methods for ContextMenuTrigger conversion
43+
extension ContextMenuTriggerExtension on ContextMenuTrigger {
44+
/// Convert this ContextMenuTrigger to a native enum value.
45+
native_context_menu_trigger_t toNative() {
46+
switch (this) {
47+
case ContextMenuTrigger.none:
48+
return native_context_menu_trigger_t.NATIVE_CONTEXT_MENU_TRIGGER_NONE;
49+
case ContextMenuTrigger.clicked:
50+
return native_context_menu_trigger_t.NATIVE_CONTEXT_MENU_TRIGGER_CLICKED;
51+
case ContextMenuTrigger.rightClicked:
52+
return native_context_menu_trigger_t
53+
.NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED;
54+
case ContextMenuTrigger.doubleClicked:
55+
return native_context_menu_trigger_t
56+
.NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED;
57+
}
58+
}
59+
60+
/// Convert a native enum value to ContextMenuTrigger.
61+
static ContextMenuTrigger fromNative(native_context_menu_trigger_t native) {
62+
switch (native) {
63+
case native_context_menu_trigger_t.NATIVE_CONTEXT_MENU_TRIGGER_NONE:
64+
return ContextMenuTrigger.none;
65+
case native_context_menu_trigger_t.NATIVE_CONTEXT_MENU_TRIGGER_CLICKED:
66+
return ContextMenuTrigger.clicked;
67+
case native_context_menu_trigger_t
68+
.NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED:
69+
return ContextMenuTrigger.rightClicked;
70+
case native_context_menu_trigger_t
71+
.NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED:
72+
return ContextMenuTrigger.doubleClicked;
73+
}
74+
}
75+
}
76+
1277
class TrayIcon
1378
with EventEmitter, CNativeApiBindingsMixin
1479
implements NativeHandleWrapper<native_tray_icon_t> {
@@ -220,6 +285,48 @@ class TrayIcon
220285
);
221286
}
222287

288+
/// Get the current context menu trigger behavior.
289+
///
290+
/// Returns the current [ContextMenuTrigger] setting that determines
291+
/// which mouse interactions will automatically display the context menu.
292+
ContextMenuTrigger get contextMenuTrigger {
293+
final native = bindings.native_tray_icon_get_context_menu_trigger(
294+
_nativeHandle,
295+
);
296+
return ContextMenuTriggerExtension.fromNative(native);
297+
}
298+
299+
/// Set the context menu trigger behavior.
300+
///
301+
/// Determines which mouse interactions will automatically display the
302+
/// context menu. By default, the trigger is set to [ContextMenuTrigger.none],
303+
/// requiring explicit control via [openContextMenu] or by setting a trigger mode.
304+
///
305+
/// Example:
306+
/// ```dart
307+
/// // Right click shows menu (common on Windows/Linux)
308+
/// trayIcon.contextMenuTrigger = ContextMenuTrigger.rightClicked;
309+
///
310+
/// // Left click shows menu (common on some Linux environments and macOS)
311+
/// trayIcon.contextMenuTrigger = ContextMenuTrigger.clicked;
312+
///
313+
/// // Double click shows menu
314+
/// trayIcon.contextMenuTrigger = ContextMenuTrigger.doubleClicked;
315+
///
316+
/// // Manual control (default) - handle events yourself
317+
/// trayIcon.contextMenuTrigger = ContextMenuTrigger.none;
318+
/// trayIcon.addListener<TrayIconRightClickedEvent>((event) {
319+
/// // Custom logic before showing menu
320+
/// trayIcon.openContextMenu();
321+
/// });
322+
/// ```
323+
set contextMenuTrigger(ContextMenuTrigger trigger) {
324+
bindings.native_tray_icon_set_context_menu_trigger(
325+
_nativeHandle,
326+
trigger.toNative(),
327+
);
328+
}
329+
223330
Rect? get bounds {
224331
final boundsPtr = ffi.calloc<native_rectangle_t>();
225332
final success = bindings.native_tray_icon_get_bounds(

0 commit comments

Comments
 (0)