Skip to content

Commit 8110110

Browse files
committed
PopupMenuButton control
1 parent 8c11922 commit 8110110

File tree

7 files changed

+306
-1
lines changed

7 files changed

+306
-1
lines changed

client/lib/controls/create_control.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import 'image.dart';
2323
import 'list_view.dart';
2424
import 'outlined_button.dart';
2525
import 'page.dart';
26+
import 'popup_menu_button.dart';
2627
import 'progress_bar.dart';
2728
import 'progress_ring.dart';
2829
import 'radio.dart';
@@ -121,6 +122,12 @@ Widget createControl(Control? parent, String id, bool parentDisabled) {
121122
control: controlView.control,
122123
children: controlView.children,
123124
parentDisabled: parentDisabled);
125+
case ControlType.popupMenuButton:
126+
return PopupMenuButtonControl(
127+
parent: parent,
128+
control: controlView.control,
129+
children: controlView.children,
130+
parentDisabled: parentDisabled);
124131
case ControlType.column:
125132
return ColumnControl(
126133
parent: parent,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import 'package:flet_view/utils/icons.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_redux/flutter_redux.dart';
4+
5+
import '../models/app_state.dart';
6+
import '../models/control.dart';
7+
import '../models/controls_view_model.dart';
8+
import '../web_socket_client.dart';
9+
import 'create_control.dart';
10+
11+
class PopupMenuButtonControl extends StatefulWidget {
12+
final Control? parent;
13+
final Control control;
14+
final bool parentDisabled;
15+
final List<Control> children;
16+
17+
const PopupMenuButtonControl(
18+
{Key? key,
19+
this.parent,
20+
required this.control,
21+
required this.children,
22+
required this.parentDisabled})
23+
: super(key: key);
24+
25+
@override
26+
State<PopupMenuButtonControl> createState() => _PopupMenuButtonControlState();
27+
}
28+
29+
class _PopupMenuButtonControlState extends State<PopupMenuButtonControl> {
30+
@override
31+
void initState() {
32+
super.initState();
33+
}
34+
35+
@override
36+
Widget build(BuildContext context) {
37+
debugPrint("PopupMenuButton build: ${widget.control.id}");
38+
39+
var icon = getMaterialIcon(widget.control.attrString("icon", "")!);
40+
bool disabled = widget.control.isDisabled || widget.parentDisabled;
41+
42+
var popupButton = StoreConnector<AppState, ControlsViewModel>(
43+
distinct: true,
44+
converter: (store) => ControlsViewModel.fromStore(
45+
store, widget.children.map((c) => c.id)),
46+
builder: (content, viewModel) {
47+
return PopupMenuButton<String>(
48+
enabled: !disabled,
49+
icon: icon != null ? Icon(icon) : null,
50+
shape: Theme.of(context).useMaterial3
51+
? RoundedRectangleBorder(
52+
borderRadius: BorderRadius.circular(10))
53+
: null,
54+
onCanceled: () {
55+
ws.pageEventFromWeb(
56+
eventTarget: widget.control.id,
57+
eventName: "cancelled",
58+
eventData: "");
59+
},
60+
onSelected: (itemId) {
61+
ws.pageEventFromWeb(
62+
eventTarget: itemId, eventName: "click", eventData: "");
63+
},
64+
itemBuilder: (BuildContext context) =>
65+
viewModel.controlViews.map((cv) {
66+
var itemIcon =
67+
getMaterialIcon(cv.control.attrString("icon", "")!);
68+
var text = cv.control.attrString("text", "")!;
69+
var checked = cv.control.attrBool("checked");
70+
71+
var contentCtrls =
72+
cv.children.where((c) => c.name == "content");
73+
74+
Widget? child;
75+
if (contentCtrls.isNotEmpty) {
76+
// custom content
77+
child = createControl(cv.control, contentCtrls.first.id,
78+
widget.parentDisabled);
79+
} else if (itemIcon != null && text != "") {
80+
// icon and text
81+
child = Row(children: [
82+
Icon(itemIcon),
83+
const SizedBox(width: 8),
84+
Text(text)
85+
]);
86+
} else if (text != "") {
87+
child = Text(text);
88+
}
89+
90+
var item = checked != null
91+
? CheckedPopupMenuItem<String>(
92+
value: cv.control.id,
93+
checked: checked,
94+
child: child,
95+
)
96+
: PopupMenuItem<String>(
97+
value: cv.control.id, child: child);
98+
99+
return child != null
100+
? item
101+
: const PopupMenuDivider() as PopupMenuEntry<String>;
102+
}).toList());
103+
});
104+
105+
return constrainedControl(popupButton, widget.parent, widget.control);
106+
}
107+
}

client/lib/models/control_type.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ enum ControlType {
1919
outlinedButton,
2020
offstage,
2121
page,
22+
popupMenuButton,
23+
popupMenuItem,
2224
progressBar,
2325
progressRing,
2426
radioGroup,

docs/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* [x] TextButton
3636
* [x] IconButton
3737
* [x] FloatingActionButton
38-
* [ ] PopupMenuButton
38+
* [x] PopupMenuButton
3939
* Input and selections
4040
* [x] TextField
4141
* [x] Dropdown

sdk/python/flet/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from flet.list_view import ListView
2020
from flet.outlined_button import OutlinedButton
2121
from flet.page import Page
22+
from flet.popup_menu_button import PopupMenuButton, PopupMenuItem
2223
from flet.progress_bar import ProgressBar
2324
from flet.progress_ring import ProgressRing
2425
from flet.radio import Radio
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from typing import Optional, Union
2+
3+
from beartype import beartype
4+
from beartype.typing import List
5+
6+
from flet.constrained_control import ConstrainedControl
7+
from flet.control import Control, OptionalNumber
8+
from flet.ref import Ref
9+
10+
11+
class PopupMenuItem(Control):
12+
def __init__(
13+
self,
14+
ref: Ref = None,
15+
checked: bool = None,
16+
icon: str = None,
17+
text: str = None,
18+
content: Control = None,
19+
on_click=None,
20+
):
21+
Control.__init__(self, ref=ref)
22+
23+
self.checked = checked
24+
self.icon = icon
25+
self.text = text
26+
self.__content: Control = None
27+
self.content = content
28+
self.on_click = on_click
29+
30+
def _get_control_name(self):
31+
return "popupmenuitem"
32+
33+
def _get_children(self):
34+
children = []
35+
if self.__content:
36+
self.__content._set_attr_internal("n", "content")
37+
children.append(self.__content)
38+
return children
39+
40+
# checked
41+
@property
42+
def checked(self):
43+
return self._get_attr("checked", data_type="bool")
44+
45+
@checked.setter
46+
@beartype
47+
def checked(self, value: Optional[bool]):
48+
self._set_attr("checked", value)
49+
50+
# icon
51+
@property
52+
def icon(self):
53+
return self._get_attr("icon")
54+
55+
@icon.setter
56+
def icon(self, value):
57+
self._set_attr("icon", value)
58+
59+
# text
60+
@property
61+
def text(self):
62+
return self._get_attr("text")
63+
64+
@text.setter
65+
def text(self, value):
66+
self._set_attr("text", value)
67+
68+
# content
69+
@property
70+
def content(self):
71+
return self.__content
72+
73+
@content.setter
74+
def content(self, value):
75+
self.__content = value
76+
77+
# on_click
78+
@property
79+
def on_click(self):
80+
return self._get_event_handler("click")
81+
82+
@on_click.setter
83+
def on_click(self, handler):
84+
self._add_event_handler("click", handler)
85+
86+
87+
class PopupMenuButton(ConstrainedControl):
88+
def __init__(
89+
self,
90+
ref: Ref = None,
91+
width: OptionalNumber = None,
92+
height: OptionalNumber = None,
93+
expand: Union[bool, int] = None,
94+
opacity: OptionalNumber = None,
95+
tooltip: str = None,
96+
visible: bool = None,
97+
disabled: bool = None,
98+
data: any = None,
99+
#
100+
# PopupMenuButton-specific
101+
items: List[PopupMenuItem] = None,
102+
icon: str = None,
103+
on_cancelled=None,
104+
):
105+
106+
ConstrainedControl.__init__(
107+
self,
108+
ref=ref,
109+
width=width,
110+
height=height,
111+
expand=expand,
112+
opacity=opacity,
113+
tooltip=tooltip,
114+
visible=visible,
115+
disabled=disabled,
116+
data=data,
117+
)
118+
119+
self.items = items
120+
self.icon = icon
121+
self.on_cancelled = on_cancelled
122+
123+
def _get_control_name(self):
124+
return "popupmenubutton"
125+
126+
def _get_children(self):
127+
return self.__items
128+
129+
# items
130+
@property
131+
def items(self):
132+
return self.__items
133+
134+
@items.setter
135+
@beartype
136+
def items(self, value: Optional[List[PopupMenuItem]]):
137+
value = value or []
138+
self.__items = value
139+
140+
# on_cancelled
141+
@property
142+
def on_cancelled(self):
143+
return self._get_event_handler("cancelled")
144+
145+
@on_cancelled.setter
146+
def on_cancelled(self, handler):
147+
self._add_event_handler("cancelled", handler)
148+
149+
# icon
150+
@property
151+
def icon(self):
152+
return self._get_attr("icon")
153+
154+
@icon.setter
155+
def icon(self, value):
156+
self._set_attr("icon", value)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import flet
2+
from flet import Icon, Page, PopupMenuButton, PopupMenuItem, Row, Text, icons
3+
4+
5+
def main(page: Page):
6+
def check_item_clicked(e):
7+
e.control.checked = not e.control.checked
8+
page.update()
9+
10+
pb = PopupMenuButton(
11+
items=[
12+
PopupMenuItem(text="Item 1"),
13+
PopupMenuItem(icon=icons.POWER_INPUT, text="Check power"),
14+
PopupMenuItem(
15+
content=Row(
16+
[
17+
Icon(icons.HOURGLASS_TOP_OUTLINED),
18+
Text("Item with a custom content"),
19+
]
20+
),
21+
on_click=lambda _: print("Button with a custom content clicked!"),
22+
),
23+
PopupMenuItem(), # divider
24+
PopupMenuItem(
25+
text="Checked item", checked=False, on_click=check_item_clicked
26+
),
27+
]
28+
)
29+
page.add(pb)
30+
31+
32+
flet.app(name="test1", port=8550, target=main, view=flet.WEB_BROWSER)

0 commit comments

Comments
 (0)