-
Notifications
You must be signed in to change notification settings - Fork 1
/
menu.py
388 lines (323 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
"""
Python Menu class
David Wahlund 2019
"""
# TODO
# -reserved nav keys (eg. p,n,u,-,0)
import os
__version__ = '190923.1'
class MenuItem:
"""The base class of all menu items
Args:
label (str): Text shown on the menu item
"""
def __init__(self, label):
self.label = label
class NoNavItem(MenuItem): # Item outside of navigation
"""Base class for all classes that are outside of navigation"""
pass
class SpaceItem(NoNavItem):
"""Just adds an empty row in the list of items"""
def __init__(self):
super().__init__('')
class TextItem(NoNavItem):
"""A simple text label in the menu
Args:
label (str): Text shown on the menu item
"""
def __init__(self, label):
super().__init__(label)
class NavItem(MenuItem):
"""Base class for all navigationitems
Args:
label (str): Text shown on the menu item
Attributes:
key (str): Shortcut for keyboard navigation
"""
def __init__(self, label):
super().__init__(label)
self.key = ''
self._msg = ''
def has_msg(self):
"""Check if there is a message to show"""
return self._msg != ''
@property
def msg(self):
msg = self._msg
self._msg = ''
return msg
@msg.setter
def msg(self, msg):
self._msg = msg
class ActionItem(NavItem):
"""Item calling a function with args and kwargs
Attributes:
msg (str): optional msg from the function
action_func: reference to function to be called
"""
def __init__(self, label, action_func, *args, **kwargs):
super().__init__(label)
self.action_func = action_func
self.args = args
self.kwargs = kwargs
def action(self):
self.action_func(self, *self.args, **self.kwargs)
class SubMenu(NavItem):
"""Base class for the hierarchical menu data model
Args:
label (str): Text shown on the menu item
title (str): Text shown as title above the menu text
text (str): Text show at the top of the menu
Attributes:
items (SubMenu): A list of child SubMenu items
"""
def __init__(self, label, title='', text=''):
super().__init__(label)
self.itemsCount = 1
self.items = []
self.title = title if title else label
self.text = text # TODO replace with TextItem only
self.parent = None
def add_space_item(self):
self.items.append(SpaceItem())
def add_text_item(self, text, space_above=False):
if space_above: # It's common to add space above titles and labels
self.add_space_item()
self.items.append(TextItem(text))
def add_item(self, item, key=''):
if not hasattr(item, 'parent') or not item.parent:
item.parent = self
if isinstance(item, NoNavItem):
self.items.append(item)
elif key:
item.key = key
self.items.append(item)
else:
item.key = str(self.itemsCount)
self.items.append(item)
self.itemsCount += 1
def clear_items(self):
self.itemsCount = 1
self.items = []
def nav_items(self):
return [i for i in self.items if isinstance(i, NavItem)]
class DynMenu(SubMenu):
"""A dynamic menu generated by a function"""
def __init__(self, label, init_func, *args, **kwargs):
super(DynMenu, self).__init__(label)
self.init_func = init_func
self.updatable = False
self.args = args
self.kwargs = kwargs
def init_menu(self):
"""Call init_func with referenc do self, args and kwargs"""
self.init_func(self, *self.args, **self.kwargs)
def reset(self):
"""Clear all items and call init_func again"""
self.clear_items()
self.text = ''
self.init_menu()
class SearchMenu(DynMenu):
"""A dynamic menu generated from a function by passing a parameter"""
def __init__(self, label, search_func, *args, **kwargs):
super(SearchMenu, self).__init__(label, None, *args, **kwargs)
self.searchFunc = search_func
def init_menu(self):
pass
def search(self, ans):
return self.searchFunc(self, ans, *self.args, **self.kwargs)
class PageMenu(DynMenu):
"""Dynamic menu split upp into pages with prev/next navigation"""
def __init__(self, label, item_init, idx=0, pages=None, title_func=None, *args, **kwargs):
super(PageMenu, self).__init__(label, None, *args, **kwargs)
if pages is None:
pages = []
self.pages = pages
self.idx = idx
self.item_init = item_init
self.length = len(pages)
self.title_func = title_func
# override DynMenu init
def init_menu(self):
# call init for listitem
page = self.pages[self.idx]
if self.title_func:
self.title = self.title_func(page)
else:
self.title = self.label
self.item_init(self, page, *self.args, **self.kwargs)
class ListMenu(PageMenu):
"""Generete a PageMenu from a list of items and a page size"""
def __init__(self, label, item_init, list_items, size=15, label_func=None, title_func=None, *args, **kwargs):
pages = []
for i in range(0, len(list_items), size):
pages.append(list_items[i:i + size])
super(ListMenu, self).__init__(label, None, 0, pages, *args, **kwargs)
self.list = list_items
self.size = size
self.listLength = len(list_items)
self.item_init = item_init
self.label_func = label_func
self.title_func = title_func
def page_len(self):
return len(self.pages[self.idx])
def init_menu(self):
page = self.pages[self.idx] # list of items
i = 0
for page_item in page:
# lbl = ''
if self.label_func:
lbl = self.label_func(page_item)
else:
lbl = str(page_item)
idx = self.idx * self.size + i
item = DynMenu(lbl, self.item_init, self.list[idx])
if self.title_func:
item.title = self.title_func(page_item)
else:
item.title = str(i)
self.add_item(item)
i += 1
class Menu(object):
"""
Class for traversing an hierarchical menu structure
Args:
root_menu: A special SubMenu that acts as the root menu
cls: Clears the screen between menus
menu: The current shown menu
"""
def __init__(self, title, cls=True):
self._root_menu = SubMenu(title)
self._root_menu.parent = None
self._cls = cls
self._current_menu = self._root_menu
def add_item(self, item, key=None):
"""Adds an item to the root menu"""
self._root_menu.add_item(item, key)
def reset(self):
"""Resets the current menu"""
if isinstance(self._current_menu, DynMenu):
self._current_menu.reset()
def show(self):
"""Show the current menu"""
if self._cls:
os.system('cls')
if not isinstance(self._current_menu, SubMenu):
raise Exception("self.menu not SubMenu")
if isinstance(self._current_menu, ListMenu):
firstidx = self._current_menu.idx * self._current_menu.size
first = firstidx + 1
last = firstidx + self._current_menu.page_len()
print("### %s [%s/%s] [%s-%s/%s] ###" % (
self._current_menu.title, self._current_menu.idx + 1, self._current_menu.length, first, last, self._current_menu.listLength))
elif isinstance(self._current_menu, PageMenu):
print("### %s [%s/%s] ###" % (self._current_menu.title, self._current_menu.idx + 1, self._current_menu.length))
else:
print("### %s ###" % self._current_menu.title)
if self._current_menu.has_msg():
print("{%s}" % (self._current_menu.msg.upper()))
print() # newline after title
if self._current_menu.text:
print("ALL YOUR TEXT ARE BELONG TO US!!! (use TextItem instead)") # everything is a textitem now
itemcount = len(self._current_menu.nav_items())
key_offset = 1
if itemcount >= 100:
key_offset = 3
elif itemcount >= 10:
key_offset = 2
format_str = "{:%sd}: [{}]" % key_offset
for item in self._current_menu.items:
if type(item) is SpaceItem:
print()
elif type(item) is TextItem:
print("%s" % item.label)
else:
if item.key.isdigit():
print(format_str.format(int(item.key), item.label))
# print("%s%s: [%s]" % (' '*(key_offset - len(item.key) - 1),item.key,item.label))
else:
print("%s: [%s]" % (item.key, item.label))
print()
if isinstance(self._current_menu, PageMenu):
print("p: [Previous]")
print("n: [Next]")
print("o: [Go to page]\n")
if isinstance(self._current_menu, DynMenu) and self._current_menu.updatable:
print("u: [Update]\n")
if self._current_menu.parent is not None:
print("-: [Back]")
print("0: [Home]")
print("q: [Quit]")
def read(self):
"""Waits for user input"""
ans = input('\n#?: ')
print()
print() # create space if cls = False
# Navigation
if ans == 'q':
self._current_menu = None
return
elif ans == '0':
self._current_menu = self._root_menu
return
elif ans == '-':
self._current_menu = self._current_menu.parent
self.reset()
return
elif ans == 'u' and isinstance(self._current_menu, DynMenu) and self._current_menu.updatable:
self.reset()
return
elif isinstance(self._current_menu, PageMenu):
if ans == 'p':
if self._current_menu.idx > 0:
self._current_menu.idx -= 1
self.reset()
return
elif ans == 'n':
if self._current_menu.idx < len(self._current_menu.pages) - 1:
self._current_menu.idx += 1
self.reset()
return
elif ans == 'o':
pi = input('#num?: ')
if pi.isdigit():
pi = int(pi)
if 0 < pi <= len(self._current_menu.pages):
self._current_menu.idx = pi - 1
self.reset()
return
# Selection
item = None
if type(self._current_menu) is SearchMenu:
item = self._current_menu.search(ans)
if not item: # return None if search failed
return
else:
nav_items = self._current_menu.nav_items()
if ans in [i.key for i in nav_items]:
for i in nav_items:
if i.key == ans:
item = i
break
else:
return
if type(item) is ActionItem:
item.action()
self.reset()
self._current_menu.msg = item.msg
# Maybe change current menu
if item is not None:
if type(item) is SubMenu:
self._current_menu = item
elif isinstance(item, DynMenu):
self._current_menu = item
self.reset()
return
def run(self):
"""Main loop that shows the current menu and waits for input"""
run = True
while run:
self.show()
self.read()
if self._current_menu is None:
run = False