A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1--[[
2/***************************************************************************
3 * __________ __ ___.
4 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
5 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
6 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
7 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
8 * \/ \/ \/ \/ \/
9 * $Id$
10 *
11 * Copyright (C) 2021 William Wilgus
12 *
13 * This program is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU General Public License
15 * as published by the Free Software Foundation; either version 2
16 * of the License, or (at your option) any later version.
17 *
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
20 *
21 ****************************************************************************/
22]]
23if not rb.lcd_framebuffer then rb.splash(rb.HZ, "No Support!") return nil end
24--GLOBALS
25menu_ctx = {}
26submenu_insert = table.insert
27submenu_remove = table.remove
28
29local last_ctx = false
30local p_settings
31local function empty_fn() end
32
33--[[root menu tables
34expanded menus get inserted / removed
35and context menus replace them but unless you
36want a new root menu they are never overwritten
37func_t functions get 3 variables passed by the menu_system
38func_t[i] =function sample(i, menu_t, func_t]
39this function gets run on user selection
40for every function in func_t:
41'i' is the selected item
42'menu_t' is the current strings table
43'func_t' is the current function table
44
45menu_t[i] will returnm the text of the item user selected and
46func_t[i] will return the function we are currently in
47]]
48
49menu_t = {}
50func_t = {}
51
52require("printmenus")
53
54local BUTTON = require("menubuttons")
55local last_sel = 0
56
57local function display_context_menu() end -- forward declaration
58
59local function dpad(x, xi, xir, y, yi, yir, timeout, overflow, selected)
60 local scroll_is_fixed = overflow ~= "manual"
61 if timeout == nil then timeout = -1 end
62 local cancel, select = 0, 0
63 local x_chg, y_chg = 0, 0
64 local button
65 while true do
66 button = rb.get_plugin_action(timeout)
67
68 if button == BUTTON.CANCEL then
69 cancel = 1
70 break;
71 elseif button == BUTTON.EXIT then
72 cancel = 1
73 break;
74 elseif button == BUTTON.SEL then
75 last_sel = 1
76 timeout = timeout + 1
77 elseif button == BUTTON.SELR then
78 last_sel = 2
79 if display_context_menu(selected or -1) == true then
80 select = 1
81 break;
82 end
83 timeout = timeout + 1
84 elseif button == BUTTON.SELREL then
85 if last_sel == 1 then
86 select = 1
87 end
88 last_sel = 0
89 timeout = timeout + 1
90 elseif button == BUTTON.LEFT then
91 x_chg = x_chg - xi
92 if scroll_is_fixed then
93 cancel = 1
94 break;
95 end
96 elseif button == BUTTON.LEFTR then
97 x_chg = x_chg - xir
98 elseif button == BUTTON.RIGHT then
99 x_chg = x_chg + xi
100 if scroll_is_fixed then
101 select = 1
102 timeout = timeout + 1
103 end
104 elseif button == BUTTON.RIGHTR then
105 x_chg = x_chg + xir
106 elseif button == BUTTON.UP then
107 y_chg = y_chg + yi
108 elseif button == BUTTON.UPR then
109 y_chg = y_chg + yir
110 elseif button == BUTTON.DOWN then
111 y_chg = y_chg - yi
112 elseif button == BUTTON.DOWNR then
113 y_chg = y_chg - yir
114 elseif timeout >= 0 then--and rb.button_queue_count() < 1 then
115 break;
116 end
117
118 if x_chg ~= 0 or y_chg ~= 0 then
119 timeout = timeout + 1
120 end
121 end
122
123 x = x + x_chg
124 y = y + y_chg
125
126 return cancel, select, x_chg, x, y_chg, y, 0xffff
127end -- dpad
128
129local function ctx_loop()
130 local loopfn = ctx_loop
131 ctx_loop = empty_fn() --prevent another execution
132 local mt, ft = get_menu()
133 local i
134 repeat
135 if menu_ctx.update then mt, ft = get_menu(); menu_ctx.update = false end
136 _, i = print_menu(mt, ft, menu_ctx.start, p_settings)
137 until menu_ctx.quit
138
139 ctx_loop = loopfn --restore for another run
140end
141
142--[[ push_ctx() save surrent menu and load another ]]
143local function push_ctx(new_getmenu)
144 last_ctx = last_ctx or {}
145 submenu_insert(last_ctx, menu_ctx)
146 menu_ctx.getmenu = get_menu
147 menu_ctx.settings = p_settings
148 --menu_ctx is a new variable after this point
149 submenu_set_defaults()
150 menu_ctx.update = true
151 if type(new_getmenu) == 'function' then
152 get_menu = new_getmenu
153 end
154end
155
156--[[ pop_ctx() restore last menu ]]
157local function pop_ctx()
158 menu_ctx = submenu_remove(last_ctx)
159 if menu_ctx then
160 get_menu = menu_ctx.getmenu
161 p_settings = menu_ctx.settings
162 if menu_ctx.restorefn then
163 menu_ctx.restorefn(menu_t, func_t)
164 menu_ctx.restorefn = nil
165 end
166 menu_ctx.getmenu = nil
167 menu_ctx.settings = nil
168 menu_ctx.update = true
169 return true
170 end
171end
172
173--[[ display_context_menu_internal() supplies a new get_menu function that returns
174 the context menu 'user_context_fn' supplied by set_menu()
175 this menu is immediately displayed and when finished will
176 automatically restore the last menu
177]]
178local function display_context_menu_internal(sel)
179 if sel <= 0 or not menu_ctx.user_context_fn then return false end
180 local parent = submenu_get_parent() or 0
181 local user_context_fn = menu_ctx.user_context_fn
182
183 local function display_context_menu(i, menu_t, func_t)
184 local function new_getmenu()
185 local mt, ft = user_context_fn(parent, i, menu_t, func_t)
186 ft[0] = pop_ctx --set back fn
187 return mt, ft
188 end
189 push_ctx(new_getmenu)
190 return true
191 end
192
193 --save the current function in closure restore_fn for later
194 local funct = func_t[sel]
195 local function restore_fn(mt, ft)
196 ft[sel] = funct
197 menu_ctx.start = sel - 1
198 end
199
200 menu_ctx.restorefn = restore_fn
201 -- insert into the current fn table so it gets execd by the menu
202 func_t[sel] = display_context_menu
203
204 return true
205end
206
207--[[ submenu_get_parent() gets the parent of the top most level
208 if lv is supplied it instead gets the parent of that level ]]
209function submenu_get_parent(lv)
210 lv = lv or #menu_ctx.collapse_fn or 1
211 collectgarbage("step")
212 local t = menu_ctx.collapse_fn[lv] or {}
213 return t[2] or -1, lv
214end
215
216--[[ submenu_collapse() collapses submenu till level or ROOT is reached ]]
217function submenu_collapse(parent, lv)
218 local lv_out, menu_sz = 0, 0
219 local item_out = -1
220 local items_removed = 0
221 if lv <= #menu_ctx.collapse_fn then
222 repeat
223 local collapse_fn = submenu_remove(menu_ctx.collapse_fn)
224 if collapse_fn then
225 lv_out, item_out, menu_sz = collapse_fn[1](parent, menu_t, func_t)
226 items_removed = items_removed + menu_sz
227 end
228
229 until not collapse_fn or lv >= lv_out
230 end
231 return lv_out, item_out, items_removed
232end
233
234--[[ submenu_create() supply level of submenu > 0, ROOT is lv 0
235 supply menu strings table and function table
236 closure returned run this function to expand the menu
237]]
238function submenu_create(lv, mt, ft)
239 if lv < 1 then error("Level < 1") end
240 if type(mt) ~= 'table' or type(ft) ~= 'table' then
241 error("mt and ft must be tables")
242 end
243
244 -- everything in lua is 1 based menu level is no exception
245 local lv_tab = string.rep ("\t", lv)
246 local function submenu_closure(i, m, f)
247 menu_ctx.lv = lv
248 local lv_out, menusz_out, start_item
249 local item_in, item_out = i, i
250 if lv <= #menu_ctx.collapse_fn then --something else expanded??
251 repeat
252 local collapse_fn = submenu_remove(menu_ctx.collapse_fn)
253 if collapse_fn then
254 lv_out, item_out, menusz_out = collapse_fn[1](i, m, f)
255 -- if the item i is below this menu, it needs to shift too
256 if item_in > item_out then i = i - (menusz_out) end
257 end
258 until not collapse_fn or lv >= lv_out
259 menu_ctx.start = i
260 if item_out == item_in then return end
261 end
262
263 local menu_sz = #mt
264 menu_ctx.start = i
265 start_item = i
266 menu_ctx.update = true
267 for item, _ in ipairs(mt) do
268 i = i + 1
269 submenu_insert(m, i, lv_tab .. mt[item])
270 submenu_insert(f, i, ft[item])
271 end
272
273 local function collapse_closure(i, m, f)
274 --creates a closure around lv, start_item and menu_sz
275 for j = 1, menu_sz, 1 do
276 submenu_remove(m, start_item + 1)
277 submenu_remove(f, start_item + 1)
278 end
279 return lv, start_item, menu_sz
280 end
281
282 submenu_insert(menu_ctx.collapse_fn, lv, {collapse_closure, start_item})
283 return true
284 end
285
286 return submenu_closure
287end
288
289--
290function submenu_set_defaults(settings, ctx)
291 p_settings = settings or {wrap = true, hasheader = true, justify = "left", dpad_fn = dpad}
292 menu_ctx = ctx or {collapse_fn = {}, lv = 0, update = false, start = 1}
293end
294
295--[[ get_menu() returns the ROOT string and fn tables]]
296function get_menu()
297 return menu_t, func_t
298end
299
300--[[ set_menu() set your menu and the menu has now been entered ]]
301function set_menu(mt, ft, user_context_fn, settings)
302
303 submenu_set_defaults(settings)
304 if type(user_context_fn) == 'function' then
305 display_context_menu = display_context_menu_internal
306 menu_ctx.user_context_fn = user_context_fn
307 else
308 display_context_menu = empty_fn
309 menu_ctx.user_context_fn = false
310 end
311 p_settings = settings or p_settings
312 menu_t, func_t = mt, ft
313 ctx_loop()
314end