Skip to content

Commit 710b158

Browse files
committed
files: Rebuild context menu
We now use MenuAnchor to show menus and the api is now generic for both popups and context menus. We don't yet handle shortcuts as they require me to think more about stuff kek
1 parent 3fe39c5 commit 710b158

File tree

10 files changed

+357
-525
lines changed

10 files changed

+357
-525
lines changed

lib/main.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import 'package:files/backend/folder_provider.dart';
1818
import 'package:files/backend/providers.dart';
1919
import 'package:files/backend/utils.dart';
2020
import 'package:files/backend/workspace.dart';
21-
import 'package:files/widgets/context_menu/context_menu_theme.dart';
2221
import 'package:files/widgets/side_pane.dart';
2322
import 'package:files/widgets/tab_strip.dart';
2423
import 'package:files/widgets/workspace.dart';
@@ -62,7 +61,13 @@ class Files extends StatelessWidget {
6261
mainAxisMargin: 0,
6362
radius: Radius.zero,
6463
),
65-
extensions: [ContextMenuTheme()],
64+
menuTheme: const MenuThemeData(
65+
style: MenuStyle(
66+
padding: MaterialStatePropertyAll(
67+
EdgeInsets.symmetric(vertical: 16),
68+
),
69+
),
70+
),
6671
),
6772
scrollBehavior: const MaterialScrollBehavior().copyWith(
6873
scrollbars: false,

lib/widgets/context_menu.dart

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import 'package:flutter/material.dart';
2+
3+
class ContextMenu extends StatefulWidget {
4+
final List<BaseContextMenuItem> entries;
5+
final Widget child;
6+
final bool openOnLongPress;
7+
final bool openOnSecondaryPress;
8+
9+
const ContextMenu({
10+
required this.entries,
11+
required this.child,
12+
this.openOnLongPress = true,
13+
this.openOnSecondaryPress = true,
14+
super.key,
15+
});
16+
17+
@override
18+
State<ContextMenu> createState() => _ContextMenuState();
19+
}
20+
21+
class _ContextMenuState extends State<ContextMenu> {
22+
late Offset lastPosition;
23+
24+
@override
25+
Widget build(BuildContext context) {
26+
return MenuAnchor(
27+
menuChildren: widget.entries.map((e) => e.buildWrapper(context)).toList(),
28+
builder: (context, controller, child) {
29+
return GestureDetector(
30+
onSecondaryTapUp: (details) =>
31+
controller.open(position: details.localPosition),
32+
onLongPressDown: (details) => lastPosition = details.localPosition,
33+
onLongPress: () => controller.open(position: lastPosition),
34+
child: child,
35+
);
36+
},
37+
child: widget.child,
38+
);
39+
}
40+
}
41+
42+
class _EnabledBuilder extends StatelessWidget {
43+
final bool enabled;
44+
final Widget child;
45+
46+
const _EnabledBuilder({
47+
required this.enabled,
48+
required this.child,
49+
});
50+
51+
@override
52+
Widget build(BuildContext context) {
53+
return IgnorePointer(
54+
ignoring: !enabled,
55+
child: Opacity(
56+
opacity: enabled ? 1 : 0.4,
57+
child: child,
58+
),
59+
);
60+
}
61+
}
62+
63+
abstract class BaseContextMenuItem {
64+
final Widget child;
65+
final Widget? leading;
66+
final Widget? trailing;
67+
final bool enabled;
68+
69+
const BaseContextMenuItem({
70+
required this.child,
71+
this.enabled = true,
72+
this.leading,
73+
this.trailing,
74+
});
75+
76+
Widget buildWrapper(BuildContext context) =>
77+
_EnabledBuilder(enabled: enabled, child: build(context));
78+
79+
Widget build(BuildContext context);
80+
Widget? buildLeading(BuildContext context) => leading;
81+
Widget? buildTrailing(BuildContext context) => trailing;
82+
}
83+
84+
class SubmenuMenuItem extends BaseContextMenuItem {
85+
final List<BaseContextMenuItem> menuChildren;
86+
87+
const SubmenuMenuItem({
88+
required super.child,
89+
required this.menuChildren,
90+
super.leading,
91+
super.enabled,
92+
}) : super(trailing: null);
93+
94+
@override
95+
Widget? buildTrailing(BuildContext context) {
96+
return const Icon(Icons.chevron_right, size: 16);
97+
}
98+
99+
@override
100+
Widget build(BuildContext context) {
101+
return SubmenuButton(
102+
menuChildren: menuChildren.map((e) => e.buildWrapper(context)).toList(),
103+
leadingIcon: buildLeading(context),
104+
trailingIcon: buildTrailing(context),
105+
style: const ButtonStyle(
106+
padding: MaterialStatePropertyAll(
107+
EdgeInsets.symmetric(horizontal: 16),
108+
),
109+
),
110+
child: child,
111+
);
112+
}
113+
}
114+
115+
class ContextMenuItem extends BaseContextMenuItem {
116+
final VoidCallback? onTap;
117+
final MenuSerializableShortcut? shortcut;
118+
119+
const ContextMenuItem({
120+
required super.child,
121+
this.onTap,
122+
super.leading,
123+
super.trailing,
124+
this.shortcut,
125+
super.enabled,
126+
});
127+
128+
@override
129+
Widget build(BuildContext context) {
130+
final Widget? leading = buildLeading(context);
131+
132+
return MenuItemButton(
133+
leadingIcon: leading != null
134+
? IconTheme.merge(
135+
data: Theme.of(context).iconTheme.copyWith(size: 16),
136+
child: leading,
137+
)
138+
: null,
139+
trailingIcon: buildTrailing(context),
140+
onPressed: onTap,
141+
shortcut: shortcut,
142+
style: const ButtonStyle(
143+
padding: MaterialStatePropertyAll(
144+
EdgeInsets.symmetric(horizontal: 16),
145+
),
146+
),
147+
child: child,
148+
);
149+
}
150+
}
151+
152+
class RadioMenuItem<T> extends ContextMenuItem {
153+
final T value;
154+
final T? groupValue;
155+
final ValueChanged<T?>? onChanged;
156+
final bool toggleable;
157+
158+
const RadioMenuItem({
159+
required this.value,
160+
required this.groupValue,
161+
this.onChanged,
162+
this.toggleable = false,
163+
required super.child,
164+
super.trailing,
165+
super.shortcut,
166+
super.enabled,
167+
}) : super(leading: null, onTap: null);
168+
169+
@override
170+
VoidCallback? get onTap => onChanged == null
171+
? null
172+
: () {
173+
if (toggleable && groupValue == value) {
174+
onChanged!.call(null);
175+
return;
176+
}
177+
onChanged!.call(value);
178+
};
179+
180+
@override
181+
Widget? buildTrailing(BuildContext context) {
182+
return ExcludeFocus(
183+
child: IgnorePointer(
184+
child: ConstrainedBox(
185+
constraints: const BoxConstraints(
186+
maxHeight: Checkbox.width,
187+
maxWidth: Checkbox.width,
188+
),
189+
child: Radio<T>(
190+
groupValue: groupValue,
191+
value: value,
192+
onChanged: onChanged,
193+
toggleable: toggleable,
194+
),
195+
),
196+
),
197+
);
198+
}
199+
}
200+
201+
class CheckboxMenuItem extends ContextMenuItem {
202+
final bool? value;
203+
final ValueChanged<bool?>? onChanged;
204+
final bool tristate;
205+
206+
const CheckboxMenuItem({
207+
required this.value,
208+
this.onChanged,
209+
this.tristate = false,
210+
required super.child,
211+
super.trailing,
212+
super.shortcut,
213+
super.enabled,
214+
}) : super(leading: null, onTap: null);
215+
216+
@override
217+
VoidCallback? get onTap => onChanged == null
218+
? null
219+
: () {
220+
switch (value) {
221+
case false:
222+
onChanged!.call(true);
223+
break;
224+
case true:
225+
onChanged!.call(tristate ? null : false);
226+
break;
227+
case null:
228+
onChanged!.call(false);
229+
break;
230+
}
231+
};
232+
233+
@override
234+
Widget? buildTrailing(BuildContext context) {
235+
return ExcludeFocus(
236+
child: IgnorePointer(
237+
child: ConstrainedBox(
238+
constraints: const BoxConstraints(
239+
maxHeight: Checkbox.width,
240+
maxWidth: Checkbox.width,
241+
),
242+
child: Checkbox(
243+
value: value,
244+
onChanged: onChanged,
245+
tristate: tristate,
246+
),
247+
),
248+
),
249+
);
250+
}
251+
}
252+
253+
class ContextMenuDivider extends BaseContextMenuItem {
254+
const ContextMenuDivider() : super(child: const SizedBox());
255+
256+
@override
257+
Widget build(BuildContext context) => const Divider();
258+
}

lib/widgets/context_menu/context_menu.dart

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)