%% %% wings_menu.erl -- %% %% Implementation of pulldown and popup menus. %% %% Copyright (c) 2001-2004 Bjorn Gustavsson %% %% See the file "license.terms" for information on usage and redistribution %% of this file, and for a DISCLAIMER OF ALL WARRANTIES. %% %% $Id: wings_menu.erl,v 1.132 2005/06/27 20:13:06 bjorng Exp $ %% -module(wings_menu). -export([is_popup_event/1,menu/5,popup_menu/4,build_command/2, kill_menus/0]). -define(NEED_OPENGL, 1). -define(NEED_ESDL, 1). -include("wings.hrl"). -import(lists, [map/2,foldl/3,reverse/1,keysearch/3,foreach/2]). -define(SUB_MENU_TIME, 150). -define(SEPARATOR_HEIGHT, (wings_text:height()-4)). -define(INITIAL_LEVEL, 1). %% Menu information kept for a popup menu. -record(mi, {ymarg, %Margin at top and bottom shortcut, %Position for shortcut (pixels) w, %Width of menu (pixels) h, %Height of menu (pixels) hs, %Tuple: height of each entry. sel=none, %Selected item (1..size(Menu)) sel_side=left, %Selection on left or right. ns=[], %Name stack. menu, %Original menu term timer=make_ref(), %Active submenu timer. level=?INITIAL_LEVEL, %Menu level. adv, %Advanced menus (true|false). ignore_rel=true, %Ignore button release if % just openened. type=plain, %Type of menu: plain|popup owner, %Owning window. flags=[] %Flags (magnet/dialog). }). %%% %%% Inside this module, each entry in a menu is kept in the following %%% normalized format: %%% %%% separator OR %%% {Text,Name,Hotkey,Help,Properties} %%% %%% The Help field is normalized to %%% String for basic menu mode %%% {L,M,R} for advanced menu %%% is_popup_event(#mousebutton{button=3,x=X0,y=Y0,state=State,mod=Mod}) -> {X,Y} = wings_wm:local2global(X0, Y0), case wings_pref:get_value(advanced_menus) of true when State =:= ?SDL_RELEASED -> {yes,X,Y,Mod}; false when State =:= ?SDL_PRESSED -> {yes,X,Y,Mod}; _Other -> no end; is_popup_event(_Event) -> no. menu(X, Y, Owner, Name, Menu) -> menu_setup(plain, X, Y, Name, Menu, #mi{adv=false,owner=Owner,ignore_rel=false}). popup_menu(X, Y, Name, Menu) -> Adv = wings_pref:get_value(advanced_menus), IgnoreRel = (Adv == false), menu_setup(popup, X, Y, Name, Menu, #mi{adv=Adv,ignore_rel=IgnoreRel,owner=wings_wm:this()}). menu_setup(Type, X0, Y0, Name, Menu0, #mi{ns=Names0,adv=Adv}=Mi0) -> Names = [Name|Names0], Menu1 = wings_plugin:menu(list_to_tuple(reverse(Names)), Menu0), Hotkeys = wings_hotkey:matching(Names), Menu = normalize_menu(Menu1, Hotkeys, Adv), {MwL,MwM,MwR,Hs} = menu_dims(Menu), Cw = wings_text:width(), TotalW = MwL + MwM + MwR + 8* Cw, Mh = lists:sum(Hs), Margin = 3, {X1,Y1} = case Type of plain -> {X0,Y0}; popup when Adv == false -> {X0-TotalW div 2,Y0 - Margin div 2}; popup -> {X0-TotalW div 2,Y0 - Margin - ?CHAR_HEIGHT} end, {X,Y} = move_if_outside(X1, Y1, TotalW, Mh+2*Margin, Mi0), W = TotalW-10, Mi = Mi0#mi{ymarg=Margin,shortcut=MwL+Cw,w=TotalW-10,h=Mh,hs=Hs, sel=none,ns=Names,menu=Menu,adv=Adv,type=Type}, #mi{level=Level} = Mi, setup_menu_killer(Mi), Op = {seq,push,get_menu_event(Mi)}, WinName = {menu,Level}, wings_wm:delete({menu,Level}), wings_wm:new(WinName, {X,Y,highest}, {W,Mh+10}, Op), delete_from(Level+1). delete_from(Level) -> Name = {menu,Level}, case wings_wm:is_window(Name) of false -> keep; true -> wings_wm:delete(Name), delete_from(Level+1) end. setup_menu_killer(#mi{owner=Owner,level=Level}) -> %% The menu killer window lies below all menus, covering the entire screen. %% It will kill all menus if clicked. case wings_wm:is_window(menu_killer) of true -> if Level =:= ?INITIAL_LEVEL -> %% A new top-level menu will be created. Make sure %% that the menu killer window will be directly below it. wings_wm:raise(menu_killer), raise_menubar(Owner); true -> %% A sub-menu will be created. Don't move the menu killer. ok end; false -> Op = {push,fun(Ev) -> menu_killer(Ev, Owner) end}, {TopW,TopH} = wings_wm:top_size(), wings_wm:new(menu_killer, {0,0,highest}, {TopW,TopH}, Op), raise_menubar(Owner) end. raise_menubar(Owner) -> Menubar = {menubar,Owner}, case wings_wm:is_window(Menubar) of false -> ok; true -> wings_wm:raise(Menubar) end. menu_killer(#mousebutton{button=1,state=?SDL_PRESSED}, Owner) -> wings_wm:notify(menu_aborted), kill_menus(Owner); menu_killer(#keyboard{sym=27}, Owner) -> %Escape. wings_wm:notify(menu_aborted), kill_menus(Owner); menu_killer(kill_menus, Owner) -> kill_menus(Owner); menu_killer(_, _) -> keep. kill_menus(Owner) -> foreach(fun({menu,_}=Name) -> wings_wm:delete(Name); ({menubar,Win}=Name) when Win =:= Owner -> wings_wm:send(Name, clear_menu_selection); (_) -> ok end, wings_wm:windows()), delete. menu_show(#mi{ymarg=Margin,shortcut=Shortcut,w=Mw,h=Mh}=Mi) -> wings_io:blend(wings_pref:get_value(menu_color), fun(Color) -> wings_io:border(0, 0, Mw-1, Mh + 2*Margin+3, Color) end), menu_draw(3*?CHAR_WIDTH, Margin+?CHAR_HEIGHT, Shortcut, Mw, 1, Mi#mi.hs, Mi). normalize_menu(Menu, Hotkeys, Adv) -> normalize_menu(Menu, Hotkeys, Adv, []). normalize_menu([{basic,_}|Els], Hotkeys, true, Acc) -> normalize_menu(Els, Hotkeys, true, Acc); normalize_menu([{basic,El}|Els], Hotkeys, false, Acc) -> normalize_menu([El|Els], Hotkeys, false, Acc); normalize_menu([{advanced,_}|Els], Hotkeys, false, Acc) -> normalize_menu(Els, Hotkeys, false, Acc); normalize_menu([{advanced,El}|Els], Hotkeys, true, Acc) -> normalize_menu([El|Els], Hotkeys, true, Acc); normalize_menu([[_|_]=List|Els], Hotkeys, Adv, Acc) -> normalize_menu(List++Els, Hotkeys, Adv, Acc); normalize_menu([Elem0|Els], Hotkeys, Adv, Acc) -> Elem1 = case Elem0 of {S,Name,Help,Ps} -> {S,Name,[],Help,Props=adv_filter(Adv, Ps)}; {S,Name,[C|_]=Help} when is_integer(C) -> {S,Name,[],Help,Props=[]}; {S,Name,Ps} -> {S,Name,[],[],Props=adv_filter(Adv, Ps)}; {S,Name} -> {S,Name,[],[],Props=[]}; separator -> Name = none, Props = [], separator end, Elem2 = norm_add_hotkey(Name, Elem1, Hotkeys, Props), Elem = norm_help(Elem2, Adv), normalize_menu(Els, Hotkeys, Adv, [Elem|Acc]); normalize_menu([], _Hotkeys, _Adv, Acc) -> list_to_tuple(reverse(Acc)). adv_filter(false, [magnet|T]) -> adv_filter(false, T); adv_filter(Flag, [H|T]) -> [H|adv_filter(Flag, T)]; adv_filter(_, []) -> []. norm_add_hotkey(_, separator, _, _) -> separator; norm_add_hotkey(_, Elem, [], _) -> Elem; norm_add_hotkey({_,[_|_]}, Elem, _, _) -> Elem; norm_add_hotkey({Key,Fun}, Elem, Hotkeys, Props) when is_function(Fun) -> case Fun(1, []) of [_|_] -> Elem; %Submenu. Name0 -> Name = {Key,reduce_ask(Name0)}, norm_add_hotkey(Name, Elem, Hotkeys, Props) end; norm_add_hotkey(Fun, Elem, Hotkeys, Props) when is_function(Fun) -> Name = reduce_name(Fun(1, [])), norm_add_hotkey(Name, Elem, Hotkeys, Props); norm_add_hotkey({'ASK',_}=Ask, Elem, Hotkeys, Props) -> norm_add_hotkey(reduce_ask(Ask), Elem, Hotkeys, Props); norm_add_hotkey(Name, Elem, Hotkeys, Props) -> Key = match_hotkey(Name, Hotkeys, have_option_box(Props)), setelement(3, Elem, Key). match_hotkey(Name, [{Name,Key}|_], false) -> Key; match_hotkey(Name, [{{Name,false},Key}|_], true) -> Key; match_hotkey(Name, [{{Name,true},Key}|_], true) -> Key; match_hotkey(Name, [_|T], OptionBox) -> match_hotkey(Name, T, OptionBox); match_hotkey(_, [], _) -> []. reduce_name({'ASK',_}=Ask) -> Ask; reduce_name({Key,{_,_}=Tuple}) when is_atom(Key) -> reduce_name(Tuple); reduce_name({Key,Val}) when is_atom(Key) -> Val; reduce_name(Name) -> Name. reduce_ask({'ASK',Ask}) -> reduce_ask_1(Ask); reduce_ask(Term) -> Term. reduce_ask_1({A,B,_}) -> reduce_ask_1({A,B}); reduce_ask_1({[],[Res]}) -> Res; reduce_ask_1({[],Res}) -> list_to_tuple(reverse(Res)); reduce_ask_1(_) -> none. norm_help(separator=Item, _) -> Item; norm_help(Item, false) -> norm_help_basic(Item); norm_help(Elem, true) -> norm_help_adv(Elem, {[],[],[]}). norm_help_basic({_,_,_,Help,_}=Item) when is_list(Help) -> Item; norm_help_basic({T,N,Hot,Help,Ps}) when is_tuple(Help), size(Help) > 0 -> {T,N,Hot,element(1, Help),Ps}. norm_help_adv({T,N,Hot,[],Ps}, Empty) -> norm_help_adv({T,N,Hot,Empty,Ps}, Empty); norm_help_adv({T,N,Hot,[_|_]=L,Ps}, Empty) -> norm_help_adv({T,N,Hot,{L,[],[]},Ps}, Empty); norm_help_adv({T,N,Hot,{L},Ps}, Empty) -> norm_help_adv({T,N,Hot,{L,[],[]},Ps}, Empty); norm_help_adv({T,N,Hot,{L,M},Ps}, Empty) -> norm_help_adv({T,N,Hot,{L,M,[]},Ps}, Empty); norm_help_adv({_,_,_,{_,_,[_|_]},_}=Item, _) -> Item; norm_help_adv({T,N,Hot,{Hl,Hm,[]},Ps}=Item, _) -> case have_option_box(Ps) of false -> Item; true -> Hr = ?__(1, "Open option dialog"), {T,N,Hot,{Hl,Hm,Hr},Ps} end. menu_dims(Menu) -> menu_dims(Menu, size(Menu), 0, 0, 0, []). menu_dims(_Menu, 0, MaxA, MaxB, MaxC, H) -> {MaxA,MaxB,MaxC,H}; menu_dims(Menu, I, MaxA0, MaxB0, MaxC0, Hacc) -> {Wa,Wb,Wc,H} = case element(I, Menu) of {S,ignore,[],[],[]} when I == 1 -> case wings_text:width([$\s|S]) - (MaxA0+MaxB0+MaxC0) of W when W < 0 -> {0,0,0,?LINE_HEIGHT}; W -> {MaxA0+W,0,0,?LINE_HEIGHT} end; {S,{_,_},Hotkey,_,_} -> {wings_text:width([$.,$.|S]),wings_text:width(Hotkey), wings_text:width(),?LINE_HEIGHT}; {S,_,Hotkey,_,Ps} -> {wings_text:width([$.,$.|S]),wings_text:width(Hotkey), right_width(Ps),?LINE_HEIGHT}; separator -> {0,0,0,?SEPARATOR_HEIGHT} end, menu_dims(Menu, I-1, max(Wa, MaxA0), max(Wb, MaxB0), max(Wc, MaxC0), [H|Hacc]). max(A, B) when A > B -> A; max(_A, B) -> B. right_width(Ps) -> Cw = wings_text:width(), case have_option_box(Ps) of true -> Cw; false -> case proplists:get_value(color, Ps, none) of none -> 0; _ -> Cw end end. %%% %%% Event loop for menus. %%% get_menu_event(Mi) -> {replace,fun(Ev) -> handle_menu_event(Ev, Mi) end}. handle_menu_event(redraw, Mi) -> redraw(Mi), keep; handle_menu_event(lost_focus, Mi) -> {_,X,Y} = wings_wm:local_mouse_state(), {_,H} = wings_wm:win_size(), if Y < 0 -> mousemotion(X, 0, Mi); Y >= H -> mousemotion(X, H-2, Mi); true -> keep end; handle_menu_event(#keyboard{}=Ev, Mi) -> handle_key(Ev, Mi); handle_menu_event(#mousemotion{x=X,y=Y}, Mi) -> mousemotion(X, Y, Mi); handle_menu_event(#mousebutton{}=Ev, Mi) -> button_pressed(Ev, Mi); handle_menu_event(#mi{}=Mi, _) -> %Key bound/unbound. help_text(Mi), wings_wm:dirty(), get_menu_event(Mi); handle_menu_event(clear_submenu, #mi{level=Level}) -> delete_from(Level+1); handle_menu_event(quit, Mi) -> clear_menu_selection(Mi), wings_wm:later(quit), delete_all(Mi); handle_menu_event(_, _) -> keep. clear_menu_selection(#mi{owner=Owner}) -> wings_wm:send({menubar,Owner}, clear_menu_selection). mousemotion(X, Y, Mi0) -> Mi1 = update_highlight(X, Y, Mi0), Mi = set_submenu_timer(Mi1, Mi0, X, Y), get_menu_event(Mi). button_pressed(#mousebutton{state=?SDL_RELEASED}, #mi{ignore_rel=true}=Mi) -> get_menu_event(Mi#mi{ignore_rel=false}); button_pressed(#mousebutton{button=B,x=X,y=Y,state=?SDL_RELEASED}, #mi{adv=false}=Mi) when (B =< 3) -> wings_wm:dirty(), button_pressed(1, 0, X, Y, Mi); button_pressed(#mousebutton{button=B,x=X,y=Y,mod=Mod,state=?SDL_RELEASED}, Mi) when (B =< 3) -> wings_wm:dirty(), button_pressed(B, Mod, X, Y, Mi); button_pressed(_, _) -> keep. button_pressed(Button, Mod, X, Y, #mi{ns=Names,menu=Menu,adv=Adv}=Mi0) -> clear_timer(Mi0), Mi1 = update_highlight(X, Y, Mi0), Mi = update_flags(Mod, Mi1), case selected_item(Y, Mi) of none -> get_menu_event(Mi); Item when is_integer(Item) -> case element(Item, Menu) of {_,{'VALUE',Act0},_,_,Ps} -> Act = was_option_hit(Button, Act0, X, Ps, Mi), do_action(Act, Names, Ps, Mi); {_,{Name,Submenu},_,_,_} when Adv == true -> popup_submenu(Button, X, Y, Name, Submenu, Mi); {_,{Name,Submenu},_,_,_} when Adv == false -> submenu(Item, Name, Submenu, Mi); {_,Act0,_,_,Ps} when is_function(Act0) -> call_action(Act0, Button, Names, Ps, Mi); {_,Act0,_,_,Ps} when is_atom(Act0); is_integer(Act0) -> Act = was_option_hit(Button, Act0, X, Ps, Mi), do_action(Act, Names, Ps, Mi) end end. call_action(Act, Button, Ns, Ps, Mi) -> case Act(Button, Ns) of ignore -> keep; Cmd0 when is_tuple(Cmd0) -> Cmd = case is_magnet_active(Ps, Mi) of false -> Cmd0; true -> insert_magnet_flags(Cmd0, Mi) end, send_action(Cmd, Mi) end. do_action(Act0, Ns, Ps, Mi) -> Act = case is_magnet_active(Ps, Mi) of false -> build_command(Act0, Ns); true -> build_command({'ASK',{[magnet],[Act0]}}, Ns) end, send_action(Act, Mi). send_action(Action, #mi{owner=Owner}=Mi) -> clear_menu_selection(Mi), wings_wm:send_after_redraw(Owner, {action,Action}), delete_all(Mi). is_magnet_active(Ps, #mi{flags=Flags}) -> have_magnet(Ps) andalso have_magnet(Flags). handle_key(Ev, Mi) -> handle_key_1(key(Ev), Mi). handle_key_1(cancel, _) -> wings_wm:send(menu_killer, #keyboard{sym=27}); handle_key_1(delete, Mi0) -> %% Delete hotkey bound to this entry. case current_command(Mi0) of none -> keep; Cmd -> wings_wm:dirty(), NextKey = wings_hotkey:delete_by_command(Cmd), Mi = set_hotkey(NextKey, Mi0), get_menu_event(Mi) end; handle_key_1(insert, Mi) -> %% Define new hotkey for this entry. case current_command(Mi) of none -> keep; Cmd -> wings_wm:dirty(), get_hotkey(Cmd, Mi) end; handle_key_1(_, _) -> keep. key(#keyboard{sym=27}) -> cancel; key(#keyboard{sym=?SDLK_INSERT}) -> insert; key(#keyboard{unicode=$/}) -> insert; key(#keyboard{sym=?SDLK_DELETE}) -> delete; key(#keyboard{unicode=$\\}) -> delete; key(_) -> none. current_command(Mi) -> case current_command_1(Mi) of none -> none; {Cmd0,OptionBox} -> Cmd = add_option(OptionBox, Cmd0, false), simplify_command(Cmd) end. current_command_1(#mi{sel=none}) -> none; current_command_1(#mi{sel=Sel,menu=Menu,ns=Names,owner=Owner}) when Owner == geom; element(1, Owner) == geom -> case element(Sel, Menu) of {_,Name,_,_,Ps} when is_atom(Name); is_integer(Name) -> {build_command(Name, Names),have_option_box(Ps)}; {_,Fun,_,_,Ps} when is_function(Fun) -> Cmd = Fun(1, Names), case is_ascii_clean(Cmd) of true -> {Cmd,have_option_box(Ps)}; false -> none end; {_,{Name,Fun},_,_,Ps} when is_function(Fun) -> Cmd = Fun(1, [Name|Names]), case is_tuple(Cmd) andalso is_ascii_clean(Cmd) of true -> {Cmd,have_option_box(Ps)}; false -> none end; _Other -> io:format("~p\n", [_Other]), none end; current_command_1(_) -> none. %% Test if a term can be represented in a text file and read back. is_ascii_clean([H|T]) -> is_ascii_clean(H) andalso is_ascii_clean(T); is_ascii_clean([]) -> true; is_ascii_clean(T) when is_tuple(T) -> is_tuple_ascii_clean(1, size(T), T); is_ascii_clean(Num) when is_number(Num) -> true; is_ascii_clean(Atom) when is_atom(Atom) -> true; is_ascii_clean(_) -> false. is_tuple_ascii_clean(I, N, T) when I =< N -> is_ascii_clean(element(I, T)) andalso is_tuple_ascii_clean(I+1, N, T); is_tuple_ascii_clean(_, _, _) -> true. add_option(false, Cmd, _) -> Cmd; add_option(true, Cmd, Val) -> add_option_1(Cmd, Val). add_option_1({Desc,Tuple}, Val) when is_tuple(Tuple) -> {Desc,add_option_1(Tuple, Val)}; add_option_1({Desc,Leave}, Val) -> {Desc,{Leave,Val}}. simplify_command({'ASK',{[],[Res],_}}) -> Res; simplify_command({'ASK',{[],Res,_}}) -> list_to_tuple(reverse(Res)); simplify_command(Cmd0) when is_tuple(Cmd0) -> Cmd = [simplify_command(El) || El <- tuple_to_list(Cmd0)], list_to_tuple(Cmd); simplify_command(Term) -> Term. set_hotkey(Val, #mi{sel=Sel,menu=Menu0}=Mi) -> case element(Sel, Menu0) of {A,B,_,D,E} -> %% First nuke any other menu entry that happens to be %% bound to this hotkey. Menu1 = map(fun({_,_,Key,_,_}=T) when Key =:= Val -> setelement(3, T, []); (El) -> El end, tuple_to_list(Menu0)), Menu2 = list_to_tuple(Menu1), %% Insert the hotkey. Menu = setelement(Sel, Menu2, {A,B,Val,D,E}), Mi#mi{menu=Menu}; _Other -> Mi end. popup_submenu(Button, X0, Y0, SubName, SubMenu0, #mi{owner=Owner,level=Level}=Mi) -> %% Only in advanced menu mode. case expand_submenu(Button, SubName, SubMenu0, Mi) of ignore -> keep; Action0 when is_tuple(Action0); is_atom(Action0) -> Action = insert_magnet_flags(Action0, Mi), clear_menu_selection(Mi), wings_wm:send(Owner, {action,Action}), delete_all(Mi); SubMenu when is_list(SubMenu) -> clear_timer(Mi), {X,Y} = wings_wm:local2global(X0, Y0), menu_setup(popup, X, Y, SubName, SubMenu, Mi#mi{level=Level+1}), delete end. update_flags(Mod, Mi) -> case wings_msg:free_rmb_modifier() of RmbMod when Mod band RmbMod =/= 0 -> Mi#mi{flags=[magnet]}; _ -> Mi end. insert_magnet_flags(Action, #mi{flags=[magnet]}) -> insert_magnet_flags_0(Action); insert_magnet_flags(Action,_) -> Action. insert_magnet_flags_0({'ASK',{PickList,Done,Flags}}=Cmd) -> case have_magnet(Flags) of false -> Cmd; true -> {'ASK',{PickList++[magnet],Done,Flags}} end; insert_magnet_flags_0(Tuple0) when is_tuple(Tuple0) -> Tuple = [insert_magnet_flags_0(El) || El <- tuple_to_list(Tuple0)], list_to_tuple(Tuple); insert_magnet_flags_0(Term) -> Term. submenu(I, Name, Menu0, #mi{w=W,hs=Hs,level=Level}=Mi0) -> %% Only in basic menu mode. Menu = expand_submenu(1, Name, Menu0, Mi0), X0 = W-?CHAR_WIDTH, Y0 = get_item_pos(I, Hs, -?LINE_HEIGHT), Mi = Mi0#mi{level=Level+1}, {X,Y} = wings_wm:local2global(X0, Y0), menu_setup(plain, X, Y, Name, Menu, Mi). get_item_pos(0, _Hs, Pos) -> Pos; get_item_pos(I, [H|Hs], Pos) -> get_item_pos(I-1, Hs, Pos+H). expand_submenu(B, Name, Submenu, #mi{ns=Ns}) when is_function(Submenu) -> Submenu(B, [Name|Ns]); expand_submenu(_Button, _Name, Submenu, _Mi) -> Submenu. clear_timer(#mi{timer=Timer}) -> wings_wm:cancel_timer(Timer). set_submenu_timer(#mi{sel=Sel}=Mi, #mi{sel=Sel}, _X, _Y) -> Mi; set_submenu_timer(#mi{sel=Sel}=Mi, OldMi, X0, Y0) -> clear_timer(OldMi), clear_timer(Mi), case is_submenu(Sel, Mi) of false -> Timer = wings_wm:set_timer(?SUB_MENU_TIME, clear_submenu), Mi#mi{timer=Timer}; true -> {X,Y} = wings_wm:local2global(X0, Y0), Event = #mousebutton{button=1,x=X,y=Y,state=?SDL_RELEASED}, Timer = wings_wm:set_timer(?SUB_MENU_TIME, Event), Mi#mi{timer=Timer} end. delete_all(Mi) -> clear_timer(Mi), kill_menus(), delete. kill_menus() -> case wings_wm:is_window(menu_killer) of true -> wings_wm:send(menu_killer, kill_menus); false -> ok end. redraw(Mi) -> wings_io:ortho_setup(), menu_show(Mi). update_highlight(X, Y, #mi{menu=Menu,sel=OldSel,sel_side=OldSide,w=W}=Mi0) -> case selected_item(Y, Mi0) of OldSel when is_integer(OldSel) -> Ps = element(5, element(OldSel, Menu)), RightWidth = right_width(Ps), Right = W - (2*RightWidth) - ?CHAR_WIDTH, Side = if X < Right; RightWidth == 0 -> left; true -> right end, if Side =:= OldSide -> Mi0; true -> wings_wm:dirty(), help_text(Mi0), Mi0#mi{sel_side=Side} end; OldSel -> Mi0; NoSel when NoSel == outside; NoSel == none -> wings_wm:dirty(), Mi = Mi0#mi{sel=none}, help_text(Mi), Mi; Item when is_integer(Item), OldSel == none -> wings_wm:dirty(), Mi = Mi0#mi{sel=Item}, help_text(Mi), Mi; Item when is_integer(Item) -> wings_wm:dirty(), Mi = Mi0#mi{sel=Item,ignore_rel=false}, help_text(Mi), Mi end. was_option_hit(Button, Act, X, Ps, Mi) -> case have_option_box(Ps) of false -> Act; true -> {Act,hit_right(Button, X, Mi)} end. hit_right(B, _, #mi{adv=true}) when B > 1 -> true; hit_right(_, X, #mi{w=W}) -> X >= W-3*?CHAR_WIDTH. selected_item(Y, #mi{adv=Adv,ymarg=Margin,h=H,menu=Menu}=Mi) -> %% The tests are simplified because we know that the mouse cursor %% must be over the menu window. if Margin =< Y, Y < H+Margin -> %% Clearly inside the menu area. selected_item_1(Y-Margin, Mi); Y < Margin -> %% Above upper margin. If advanced menus, it is safe %% to count that as if the mouse is over the first row. %% (The menu doesn't popup until the RMB has been released, %% unlike the basic menus.) if Adv -> selected_item_1(0, Mi); true -> none end; true -> %% Below the lower margin. Pretend that the cursor is over the %% last row (we assume that it cannot be inactive text %% or a separator). size(Menu) end. selected_item_1(Y, #mi{hs=Hs}=Mi) -> selected_item_1(Y, 1, Hs, Mi). selected_item_1(Y0, I, [H|Hs], #mi{sel=OldSel,menu=Menu}=Mi) -> case Y0 - H of Y when Y =< 0 -> case element(I, Menu) of separator -> if OldSel =:= I-1; OldSel =:= I+1 -> OldSel; true -> none end; {_Text,ignore,_,_,_} -> if I-1 =< OldSel, OldSel =< I+2-> OldSel; true -> none end; _Other -> I end; Y -> selected_item_1(Y, I+1, Hs, Mi) end. is_submenu(_I, #mi{adv=true}) -> false; is_submenu(I, #mi{menu=Menu}) when is_integer(I) -> case element(I, Menu) of separator -> false; {_Text,{'VALUE',_},_Hotkey,_Help,_Ps} -> false; {_Text,{_,_},_Hotkey,_Help,_Ps} -> true; _Other -> false end; is_submenu(_, _) -> false. build_command(Name, Names) -> foldl(fun(N, A) -> {N,A} end, Name, Names). menu_draw(_X, _Y, _Shortcut, _Mw, _I, [], _Mi) -> ok; menu_draw(X, Y, Shortcut, Mw, I, [H|Hs], #mi{menu=Menu,adv=Adv}=Mi) -> ?CHECK_ERROR(), Elem = element(I, Menu), Text = menu_text(Elem, Adv), case Elem of separator -> draw_separator(X, Y, Mw); {_,ignore,_,_,Ps} -> menu_draw_1(Y, Ps, I, Mi, fun() -> wings_io:unclipped_text(X, Y, Text) end); {_,{'VALUE',_},Hotkey,_Help,Ps} -> %% Not a sub-menu. menu_draw_1(Y, Ps, I, Mi, fun() -> draw_menu_text(X, Y, Text, Ps), draw_hotkey(X, Y, Shortcut, Hotkey) end, fun() -> draw_right(X, Y, Mw, Ps) end); {_,{_,_}=Sub,Hotkey,_Help,Ps} -> %% Submenu. menu_draw_1(Y, Ps, I, Mi, fun() -> wings_io:unclipped_text(X, Y, Text), draw_hotkey(X, Y, Shortcut, Hotkey) end), draw_submenu(Adv, Sub, X+Mw-5*?CHAR_WIDTH, Y-?CHAR_HEIGHT div 3); {_,_,Hotkey,_Help,Ps} -> menu_draw_1(Y, Ps, I, Mi, fun() -> draw_menu_text(X, Y, Text, Ps), draw_hotkey(X, Y, Shortcut, Hotkey) end, fun() -> draw_right(X, Y, Mw, Ps) end) end, ?CHECK_ERROR(), menu_draw(X, Y+H, Shortcut, Mw, I+1, Hs, Mi). menu_draw_1(Y, Ps, Sel, Mi, DrawLeft) -> menu_draw_1(Y, Ps, Sel, Mi, DrawLeft, ignore). menu_draw_1(Y, Ps, Sel, #mi{sel=Sel,sel_side=Side,w=W}, DrawLeft, DrawRight) -> %% Draw blue background for highlighted item. wings_io:set_color(wings_pref:get_value(menu_hilite)), Cw = wings_text:width(), Right = W - (2*right_width(Ps)) - Cw, case Side of right -> gl:recti(Right, Y-?CHAR_HEIGHT, Right+3*Cw-2, Y+3), wings_io:set_color(wings_pref:get_value(menu_text)); left -> gl:recti(?CHAR_WIDTH, Y-?CHAR_HEIGHT, Right, Y+3), wings_io:set_color(wings_pref:get_value(menu_hilited_text)) end, DrawLeft(), case {DrawRight,Side} of {ignore,_} -> ok; {_,left} -> wings_io:set_color(wings_pref:get_value(menu_text)), DrawRight(); {_,right} -> wings_io:set_color(wings_pref:get_value(menu_hilited_text)), DrawRight() end; menu_draw_1(_, _, _, _, DrawLeft, DrawRight) -> wings_io:set_color(wings_pref:get_value(menu_text)), DrawLeft(), case DrawRight of ignore -> ok; _ -> DrawRight() end. menu_text({Text,{_,Fun},_,_,_}, true) when is_function(Fun) -> [$.,Text,$.]; menu_text({Text,Fun,_,_,_}, true) when is_function(Fun) -> [$.,Text,$.]; menu_text({Text,_,_,_,_}, _) -> Text; menu_text(separator, _) -> []. draw_hotkey(_, _, _, []) -> ok; draw_hotkey(X, Y, Pos, Hotkey) -> wings_io:text_at(X+Pos, Y, Hotkey). draw_menu_text(X, Y, Text, Props) -> case proplists:is_defined(crossmark, Props) of true -> wings_io:unclipped_text(X-2*?CHAR_WIDTH, Y, [crossmark,$\s|Text]); false -> case proplists:is_defined(grey_crossmark, Props) of false -> ok; true -> gl:pushAttrib(?GL_CURRENT_BIT), gl:color3f(0.25, 0.25, 0.25), wings_io:unclipped_text(X-2*?CHAR_WIDTH, Y, [crossmark]), gl:popAttrib() end, wings_io:unclipped_text(X, Y, Text) end. help_text(#mi{sel=none}) -> wings_wm:message(""); help_text(#mi{menu=Menu,sel=Sel}=Mi) -> Elem = element(Sel, Menu), help_text_1(Elem, Mi). help_text_1({Text,{Sub,_},_,_,_}, #mi{adv=false}) when Sub =/= 'VALUE' -> %% No specific help text for submenus in basic mode. Help = [Text|?__(1," submenu")], wings_wm:message(Help, ""); help_text_1({_,{Name,Fun},_,_,Ps}, #mi{ns=Ns}=Mi) when is_function(Fun) -> %% "Submenu" in advanced mode. Help0 = Fun(help, [Name|Ns]), Help = help_text_2(Help0), magnet_help(Help, Ps, Mi); help_text_1({_,_,_,Help0,Ps}, Mi) -> %% Plain entry - not submenu. Help = help_text_2(Help0), magnet_help(Help, Ps, Mi); help_text_1(separator, _) -> ok. help_text_2({S1,S2,S3}) -> wings_msg:button_format(S1, S2, S3); help_text_2(Help) -> Help. magnet_help(Msg0, Ps, #mi{flags=Flags}) -> case have_magnet(Ps) of false -> wings_wm:message(Msg0); true -> case have_magnet(Flags) of false -> ModRmb = wings_msg:free_rmb_modifier(), ModName = wings_msg:mod_name(ModRmb), MagMsg = [ModName,$+, ?__(2,"Click for Magnet")], Msg = wings_msg:join(Msg0, MagMsg), wings_wm:message(Msg); true -> Msg = wings_msg:join(Msg0, wings_magnet:info_string()), wings_wm:message(Msg, "") end end. draw_right(X0, Y0, Mw, Ps) -> case have_option_box(Ps) of true -> X = X0 + Mw - 5*?CHAR_WIDTH, Y = Y0 - ?CHAR_HEIGHT div 3, wings_io:text_at(X, Y, [option_box]); false -> draw_right_1(X0, Y0, Mw, Ps) end. draw_right_1(X0, Y0, Mw, Ps) -> case proplists:get_value(color, Ps, none) of none -> ok; Color -> Cw = wings_text:width(), Ch = wings_text:height(), X = X0 + Mw - 5*Cw, Y = Y0 - Ch + 1, wings_io:border(X, Y, Cw, Ch-1, Color) end. draw_submenu(_Adv, Item, _X, _Y) when is_atom(Item); is_integer(Item); is_list(Item) -> ok; draw_submenu(true, _Item, _X, _Y) -> ok; draw_submenu(false, _Item, X, Y) -> Cw = wings_text:width(), H = (wings_text:height()+2) div 3, ?CHECK_ERROR(), gl:'begin'(?GL_TRIANGLES), gl:vertex2i(X-Cw div 2, Y), gl:vertex2i(X-Cw, Y-H), gl:vertex2i(X-Cw, Y+H), gl:'end'(), ?CHECK_ERROR(). draw_separator(X, Y, Mw) -> ?CHECK_ERROR(), Cw = wings_text:width(), LeftX = X-2*Cw+0.5, RightX = X+Mw-4*Cw+0.5, UpperY = Y-?SEPARATOR_HEIGHT+0.5, gl:lineWidth(1), gl:color3f(0.10, 0.10, 0.10), gl:'begin'(?GL_LINES), gl:vertex2f(LeftX, UpperY), gl:vertex2f(RightX, UpperY), gl:'end'(), gl:color3b(0, 0, 0). move_if_outside(X0, Y, Mw, Mh, Mi) -> {W,H} = wings_wm:top_size(), if X0+Mw > W -> X = left_of_parent(Mw, W, Mi), move_if_outside_1(X, Y, Mh, H); true -> move_if_outside_1(X0, Y, Mh, H) end. move_if_outside_1(X, Y, Mh, H) when Y+Mh > H -> move_if_outside_2(X, H-Mh); move_if_outside_1(X, Y, _, _) -> move_if_outside_2(X, Y). move_if_outside_2(X, Y) when X < 0 -> move_if_outside_3(0, Y); move_if_outside_2(X, Y) -> move_if_outside_3(X, Y). move_if_outside_3(X, Y) when Y < 1 -> {X,1}; move_if_outside_3(X, Y) -> {X,Y}. left_of_parent(Mw, W, #mi{level=?INITIAL_LEVEL}) -> W-Mw; left_of_parent(Mw, _, #mi{level=Level}) -> {X,_,_,_} = wings_wm:viewport({menu,Level-1}), X-Mw+10. have_option_box(Ps) -> proplists:is_defined(option, Ps). have_magnet(Ps) -> proplists:is_defined(magnet, Ps). %%% %%% Get a key to bind a command to. %%% get_hotkey(Cmd, Mi) -> wings_wm:dirty(), wings_wm:message(?__(1,"Press key to bind command to.")), {push,fun(Ev) -> handle_key_event(Ev, Cmd, Mi) end}. handle_key_event(redraw, _Cmd, Mi) -> redraw(Mi), keep; handle_key_event(#keyboard{sym=Sym}, _, _) when Sym >= ?SDLK_NUMLOCK -> keep; handle_key_event(#keyboard{}=Ev, Cmd, Mi) -> Win = wings_wm:this(), case wings_hotkey:event(Ev, Cmd) of next -> do_bind(Win, Ev, Cmd, Mi); OtherCmd -> C = wings_util:stringify(OtherCmd), Q = ?__(1,"This key is already bound to the ") ++ C ++ ?__(2," command. Do you want to re-define it?"), wings_u:yes_no(Q, fun() -> do_bind(Win, Ev, Cmd, Mi) end) end, pop; handle_key_event(_, _, _) -> keep. do_bind(Win, Ev, Cmd, Mi0) -> Keyname = wings_hotkey:bind_from_event(Ev, Cmd), Mi = set_hotkey(Keyname, Mi0), wings_wm:send(Win, Mi), ignore.