Skip to content

Commit 0e81616

Browse files
authored
Merge pull request #1824 from EnsembleUI/drawerBranch
[1806] Updated Drawer to have custom widgets and functions
2 parents 4ba57cb + 22cf5be commit 0e81616

File tree

6 files changed

+258
-37
lines changed

6 files changed

+258
-37
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:ensemble/ensemble.dart';
2+
import 'package:ensemble/framework/action.dart';
3+
import 'package:ensemble/framework/scope.dart';
4+
import 'package:ensemble/util/utils.dart';
5+
import 'package:ensemble_ts_interpreter/invokables/invokable.dart';
6+
import 'package:flutter/material.dart';
7+
8+
/// Action to open drawer in the current context
9+
/// Will open screen-level drawer if exists, otherwise app-level drawer
10+
class OpenDrawerAction extends EnsembleAction {
11+
OpenDrawerAction({super.initiator});
12+
13+
factory OpenDrawerAction.from({Invokable? initiator, Map? payload}) {
14+
return OpenDrawerAction(initiator: initiator);
15+
}
16+
17+
@override
18+
Future<void> execute(BuildContext context, ScopeManager scopeManager) {
19+
try {
20+
// Get nearest Scaffold
21+
ScaffoldState? scaffold = Scaffold.maybeOf(context);
22+
23+
// Check if scaffold exists and has drawer
24+
if (scaffold != null && scaffold.hasDrawer) {
25+
scaffold.openDrawer();
26+
}
27+
} catch (e) {
28+
debugPrint('Error opening drawer: $e');
29+
}
30+
return Future.value();
31+
}
32+
}
33+
34+
class CloseDrawerAction extends EnsembleAction {
35+
CloseDrawerAction({super.initiator});
36+
37+
factory CloseDrawerAction.from({Invokable? initiator, Map? payload}) {
38+
return CloseDrawerAction(initiator: initiator);
39+
}
40+
41+
@override
42+
Future execute(BuildContext context, ScopeManager scopeManager) async {
43+
try {
44+
// Get the current route
45+
final currentRoute = Ensemble().getCurrentRoute();
46+
final scaffold = Scaffold.maybeOf(context);
47+
48+
// Check if we're in a drawer route
49+
if (currentRoute != null &&
50+
currentRoute.isCurrent &&
51+
currentRoute.navigator != null &&
52+
currentRoute is MaterialPageRoute) {
53+
if(scaffold != null && !scaffold.hasDrawer) {
54+
currentRoute.navigator!.maybePop();
55+
return Future.value(true);
56+
}
57+
}
58+
return Future.value(false);
59+
} catch (e) {
60+
debugPrint('Error closing drawer: $e');
61+
return Future.value(false);
62+
}
63+
}
64+
}

modules/ensemble/lib/framework/action.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:ensemble/action/deep_link_action.dart';
88
import 'package:ensemble/action/call_external_method.dart';
99
import 'package:ensemble/action/device_security.dart';
1010
import 'package:ensemble/action/dialog_actions.dart';
11+
import 'package:ensemble/action/drawer_actions.dart';
1112
import 'package:ensemble/action/execute_action_group_action.dart';
1213
import 'package:ensemble/action/get_network_info_action.dart';
1314
import 'package:ensemble/action/haptic_action.dart';
@@ -1061,6 +1062,8 @@ enum ActionType {
10611062
bluetoothSubscribeCharacteristic,
10621063
bluetoothUnsubscribeCharacteristic,
10631064
controlDeviceBackNavigation,
1065+
openDrawer,
1066+
closeDrawer,
10641067
closeApp,
10651068
saveFile,
10661069
sendVerificationCode,
@@ -1290,6 +1293,10 @@ abstract class EnsembleAction {
12901293
return SubscribeBluetoothCharacteristicsAction.from(payload: payload);
12911294
} else if (actionType == ActionType.bluetoothUnsubscribeCharacteristic) {
12921295
return UnSubscribeBluetoothCharacteristicsAction.from(payload: payload);
1296+
} else if (actionType == ActionType.openDrawer) {
1297+
return OpenDrawerAction.from(initiator: initiator, payload: payload);
1298+
} else if (actionType == ActionType.closeDrawer) {
1299+
return CloseDrawerAction.from(initiator: initiator, payload: payload);
12931300
} else if (actionType == ActionType.sendVerificationCode) {
12941301
return SendVerificationCodeAction.fromYaml(
12951302
initiator: initiator, payload: payload);

modules/ensemble/lib/framework/menu.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ abstract class Menu extends Object with HasStyles, Invokable {
9292
Utils.optionalString(item['floatingAlignment']) ?? 'center',
9393
floatingMargin: Utils.optionalInt(item['floatingMargin']),
9494
switchScreen: Utils.getBool(item['switchScreen'], fallback: true),
95+
isClickable: Utils.getBool(item['isClickable'], fallback: true),
9596
onTap: item['onTap'],
9697
onTapHaptic: Utils.optionalString(item['onTapHaptic']),
9798
isExternal: Utils.getBool(item['isExternal'], fallback: false),
@@ -291,6 +292,7 @@ class MenuItem {
291292
this.selected,
292293
this.floating = false,
293294
this.switchScreen = true,
295+
this.isClickable = true,
294296
this.floatingAlignment = 'center',
295297
this.floatingMargin,
296298
this.onTap,
@@ -309,6 +311,7 @@ class MenuItem {
309311
final dynamic selected;
310312
final bool floating;
311313
final bool switchScreen;
314+
final bool isClickable;
312315
final String floatingAlignment;
313316
final int? floatingMargin;
314317
final dynamic onTap;

modules/ensemble/lib/framework/view/page.dart

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:developer';
22

33
import 'package:ensemble/ensemble.dart';
4+
import 'package:ensemble/framework/action.dart';
45
import 'package:ensemble/ensemble_app.dart';
56
import 'package:ensemble/framework/data_context.dart';
67
import 'package:ensemble/framework/devmode.dart';
@@ -331,6 +332,12 @@ class PageState extends State<Page>
331332
backgroundWidget =
332333
_scopeManager.buildWidget(headerModel.flexibleBackground!);
333334
}
335+
// Build custom leading widget if provided
336+
Widget? leadingWidget;
337+
if (headerModel.leadingWidget != null) {
338+
leadingWidget = _scopeManager.buildWidget(headerModel.leadingWidget!);
339+
}
340+
334341
final evaluatedHeader = EnsembleThemeManager()
335342
.getRuntimeStyles(_scopeManager.dataContext, headerModel);
336343

@@ -362,7 +369,9 @@ class PageState extends State<Page>
362369

363370
if (scrollableView) {
364371
return SliverAppBar(
365-
automaticallyImplyLeading: showNavigationIcon != false,
372+
automaticallyImplyLeading:
373+
leadingWidget == null && showNavigationIcon != false,
374+
leading: leadingWidget,
366375
title: titleWidget,
367376
centerTitle: centerTitle,
368377
backgroundColor: backgroundColor,
@@ -383,7 +392,9 @@ class PageState extends State<Page>
383392
);
384393
} else {
385394
return AppBar(
386-
automaticallyImplyLeading: showNavigationIcon != false,
395+
automaticallyImplyLeading:
396+
leadingWidget == null && showNavigationIcon != false,
397+
leading: leadingWidget,
387398
title: titleWidget,
388399
centerTitle: centerTitle,
389400
backgroundColor: backgroundColor,
@@ -696,21 +707,68 @@ class PageState extends State<Page>
696707
}
697708

698709
Drawer? _buildDrawer(BuildContext context, DrawerMenu menu) {
699-
List<ListTile> navItems = [];
700-
for (int i = 0; i < menu.menuItems.length; i++) {
701-
MenuItem item = menu.menuItems[i];
702-
navItems.add(ListTile(
703-
selected: i == selectedPage,
704-
title: Text(Utils.translate(item.label ?? '', context)),
705-
leading: ensemble.Icon(item.icon ?? '', library: item.iconLibrary),
706-
horizontalTitleGap: 0,
707-
onTap: () => selectNavigationIndex(context, item),
708-
));
710+
List<MenuItem> visibleItems =
711+
Menu.getVisibleMenuItems(_scopeManager.dataContext, menu.menuItems);
712+
List<Widget> menuItems = [];
713+
714+
for (int i = 0; i < visibleItems.length; i++) {
715+
MenuItem item = visibleItems[i];
716+
717+
final customWidget = _buildCustomIcon(item);
718+
final label = customWidget != null
719+
? ''
720+
: Utils.translate(item.label ?? '', context);
721+
722+
Widget menuItem;
723+
void handleTap() {
724+
if (!item.isClickable) return;
725+
726+
if (item.onTap != null) {
727+
ScreenController().executeActionWithScope(
728+
context, _scopeManager, EnsembleAction.from(item.onTap)!);
729+
}
730+
if (item.switchScreen) {
731+
selectNavigationIndex(context, item);
732+
}
733+
}
734+
735+
if (customWidget != null) {
736+
menuItem = item.isClickable
737+
? InkWell(onTap: handleTap, child: customWidget)
738+
: customWidget;
739+
} else {
740+
menuItem = ListTile(
741+
enabled: item.isClickable,
742+
selected: i == selectedPage,
743+
title: Text(label),
744+
leading: ensemble.Icon(item.icon ?? '', library: item.iconLibrary),
745+
horizontalTitleGap: 0,
746+
onTap: item.isClickable ? handleTap : null,
747+
);
748+
}
749+
750+
menuItems.add(menuItem);
709751
}
752+
710753
return Drawer(
711754
backgroundColor: Utils.getColor(menu.runtimeStyles?['backgroundColor']),
712-
child: ListView(
713-
children: navItems,
755+
child: Column(
756+
children: [
757+
// Header
758+
if (menu.headerModel != null)
759+
_scopeManager.buildWidget(menu.headerModel!),
760+
761+
// Menu Items in scrollable area
762+
Expanded(
763+
child: ListView(
764+
children: menuItems,
765+
),
766+
),
767+
768+
// Footer at bottom
769+
if (menu.footerModel != null)
770+
_scopeManager.buildWidget(menu.footerModel!),
771+
],
714772
),
715773
);
716774
}

modules/ensemble/lib/framework/view/page_group.dart

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'dart:developer';
22

3+
import 'package:ensemble/action/drawer_actions.dart';
4+
import 'package:ensemble/framework/action.dart';
35
import 'package:ensemble/framework/data_context.dart';
46
import 'package:ensemble/framework/device.dart';
57
import 'package:ensemble/framework/error_handling.dart';
@@ -100,7 +102,8 @@ class PageGroupState extends State<PageGroup> with MediaQueryCapability {
100102
);
101103

102104
// Need to get the items which are only visible
103-
widget.menu.menuItems = Menu.getVisibleMenuItems(_scopeManager.dataContext, widget.menu.menuItems);
105+
widget.menu.menuItems = Menu.getVisibleMenuItems(
106+
_scopeManager.dataContext, widget.menu.menuItems);
104107

105108
int? selectedIndex = Utils.optionalInt(widget.pageArgs?["viewIndex"],
106109
min: 0, max: widget.menu.menuItems.length - 1);
@@ -366,34 +369,110 @@ class PageGroupState extends State<PageGroup> with MediaQueryCapability {
366369
// }
367370

368371
Drawer? _buildDrawer(BuildContext context, Menu menu) {
369-
List<ListenableBuilder> navItems = [];
370-
for (var i = 0; i < menu.menuItems.length; i++) {
371-
MenuItem item = menu.menuItems[i];
372-
navItems.add(ListenableBuilder(
373-
listenable: viewGroupNotifier,
374-
builder: (context, _) {
375-
return ListTile(
376-
selected: i == viewGroupNotifier.viewIndex,
377-
title: Text(Utils.translate(item.label ?? '', context)),
378-
leading:
379-
ensemble.Icon(item.icon ?? '', library: item.iconLibrary),
380-
horizontalTitleGap: 0,
381-
onTap: () {
382-
//close the drawer
383-
Navigator.maybePop(context);
384-
viewGroupNotifier.updatePage(i);
385-
},
386-
);
387-
}));
388-
}
372+
// Filter menu items based on visibility conditions
373+
List<MenuItem> visibleItems =
374+
Menu.getVisibleMenuItems(_scopeManager.dataContext, menu.menuItems);
375+
389376
return Drawer(
390377
backgroundColor: Utils.getColor(menu.runtimeStyles?['backgroundColor']),
391-
child: ListView(
392-
children: navItems,
378+
child: Column(
379+
children: [
380+
// Optional header section at the top of drawer
381+
if (menu.headerModel != null)
382+
_scopeManager.buildWidget(menu.headerModel!),
383+
384+
// Main scrollable menu content wrapped in Expanded
385+
Expanded(
386+
child: ListenableBuilder(
387+
listenable: viewGroupNotifier,
388+
builder: (context, _) {
389+
List<Widget> navItems = [];
390+
391+
for (int i = 0; i < visibleItems.length; i++) {
392+
MenuItem item = visibleItems[i];
393+
final isSelected = i == viewGroupNotifier.viewIndex;
394+
final customIcon = _buildCustomWidget(item);
395+
final customActiveIcon =
396+
_buildCustomWidget(item, isActive: true);
397+
398+
// Determine if using custom widgets or default ListTile
399+
final isCustom =
400+
customIcon != null || customActiveIcon != null;
401+
final label = isCustom
402+
? ''
403+
: Utils.translate(item.label ?? '', context);
404+
405+
// Handler for menu item taps
406+
void handleTap() {
407+
// Skip if item is not clickable
408+
if (!item.isClickable) return;
409+
410+
// Always close drawer when item is tapped
411+
Navigator.maybePop(context);
412+
if (item.onTap != null) {
413+
ScreenController().executeActionWithScope(context,
414+
_scopeManager, EnsembleAction.from(item.onTap)!);
415+
}
416+
417+
// Switch screens only if enabled and not already selected
418+
if (item.switchScreen &&
419+
viewGroupNotifier.viewIndex != i) {
420+
viewGroupNotifier.updatePage(i);
421+
}
422+
}
423+
424+
Widget menuItem;
425+
if (isCustom) {
426+
// Choose between active and normal custom widget based on selection
427+
final displayWidget = isSelected
428+
? (customActiveIcon ??
429+
customIcon!) // Fallback to normal if active not provided
430+
: customIcon!;
431+
432+
// Wrap in InkWell only if clickable
433+
menuItem = item.isClickable
434+
? InkWell(onTap: handleTap, child: displayWidget)
435+
: displayWidget;
436+
} else {
437+
// Default ListTile implementation
438+
menuItem = ListTile(
439+
enabled: item.isClickable,
440+
selected: isSelected,
441+
title: Text(label),
442+
// Use active icon when selected, fallback to normal icon
443+
leading: ensemble.Icon(
444+
isSelected
445+
? (item.activeIcon ?? item.icon)
446+
: (item.icon ?? ''),
447+
library: item.iconLibrary),
448+
horizontalTitleGap: 0,
449+
onTap: item.isClickable ? handleTap : null,
450+
);
451+
}
452+
navItems.add(menuItem);
453+
}
454+
return ListView(children: navItems);
455+
}),
456+
),
457+
458+
// Optional footer section at bottom of drawer
459+
if (menu.footerModel != null)
460+
_scopeManager.buildWidget(menu.footerModel!),
461+
],
393462
),
394463
);
395464
}
396465

466+
Widget? _buildCustomWidget(MenuItem item, {bool isActive = false}) {
467+
Widget? iconWidget;
468+
dynamic customWidgetModel =
469+
isActive ? item.customActiveWidget : item.customWidget;
470+
if (customWidgetModel != null) {
471+
iconWidget = _scopeManager.buildWidget(customWidgetModel);
472+
}
473+
return iconWidget;
474+
}
475+
397476
/// TODO: can't do this anymore without Conditional widget
398477
/// get the menu mode depending on user spec + device types / screen resolutions
399478
// MenuDisplay _getPreferredMenuDisplay(Menu menu) {

0 commit comments

Comments
 (0)