Skip to content

Commit 32b27b2

Browse files
committed
Add advanced menu item state and submenu controls
Introduces extension methods for converting MenuItemType and MenuItemState to and from native enums. Adds getter/setter support for menu item state, radio group, enabled state, and submenu attachment in the native API. Updates the menu example to demonstrate dynamic submenu management, mixed checkbox state, and radio group handling.
1 parent b3a987e commit 32b27b2

2 files changed

Lines changed: 205 additions & 58 deletions

File tree

examples/menu_example/lib/main.dart

Lines changed: 102 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ class _MenuExamplePageState extends State<MenuExamplePage> {
4141
String _currentLabel = 'Dynamic Label Item';
4242

4343
int _menuItemCount = 0;
44+
45+
// Store references to menu items for state management
46+
late final MenuItem _checkboxItem;
47+
late final MenuItem _radio1;
48+
late final MenuItem _radio2;
49+
late final MenuItem _radio3;
50+
late final MenuItem _submenuItem;
51+
late final Menu _submenu;
4452

4553
@override
4654
void initState() {
@@ -73,46 +81,63 @@ class _MenuExamplePageState extends State<MenuExamplePage> {
7381
_contextMenu.addSeparator();
7482

7583
// 3. Checkbox menu item
76-
final checkboxItem = MenuItem('Checkbox Item', MenuItemType.checkbox);
77-
checkboxItem.on<MenuItemClickedEvent>((event) {
84+
_checkboxItem = MenuItem('Checkbox Item', MenuItemType.checkbox);
85+
_checkboxItem.state = MenuItemState.unchecked;
86+
_checkboxItem.on<MenuItemClickedEvent>((event) {
7887
setState(() {
7988
_checkboxState = !_checkboxState;
89+
_checkboxItem.state = _checkboxState ? MenuItemState.checked : MenuItemState.unchecked;
8090
});
8191
_addToHistory('Checkbox clicked - State: $_checkboxState (ID: ${event.menuItemId})');
8292
});
83-
_contextMenu.addItem(checkboxItem);
84-
_menuItems.add(checkboxItem);
85-
86-
// 4. Radio menu items
87-
final radio1 = MenuItem('Radio Option 1', MenuItemType.radio);
88-
radio1.on<MenuItemClickedEvent>((event) {
93+
_contextMenu.addItem(_checkboxItem);
94+
_menuItems.add(_checkboxItem);
95+
96+
// 4. Radio menu items (grouped together)
97+
_radio1 = MenuItem('Radio Option 1', MenuItemType.radio);
98+
_radio1.radioGroup = 1; // Set radio group ID
99+
_radio1.state = MenuItemState.checked; // Default selection
100+
_radio1.on<MenuItemClickedEvent>((event) {
89101
setState(() {
90102
_radioSelection = 'Option 1';
103+
_radio1.state = MenuItemState.checked;
104+
_radio2.state = MenuItemState.unchecked;
105+
_radio3.state = MenuItemState.unchecked;
91106
});
92107
_addToHistory('Radio Option 1 selected (ID: ${event.menuItemId})');
93108
});
94-
_contextMenu.addItem(radio1);
95-
_menuItems.add(radio1);
109+
_contextMenu.addItem(_radio1);
110+
_menuItems.add(_radio1);
96111

97-
final radio2 = MenuItem('Radio Option 2', MenuItemType.radio);
98-
radio2.on<MenuItemClickedEvent>((event) {
112+
_radio2 = MenuItem('Radio Option 2', MenuItemType.radio);
113+
_radio2.radioGroup = 1; // Same radio group
114+
_radio2.state = MenuItemState.unchecked;
115+
_radio2.on<MenuItemClickedEvent>((event) {
99116
setState(() {
100117
_radioSelection = 'Option 2';
118+
_radio1.state = MenuItemState.unchecked;
119+
_radio2.state = MenuItemState.checked;
120+
_radio3.state = MenuItemState.unchecked;
101121
});
102122
_addToHistory('Radio Option 2 selected (ID: ${event.menuItemId})');
103123
});
104-
_contextMenu.addItem(radio2);
105-
_menuItems.add(radio2);
124+
_contextMenu.addItem(_radio2);
125+
_menuItems.add(_radio2);
106126

107-
final radio3 = MenuItem('Radio Option 3', MenuItemType.radio);
108-
radio3.on<MenuItemClickedEvent>((event) {
127+
_radio3 = MenuItem('Radio Option 3', MenuItemType.radio);
128+
_radio3.radioGroup = 1; // Same radio group
129+
_radio3.state = MenuItemState.unchecked;
130+
_radio3.on<MenuItemClickedEvent>((event) {
109131
setState(() {
110132
_radioSelection = 'Option 3';
133+
_radio1.state = MenuItemState.unchecked;
134+
_radio2.state = MenuItemState.unchecked;
135+
_radio3.state = MenuItemState.checked;
111136
});
112137
_addToHistory('Radio Option 3 selected (ID: ${event.menuItemId})');
113138
});
114-
_contextMenu.addItem(radio3);
115-
_menuItems.add(radio3);
139+
_contextMenu.addItem(_radio3);
140+
_menuItems.add(_radio3);
116141

117142
// 5. Separator
118143
_contextMenu.addSeparator();
@@ -138,13 +163,13 @@ class _MenuExamplePageState extends State<MenuExamplePage> {
138163
_contextMenu.addSeparator();
139164

140165
// 9. Submenu
141-
final submenu = Menu();
142-
final submenuItem = MenuItem('Submenu', MenuItemType.submenu);
166+
_submenu = Menu();
167+
_submenuItem = MenuItem('Submenu', MenuItemType.submenu);
143168

144-
submenuItem.on<MenuItemSubmenuOpenedEvent>((event) {
169+
_submenuItem.on<MenuItemSubmenuOpenedEvent>((event) {
145170
_addToHistory('Submenu opened (ID: ${event.menuItemId})');
146171
});
147-
submenuItem.on<MenuItemSubmenuClosedEvent>((event) {
172+
_submenuItem.on<MenuItemSubmenuClosedEvent>((event) {
148173
_addToHistory('Submenu closed (ID: ${event.menuItemId})');
149174
});
150175

@@ -153,24 +178,27 @@ class _MenuExamplePageState extends State<MenuExamplePage> {
153178
subItem1.on<MenuItemClickedEvent>((event) {
154179
_addToHistory('Submenu Item 1 clicked (ID: ${event.menuItemId})');
155180
});
156-
submenu.addItem(subItem1);
181+
_submenu.addItem(subItem1);
157182

158183
final subItem2 = MenuItem('Submenu Item 2');
159184
subItem2.on<MenuItemClickedEvent>((event) {
160185
_addToHistory('Submenu Item 2 clicked (ID: ${event.menuItemId})');
161186
});
162-
submenu.addItem(subItem2);
187+
_submenu.addItem(subItem2);
163188

164-
submenu.addSeparator();
189+
_submenu.addSeparator();
165190

166191
final subItem3 = MenuItem('Submenu Item 3');
167192
subItem3.on<MenuItemClickedEvent>((event) {
168193
_addToHistory('Submenu Item 3 clicked (ID: ${event.menuItemId})');
169194
});
170-
submenu.addItem(subItem3);
195+
_submenu.addItem(subItem3);
196+
197+
// Associate the submenu with the menu item
198+
_submenuItem.submenu = _submenu;
171199

172-
_contextMenu.addItem(submenuItem);
173-
_menuItems.add(submenuItem);
200+
_contextMenu.addItem(_submenuItem);
201+
_menuItems.add(_submenuItem);
174202

175203
// 10. Separator
176204
_contextMenu.addSeparator();
@@ -265,6 +293,34 @@ class _MenuExamplePageState extends State<MenuExamplePage> {
265293
_addToHistory('Menu item label changed to: $_currentLabel');
266294
}
267295

296+
void _setCheckboxMixed() {
297+
setState(() {
298+
_checkboxItem.state = MenuItemState.mixed;
299+
});
300+
_addToHistory('Checkbox state set to Mixed (indeterminate)');
301+
}
302+
303+
void _addSubmenuItem() {
304+
final newSubItem = MenuItem('Dynamic Submenu Item ${_submenu.itemCount + 1}');
305+
newSubItem.on<MenuItemClickedEvent>((event) {
306+
_addToHistory('Dynamic submenu item clicked (ID: ${event.menuItemId})');
307+
});
308+
_submenu.addItem(newSubItem);
309+
_addToHistory('Added new item to submenu (Total: ${_submenu.itemCount})');
310+
}
311+
312+
void _toggleSubmenu() {
313+
setState(() {
314+
if (_submenuItem.submenu != null) {
315+
_submenuItem.submenu = null;
316+
_addToHistory('Submenu detached from menu item');
317+
} else {
318+
_submenuItem.submenu = _submenu;
319+
_addToHistory('Submenu attached to menu item');
320+
}
321+
});
322+
}
323+
268324
void _addNewMenuItem() {
269325
final newItem = MenuItem('New Item ${_menuItems.length + 1}');
270326
newItem.on<MenuItemClickedEvent>((event) {
@@ -387,6 +443,24 @@ class _MenuExamplePageState extends State<MenuExamplePage> {
387443
label: const Text('Change Dynamic Label'),
388444
),
389445
const SizedBox(height: 8),
446+
ElevatedButton.icon(
447+
onPressed: _setCheckboxMixed,
448+
icon: const Icon(Icons.indeterminate_check_box, size: 18),
449+
label: const Text('Set Checkbox to Mixed State'),
450+
),
451+
const SizedBox(height: 8),
452+
ElevatedButton.icon(
453+
onPressed: _addSubmenuItem,
454+
icon: const Icon(Icons.add_box, size: 18),
455+
label: const Text('Add Item to Submenu'),
456+
),
457+
const SizedBox(height: 8),
458+
ElevatedButton.icon(
459+
onPressed: _toggleSubmenu,
460+
icon: const Icon(Icons.swap_horiz, size: 18),
461+
label: const Text('Toggle Submenu Attachment'),
462+
),
463+
const SizedBox(height: 8),
390464
ElevatedButton.icon(
391465
onPressed: _addNewMenuItem,
392466
icon: const Icon(Icons.add, size: 18),

packages/nativeapi/lib/src/menu.dart

Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,64 @@ enum MenuItemType { normal, separator, submenu, checkbox, radio }
1313

1414
enum MenuItemState { unchecked, checked, mixed }
1515

16+
// Extension methods for MenuItemType conversion
17+
extension MenuItemTypeExtension on MenuItemType {
18+
native_menu_item_type_t toNative() {
19+
switch (this) {
20+
case MenuItemType.normal:
21+
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_NORMAL;
22+
case MenuItemType.separator:
23+
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SEPARATOR;
24+
case MenuItemType.submenu:
25+
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SUBMENU;
26+
case MenuItemType.checkbox:
27+
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_CHECKBOX;
28+
case MenuItemType.radio:
29+
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_RADIO;
30+
}
31+
}
32+
33+
static MenuItemType fromNative(native_menu_item_type_t nativeType) {
34+
switch (nativeType) {
35+
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_NORMAL:
36+
return MenuItemType.normal;
37+
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SEPARATOR:
38+
return MenuItemType.separator;
39+
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SUBMENU:
40+
return MenuItemType.submenu;
41+
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_CHECKBOX:
42+
return MenuItemType.checkbox;
43+
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_RADIO:
44+
return MenuItemType.radio;
45+
}
46+
}
47+
}
48+
49+
// Extension methods for MenuItemState conversion
50+
extension MenuItemStateExtension on MenuItemState {
51+
native_menu_item_state_t toNative() {
52+
switch (this) {
53+
case MenuItemState.unchecked:
54+
return native_menu_item_state_t.NATIVE_MENU_ITEM_STATE_UNCHECKED;
55+
case MenuItemState.checked:
56+
return native_menu_item_state_t.NATIVE_MENU_ITEM_STATE_CHECKED;
57+
case MenuItemState.mixed:
58+
return native_menu_item_state_t.NATIVE_MENU_ITEM_STATE_MIXED;
59+
}
60+
}
61+
62+
static MenuItemState fromNative(native_menu_item_state_t nativeState) {
63+
switch (nativeState) {
64+
case native_menu_item_state_t.NATIVE_MENU_ITEM_STATE_UNCHECKED:
65+
return MenuItemState.unchecked;
66+
case native_menu_item_state_t.NATIVE_MENU_ITEM_STATE_CHECKED:
67+
return MenuItemState.checked;
68+
case native_menu_item_state_t.NATIVE_MENU_ITEM_STATE_MIXED:
69+
return MenuItemState.mixed;
70+
}
71+
}
72+
}
73+
1674
class MenuItem
1775
with EventEmitter, CNativeApiBindingsMixin
1876
implements NativeHandleWrapper<native_menu_item_t> {
@@ -40,7 +98,7 @@ class MenuItem
4098
final labelPtr = label.toNativeUtf8().cast<Char>();
4199
_nativeHandle = bindings.native_menu_item_create(
42100
labelPtr,
43-
_convertMenuItemType(type),
101+
type.toNative(),
44102
);
45103
ffi.calloc.free(labelPtr);
46104

@@ -148,40 +206,13 @@ class MenuItem
148206
}
149207
}
150208

151-
// Helper method to convert MenuItemType to native enum
152-
static native_menu_item_type_t _convertMenuItemType(MenuItemType type) {
153-
switch (type) {
154-
case MenuItemType.normal:
155-
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_NORMAL;
156-
case MenuItemType.separator:
157-
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SEPARATOR;
158-
case MenuItemType.submenu:
159-
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SUBMENU;
160-
case MenuItemType.checkbox:
161-
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_CHECKBOX;
162-
case MenuItemType.radio:
163-
return native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_RADIO;
164-
}
165-
}
166-
167209
int get id {
168210
return bindings.native_menu_item_get_id(_nativeHandle);
169211
}
170212

171213
MenuItemType get type {
172-
final type = bindings.native_menu_item_get_type(_nativeHandle);
173-
switch (type) {
174-
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_NORMAL:
175-
return MenuItemType.normal;
176-
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SEPARATOR:
177-
return MenuItemType.separator;
178-
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_SUBMENU:
179-
return MenuItemType.submenu;
180-
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_CHECKBOX:
181-
return MenuItemType.checkbox;
182-
case native_menu_item_type_t.NATIVE_MENU_ITEM_TYPE_RADIO:
183-
return MenuItemType.radio;
184-
}
214+
final nativeType = bindings.native_menu_item_get_type(_nativeHandle);
215+
return MenuItemTypeExtension.fromNative(nativeType);
185216
}
186217

187218
String get label {
@@ -225,6 +256,48 @@ class MenuItem
225256
ffi.calloc.free(tooltipPtr);
226257
}
227258

259+
MenuItemState get state {
260+
final nativeState = bindings.native_menu_item_get_state(_nativeHandle);
261+
return MenuItemStateExtension.fromNative(nativeState);
262+
}
263+
264+
set state(MenuItemState state) {
265+
bindings.native_menu_item_set_state(_nativeHandle, state.toNative());
266+
}
267+
268+
int get radioGroup {
269+
return bindings.native_menu_item_get_radio_group(_nativeHandle);
270+
}
271+
272+
set radioGroup(int groupId) {
273+
bindings.native_menu_item_set_radio_group(_nativeHandle, groupId);
274+
}
275+
276+
bool get enabled {
277+
return bindings.native_menu_item_is_enabled(_nativeHandle);
278+
}
279+
280+
set enabled(bool enabled) {
281+
bindings.native_menu_item_set_enabled(_nativeHandle, enabled);
282+
}
283+
284+
Menu? get submenu {
285+
final submenuHandle = bindings.native_menu_item_get_submenu(_nativeHandle);
286+
if (submenuHandle == nullptr) {
287+
return null;
288+
}
289+
// Return a Menu wrapper for the existing native handle
290+
return Menu(submenuHandle);
291+
}
292+
293+
set submenu(Menu? menu) {
294+
if (menu == null) {
295+
bindings.native_menu_item_remove_submenu(_nativeHandle);
296+
} else {
297+
bindings.native_menu_item_set_submenu(_nativeHandle, menu.nativeHandle);
298+
}
299+
}
300+
228301
@override
229302
native_menu_item_t get nativeHandle => _nativeHandle;
230303

0 commit comments

Comments
 (0)