@@ -4,10 +4,10 @@ local Element = require('elements/Element')
44--- @alias MenuData { type ?: string ; title ?: string ; hint ?: string ; keep_open ?: boolean ; separator ?: boolean ; items ?: MenuDataItem[] ; selected_index ?: integer ;}
55--- @alias MenuDataItem MenuDataValue | MenuData
66--- @alias MenuDataValue { title ?: string ; hint ?: string ; icon ?: string ; value : any ; bold ?: boolean ; italic ?: boolean ; muted ?: boolean ; active ?: boolean ; keep_open ?: boolean ; separator ?: boolean ;}
7- --- @alias MenuOptions { mouse_nav ?: boolean ; on_open ?: fun () ; on_close ?: fun () ; on_back ?: fun ()}
7+ --- @alias MenuOptions { mouse_nav ?: boolean ; on_open ?: fun () ; on_close ?: fun () ; on_back ?: fun () ; on_move_item ?: fun ( from_index : integer , to_index : integer , submenu_path : integer[] ) ; on_delete_item ?: fun ( index : integer , submenu_path : integer[] ) }
88
99-- Internal data structure created from `Menu`.
10- --- @alias MenuStack { id ?: string ; type ?: string ; title ?: string ; hint ?: string ; selected_index ?: number ; keep_open ?: boolean ; separator ?: boolean ; items : MenuStackItem[] ; parent_menu ?: MenuStack ; active ?: boolean ; width : number ; height : number ; top : number ; scroll_y : number ; scroll_height : number ; title_width : number ; hint_width : number ; max_width : number ; is_root ?: boolean ; fling ?: Fling }
10+ --- @alias MenuStack { id ?: string ; type ?: string ; title ?: string ; hint ?: string ; selected_index ?: number ; keep_open ?: boolean ; separator ?: boolean ; items : MenuStackItem[] ; parent_menu ?: MenuStack ; submenu_path : integer[] ; active ?: boolean ; width : number ; height : number ; top : number ; scroll_y : number ; scroll_height : number ; title_width : number ; hint_width : number ; max_width : number ; is_root ?: boolean ; fling ?: Fling }
1111--- @alias MenuStackItem MenuStackValue | MenuStack
1212--- @alias MenuStackValue { title ?: string ; hint ?: string ; icon ?: string ; value : any ; active ?: boolean ; bold ?: boolean ; italic ?: boolean ; muted ?: boolean ; keep_open ?: boolean ; separator ?: boolean ; title_width : number ; hint_width : number }
1313--- @alias Fling { y : number , distance : number , time : number , easing : fun ( x : number ), duration : number , update_cursor ?: boolean }
@@ -56,6 +56,7 @@ function Menu:close(immediate, callback)
5656 menu .is_closing , menu .stack , menu .current , menu .all , menu .by_id = false , nil , nil , {}, {}
5757 menu :disable_key_bindings ()
5858 Elements :update_proximities ()
59+ cursor .queue_autohide ()
5960 if callback then callback () end
6061 request_render ()
6162 end
135136function Menu :update (data )
136137 self .type = data .type
137138
138- local new_root = {is_root = true }
139+ local new_root = {is_root = true , submenu_path = {} }
139140 local new_all = {}
140141 local new_by_id = {}
141142 local menus_to_serialize = {{new_root , data }}
@@ -169,6 +170,7 @@ function Menu:update(data)
169170 -- Submenu
170171 if item_data .items then
171172 item .parent_menu = menu
173+ item .submenu_path = itable_join (menu .submenu_path , {i })
172174 menus_to_serialize [# menus_to_serialize + 1 ] = {item , item_data }
173175 end
174176
@@ -210,8 +212,10 @@ function Menu:update_content_dimensions()
210212 local hint_opts = {size = self .font_size_hint }
211213
212214 for _ , menu in ipairs (self .all ) do
215+ title_opts .bold , title_opts .italic = true , false
216+ local max_width = text_width (menu .title , title_opts ) + 2 * self .item_padding
217+
213218 -- Estimate width of a widest item
214- local max_width = 0
215219 for _ , item in ipairs (menu .items ) do
216220 local icon_width = item .icon and self .font_size or 0
217221 item .title_width = text_width (item .title , title_opts )
@@ -223,11 +227,6 @@ function Menu:update_content_dimensions()
223227 if estimated_width > max_width then max_width = estimated_width end
224228 end
225229
226- -- Also check menu title
227- title_opts .bold , title_opts .italic = true , false
228- local menu_title_width = text_width (menu .title , title_opts )
229- if menu_title_width > max_width then max_width = menu_title_width end
230-
231230 menu .max_width = max_width
232231 end
233232
236235
237236function Menu :update_dimensions ()
238237 -- Coordinates and sizes are of the scrollable area to make
239- -- consuming values in rendering and collisions easier. Title drawn above this, so
240- -- we need to account for that in max_height and ay position.
238+ -- consuming values in rendering and collisions easier. Title is rendered
239+ -- above it, so we need to account for that in max_height and ay position.
241240 local min_width = state .fullormaxed and options .menu_min_width_fullscreen or options .menu_min_width
242241
243242 for _ , menu in ipairs (self .all ) do
@@ -252,6 +251,11 @@ function Menu:update_dimensions()
252251 self :scroll_to (menu .scroll_y , menu ) -- clamps scroll_y to scroll limits
253252 end
254253
254+ self :update_coordinates ()
255+ end
256+
257+ -- Updates element coordinates to match currently open (sub)menu.
258+ function Menu :update_coordinates ()
255259 local ax = round ((display .width - self .current .width ) / 2 ) + self .offset_x
256260 self :set_coordinates (ax , self .current .top , ax + self .current .width , self .current .top + self .current .height )
257261end
359363function Menu :select_value (value , menu )
360364 menu = menu or self .current
361365 local index = itable_find (menu .items , function (item ) return item .value == value end )
362- self :select_index (index , 5 )
366+ self :select_index (index )
363367end
364368
365369--- @param menu ? MenuStack
@@ -400,16 +404,23 @@ function Menu:activate_one_value(value, menu)
400404 self :activate_one_index (index , menu )
401405end
402406
403- --- @param id string
404- function Menu :activate_submenu (id )
405- local submenu = self .by_id [id ]
406- if submenu then
407- self .current = submenu
407+ --- @param menu MenuStack One of menus in ` self.all` .
408+ function Menu :activate_menu (menu )
409+ if itable_index_of (self .all , menu ) then
410+ self .current = menu
411+ self :update_coordinates ()
412+ self :reset_navigation ()
408413 request_render ()
409414 else
410- msg .error (string.format ( ' Requested submenu id "%s" doesn \' t exist ' , id ) )
415+ msg .error (' Attempt to open a menu not in `self.all` list. ' )
411416 end
412- self :reset_navigation ()
417+ end
418+
419+ --- @param id string
420+ function Menu :activate_submenu (id )
421+ local submenu = self .by_id [id ]
422+ if submenu then self :activate_menu (submenu )
423+ else msg .error (string.format (' Requested submenu id "%s" doesn\' t exist' , id )) end
413424end
414425
415426--- @param index ? integer
@@ -456,8 +467,7 @@ function Menu:back()
456467
457468 if parent then
458469 menu .selected_index = nil
459- self .current = parent
460- self :update_dimensions ()
470+ self :activate_menu (parent )
461471 self :tween (self .offset_x - menu .width / 2 , 0 , function (offset ) self :set_offset_x (offset ) end )
462472 self .opacity = 1 -- in case tween above canceled fade in animation
463473 else
@@ -473,11 +483,10 @@ function Menu:open_selected_item(opts)
473483 local item = menu .items [menu .selected_index ]
474484 -- Is submenu
475485 if item .items then
476- self .current = item
477486 if opts .preselect_submenu_item then
478487 item .selected_index = # item .items > 0 and 1 or nil
479488 end
480- self :update_dimensions ( )
489+ self :activate_menu ( item )
481490 self :tween (self .offset_x + menu .width / 2 , 0 , function (offset ) self :set_offset_x (offset ) end )
482491 self .opacity = 1 -- in case tween above canceled fade in animation
483492 else
@@ -491,10 +500,33 @@ function Menu:open_selected_item_soft() self:open_selected_item({keep_open = tru
491500function Menu :open_selected_item_preselect () self :open_selected_item ({preselect_submenu_item = true }) end
492501function Menu :select_item_below_cursor () self .current .selected_index = self :get_item_index_below_cursor () end
493502
503+ --- @param index integer
504+ function Menu :move_selected_item_to (index )
505+ local from , callback = self .current .selected_index , self .opts .on_move_item
506+ if callback and from and from ~= index and index >= 1 and index <= # self .current .items then
507+ callback (from , index , self .current .submenu_path )
508+ self .current .selected_index = index
509+ request_render ()
510+ end
511+ end
512+
513+ function Menu :move_selected_item_up ()
514+ if self .current .selected_index then self :move_selected_item_to (self .current .selected_index - 1 ) end
515+ end
516+
517+ function Menu :move_selected_item_down ()
518+ if self .current .selected_index then self :move_selected_item_to (self .current .selected_index + 1 ) end
519+ end
520+
521+ function Menu :delete_selected_item ()
522+ local index , callback = self .current .selected_index , self .opts .on_delete_item
523+ if callback and index then callback (index , self .current .submenu_path ) end
524+ end
525+
494526function Menu :on_display () self :update_dimensions () end
495527function Menu :on_prop_fullormaxed () self :update_content_dimensions () end
496528
497- function Menu :on_global_mbtn_left_down ()
529+ function Menu :handle_cursor_down ()
498530 if self .proximity_raw == 0 then
499531 self .drag_data = {{y = cursor .y , time = mp .get_time ()}}
500532 self .current .fling = nil
@@ -514,7 +546,7 @@ function Menu:fling_distance()
514546 return # self .drag_data < 2 and 0 or ((first .y - last .y ) / ((first .time - last .time ) / 0.03 )) * 10
515547end
516548
517- function Menu :on_global_mbtn_left_up ()
549+ function Menu :handle_cursor_up ()
518550 if self .proximity_raw == 0 and self .drag_data and not self .is_dragging then
519551 self :select_item_below_cursor ()
520552 self :open_selected_item ({preselect_submenu_item = false , keep_open = self .modifiers and self .modifiers .shift })
@@ -546,8 +578,8 @@ function Menu:on_global_mouse_move()
546578 request_render ()
547579end
548580
549- function Menu :on_wheel_up () self :scroll_by (self .scroll_step * - 3 , nil , {update_cursor = true }) end
550- function Menu :on_wheel_down () self :scroll_by (self .scroll_step * 3 , nil , {update_cursor = true }) end
581+ function Menu :handle_wheel_up () self :scroll_by (self .scroll_step * - 3 , nil , {update_cursor = true }) end
582+ function Menu :handle_wheel_down () self :scroll_by (self .scroll_step * 3 , nil , {update_cursor = true }) end
551583
552584function Menu :on_pgup ()
553585 local menu = self .current
@@ -585,6 +617,8 @@ function Menu:enable_key_bindings()
585617 -- doesn't support 'repeatable' flag, so we are stuck with this monster.
586618 self :add_key_binding (' up' , ' menu-prev1' , self :create_key_action (' prev' ), ' repeatable' )
587619 self :add_key_binding (' down' , ' menu-next1' , self :create_key_action (' next' ), ' repeatable' )
620+ self :add_key_binding (' ctrl+up' , ' menu-move-up' , self :create_key_action (' move_selected_item_up' ), ' repeatable' )
621+ self :add_key_binding (' ctrl+down' , ' menu-move-down' , self :create_key_action (' move_selected_item_down' ), ' repeatable' )
588622 self :add_key_binding (' left' , ' menu-back1' , self :create_key_action (' back' ))
589623 self :add_key_binding (' right' , ' menu-select1' , self :create_key_action (' open_selected_item_preselect' ))
590624 self :add_key_binding (' shift+right' , ' menu-select-soft1' ,
@@ -608,6 +642,7 @@ function Menu:enable_key_bindings()
608642 self :add_key_binding (' pgdwn' , ' menu-page-down' , self :create_key_action (' on_pgdwn' ), ' repeatable' )
609643 self :add_key_binding (' home' , ' menu-home' , self :create_key_action (' on_home' ))
610644 self :add_key_binding (' end' , ' menu-end' , self :create_key_action (' on_end' ))
645+ self :add_key_binding (' del' , ' menu-delete-item' , self :create_key_action (' delete_selected_item' ))
611646end
612647
613648function Menu :disable_key_bindings ()
@@ -620,8 +655,8 @@ function Menu:create_modified_mbtn_left_handler(modifiers)
620655 return function ()
621656 self .mouse_nav = true
622657 self .modifiers = modifiers
623- self :on_global_mbtn_left_down ()
624- self :on_global_mbtn_left_up ()
658+ self :handle_cursor_down ()
659+ self :handle_cursor_up ()
625660 self .modifiers = nil
626661 end
627662end
@@ -650,6 +685,13 @@ function Menu:render()
650685 end
651686 if update_cursor then self :select_item_below_cursor () end
652687
688+ cursor .on_primary_down = function () self :handle_cursor_down () end
689+ cursor .on_primary_up = function () self :handle_cursor_up () end
690+ if self .proximity_raw == 0 then
691+ cursor .on_wheel_down = function () self :handle_wheel_down () end
692+ cursor .on_wheel_up = function () self :handle_wheel_up () end
693+ end
694+
653695 local ass = assdraw .ass_new ()
654696 local opacity = options .menu_opacity * self .opacity
655697 local spacing = self .item_padding
0 commit comments