-
-
Notifications
You must be signed in to change notification settings - Fork 663
/
menu.py
executable file
·395 lines (321 loc) · 11.7 KB
/
menu.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
"""
Menus
=====
Copyright (c) 2015 Andrés Rodríguez and KivyMD contributors -
KivyMD library up to version 0.1.2
Copyright (c) 2019 Ivanov Yuri and KivyMD contributors -
KivyMD library version 0.1.3 and higher
For suggestions and questions:
<kivydevelopment@gmail.com>
This file is distributed under the terms of the same license,
as the Kivy framework.
`Material Design spec, Menus <https://material.io/design/components/menus.html>`_
Example
-------
from kivy.app import App
from kivy.lang import Builder
from kivy.factory import Factory
from kivymd.theming import ThemeManager
from kivymd.toast import toast
Builder.load_string('''
# Here a compulsory import
#:import MDDropdownMenu kivymd.uix.menu.MDDropdownMenu
<Menu@Screen>
MDRaisedButton:
size_hint: None, None
size: 3 * dp(48), dp(48)
text: 'Open menu'
opposite_colors: True
pos_hint: {'center_x': .2, 'center_y': .9}
on_release: MDDropdownMenu(items=app.menu_items, width_mult=3).open(self)
MDRaisedButton:
size_hint: None, None
size: 3 * dp(48), dp(48)
text: 'Open menu'
opposite_colors: True
pos_hint: {'center_x': .2, 'center_y': .1}
on_release:
MDDropdownMenu(items=app.menu_items, width_mult=3).open(self)
MDRaisedButton:
size_hint: None, None
size: 3 * dp(48), dp(48)
text: 'Open menu'
opposite_colors: True
pos_hint: {'center_x': .8, 'center_y': .1}
on_release: MDDropdownMenu(items=app.menu_items, width_mult=3).open(self)
MDRaisedButton:
size_hint: None, None
size: 3 * dp(48), dp(48)
text: 'Open menu'
opposite_colors: True
pos_hint: {'center_x': .8, 'center_y': .9}
on_release: MDDropdownMenu(items=app.menu_items, width_mult=3).open(self)
MDRaisedButton:
size_hint: None, None
size: 3 * dp(48), dp(48)
text: 'Open menu'
opposite_colors: True
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: MDDropdownMenu(items=app.menu_items, width_mult=4).open(self)
''')
class Test(App):
theme_cls = ThemeManager()
menu_items = []
def callback_for_menu_items(self, *args):
toast(args[0])
def build(self):
self.menu_items = [
{
"viewclass": "MDMenuItem",
"text": "Example item %d" % i,
"callback": self.callback_for_menu_items,
}
for i in range(15)
]
return Factory.Menu()
Test().run()
"""
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.metrics import dp
from kivy.properties import (
NumericProperty,
ListProperty,
OptionProperty,
StringProperty,
BooleanProperty,
)
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
import kivymd.material_resources as m_res
from kivymd.theming import ThemableBehavior
Builder.load_string(
"""
#:import STD_INC kivymd.material_resources.STANDARD_INCREMENT
<MDMenuItem>
size_hint: None, None
height: dp(48)
padding: dp(16), 0
# Horrible, but hey it works.
on_release:
root.parent.parent.parent.parent.dispatch("on_dismiss")
root.callback(root.text)
Label:
id: item_text
text: root.text
markup: True
#font_size: '14sp'
size_hint_x: None
width: self.texture_size[0]
halign: 'left'
<MDMenu>
size_hint: None, None
width: root.width_mult * STD_INC
key_viewclass: 'viewclass'
key_size: 'height'
RecycleBoxLayout:
default_size: None, dp(48)
default_size_hint: 1, None
orientation: 'vertical'
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
<MDDropdownMenu>
FloatLayout:
id: fl
MDMenu:
id: md_menu
data: root.items
width_mult: root.width_mult
size_hint: None, None
size: 0, 0
canvas.before:
Color:
rgba: root.background_color
Rectangle:
size: self.size
pos: self.pos
canvas.after:
Color:
rgba: root.color_rectangle
Line:
width: dp(root.width_rectangle)
points:
(
self.x, self.y,
self.right, self.y,
self.right, self.top,
self.x, self.top,
self.x, self.y
)
"""
)
class MDMenuItem(RecycleDataViewBehavior, ButtonBehavior, BoxLayout):
text = StringProperty()
class MDMenu(RecycleView):
width_mult = NumericProperty(1)
class MDDropdownMenu(ThemableBehavior, BoxLayout):
items = ListProperty()
"""See :attr:`~kivy.uix.recycleview.RecycleView.data`
"""
width_mult = NumericProperty(1)
"""This number multiplied by the standard increment (56dp on mobile,
64dp on desktop, determines the width of the menu items.
If the resulting number were to be too big for the application Window,
the multiplier will be adjusted for the biggest possible one.
"""
max_height = NumericProperty()
"""The menu will grow no bigger than this number.
Set to 0 for no limit. Defaults to 0.
"""
border_margin = NumericProperty(dp(4))
"""Margin between Window border and menu
"""
ver_growth = OptionProperty(None, allownone=True, options=["up", "down"])
"""Where the menu will grow vertically to when opening
Set to None to let the widget pick for you. Defaults to None.
"""
hor_growth = OptionProperty(None, allownone=True, options=["left", "right"])
"""Where the menu will grow horizontally to when opening
Set to None to let the widget pick for you. Defaults to None.
"""
background_color = ListProperty()
"""Color of the background of the menu
"""
color_rectangle = ListProperty()
"""Color of the rectangle of the menu
"""
width_rectangle = NumericProperty(2)
"""Width of the rectangle of the menu
"""
_center = BooleanProperty(False)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.register_event_type("on_dismiss")
if not len(self.background_color):
self.background_color = self.theme_cls.primary_color
if not len(self.color_rectangle):
self.color_rectangle = self.theme_cls.divider_color
def open(self, *args):
if self.parent:
self.parent.remove_widget(self)
Window.add_widget(self)
Clock.schedule_once(lambda x: self.display_menu(args[0]), -1)
def display_menu(self, caller):
# We need to pick a starting point, see how big we need to be,
# and where to grow to.
c = caller.to_window(
caller.center_x, caller.center_y
) # Starting coords
# TODO: ESTABLISH INITIAL TARGET SIZE ESTIMATE
target_width = self.width_mult * m_res.STANDARD_INCREMENT
# md_menu = self.ids.md_menu
# opts = md_menu.layout_manager.view_opts
# md_item = md_menu.view_adapter.get_view(1, md_menu.data[1],
# opts[1]['viewclass'])
# If we're wider than the Window...
if target_width > Window.width:
# ...reduce our multiplier to max allowed.
target_width = (
int(Window.width / m_res.STANDARD_INCREMENT)
* m_res.STANDARD_INCREMENT
)
target_height = sum([dp(48) for i in self.items])
# If we're over max_height...
if 0 < self.max_height < target_height:
target_height = self.max_height
# ---ESTABLISH VERTICAL GROWTH DIRECTION---
if self.ver_growth is not None:
ver_growth = self.ver_growth
else:
# If there's enough space below us:
if target_height <= c[1] - self.border_margin:
ver_growth = "down"
# if there's enough space above us:
elif target_height < Window.height - c[1] - self.border_margin:
ver_growth = "up"
# otherwise, let's pick the one with more space and adjust ourselves
else:
# if there's more space below us:
if c[1] >= Window.height - c[1]:
ver_growth = "down"
target_height = c[1] - self.border_margin
# if there's more space above us:
else:
ver_growth = "up"
target_height = Window.height - c[1] - self.border_margin
if self.hor_growth is not None:
hor_growth = self.hor_growth
else:
# If there's enough space to the right:
if target_width <= Window.width - c[0] - self.border_margin:
hor_growth = "right"
# if there's enough space to the left:
elif target_width < c[0] - self.border_margin:
hor_growth = "left"
# otherwise, let's pick the one with more space and adjust ourselves
else:
# if there's more space to the right:
if Window.width - c[0] >= c[0]:
hor_growth = "right"
target_width = Window.width - c[0] - self.border_margin
# if there's more space to the left:
else:
hor_growth = "left"
target_width = c[0] - self.border_margin
if ver_growth == "down":
tar_y = c[1] - target_height
else: # should always be 'up'
tar_y = c[1]
if hor_growth == "right":
tar_x = c[0]
else: # should always be 'left'
tar_x = c[0] - target_width
menu = self.ids.md_menu
if not self._center:
anim = Animation(
x=tar_x,
y=tar_y,
width=target_width,
height=target_height,
duration=0.3,
transition="out_quint",
)
menu.pos = c
anim.start(menu)
else:
menu.width = target_width
menu.height = target_height
menu.pos = (c[0] - target_width / 2, c[1] - target_height / 2)
# TODO: Add the ability to set the list to the current user selection.
"""
for data in menu.data:
if data["text"] == caller.ids.label_item.text:
opts = menu.layout_manager.view_opts
item = menu.view_adapter.get_view(1, data, opts[1]["viewclass"])
# AttributeError: 'function' object has no attribute 'is_triggered'
# https://github.com/kivy/kivy/issues/5014
# Attempt to fix - https://github.com/Bakterija/log_fruit/blob/dev/src/app_modules/widgets/app_recycleview/recycleview.py#L25-L34
menu.scroll_to(item)
break
"""
def on_touch_down(self, touch):
if not self.ids.md_menu.collide_point(*touch.pos):
self.dispatch("on_dismiss")
return True
super().on_touch_down(touch)
return True
def on_touch_move(self, touch):
super().on_touch_move(touch)
return True
def on_touch_up(self, touch):
super().on_touch_up(touch)
return True
def on_dismiss(self):
Window.remove_widget(self)
def dismiss(self):
self.on_dismiss()