1%%
2%%  wpc_view_win.erl --
3%%
4%%     This module implements the Save Views commands in a window.
5%%
6%%  Copyright (c) 2012 Micheus
7%%
8%%  See the file "license.terms" for information on usage and redistribution
9%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
10%%
11%%     $Id$
12%%
13
14-module(wpc_views_win).
15-export([init/0,menu/2,command/2,win_data/1,win_name/0]).
16-export([window/1,window/5]).
17
18-export([init/1,
19	 handle_call/3, handle_cast/2,
20	 handle_event/2, handle_sync_event/3,
21	 handle_info/2, code_change/3, terminate/2
22	]).
23
24-define(NEED_ESDL, true).
25-define(WIN_NAME, {plugin,saved_views}).
26-include_lib("src/wings.hrl").
27
28%%%
29%%% Saved Views window.
30%%%
31init() -> true.
32
33menu({window}, Menu) ->
34    Menu++[camera_menu()];
35menu({view}, Menu) ->
36    PatchMenu = fun({String, {views, List}}) ->
37			{String, {views, List++[separator, camera_menu()]}};
38		   (Entry) -> Entry
39		end,
40    [PatchMenu(Entry) || Entry <- Menu];
41menu(_,Menu) ->
42    Menu.
43
44camera_menu() ->
45	 {?__(1,"Manage Saved Views"), saved_views,
46	  ?__(2,"Shows all saved views")}.
47
48command({window,saved_views}, St) ->
49    window(St),
50    keep;
51command({view,{views, saved_views}}, St) ->
52    window(St),
53    keep;
54command(_,_) ->
55	next.
56
57%% win_data/1 function allows many plugin windows to be saved.
58%% it returns: {Name, {Horiz alignment, Custom_data}}
59%% horiz alignment should be either "left" or "right"
60%% custom data is used to store windows properties and custom data - it should be parsed in window/5
61win_data(?WIN_NAME=Name) ->
62    {Name, {right,[]}}.
63
64win_name() ->
65    ?WIN_NAME.
66
67window(St) ->
68    case wings_wm:is_window(?WIN_NAME) of
69	true ->
70	    wings_wm:raise(?WIN_NAME),
71	    keep;
72	false ->
73	    {_DeskW,DeskH} = wings_wm:top_size(),
74	    W = 18*?CHAR_WIDTH,
75	    Pos = {5,105},
76	    Size = {W,DeskH div 3},
77	    window(?WIN_NAME, Pos, Size, [], St),
78	    keep
79    end.
80
81window(WinName, Pos, Size, Ps0, St) ->
82    View = get_view_state(St),
83    {Frame,Ps} = wings_frame:make_win(title(), [{size, Size}, {pos, Pos}|Ps0]),
84    Window = wings_sup:window(undefined, ?MODULE, [Frame, Ps, View]),
85    Fs = [{display_data, geom_display_lists}|Ps],
86    wings_wm:toplevel(WinName, Window, Fs, {push,change_state(Window, St)}),
87    keep.
88
89title() ->
90    ?__(1,"Saved views").
91
92%%%%%%%% View Window internals %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
93%% Inside wings (process)
94%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
95
96change_state(Window, St) ->
97    fun(Ev) -> forward_event(Ev, Window, St) end.
98
99forward_event(redraw, _Window, _St) -> keep;
100forward_event({current_state, _,_}, _Window, _St) -> keep;
101forward_event({current_state, St}, Window, St0) ->
102    case (New = get_view_state(St)) =:= get_view_state(St0) of
103	true  -> ignore;
104	false -> wx_object:cast(Window, {new_state, New})
105    end,
106    {replace, change_state(Window, St)};
107forward_event({apply, ReturnSt, Fun}, Window, St0) ->
108    %% Apply ops from window in wings process
109    case ReturnSt of
110	true ->
111	    St = Fun(St0),
112	    {replace, change_state(Window, St)};
113	false ->
114	    Fun(St0)
115    end;
116forward_event({action,{view_win,Cmd}}, _Window, #st{views={_,Views0}}=St0) ->
117    case Cmd of
118        {save,Geom} -> %% New
119            wings_wm:send(Geom, {action, {view, {views, {save,true}}}});
120        {rename,_} ->
121            wings_wm:send(geom, {action, {view, {views, Cmd}}});
122        {replace,Idx} ->
123            {_,Legend} = element(Idx, Views0),
124            View = current(),
125            Views = setelement(Idx, Views0, {View, Legend}),
126            wings_wm:send(geom, {new_state, St0#st{views={Idx,Views}}});
127        {delete,_} ->
128            wings_wm:send(geom, {action, {view, {views, delete}}});
129        {delete_all,_} ->
130            wings_wm:send(geom, {action, {view, {views, delete_all}}})
131    end,
132    keep;
133forward_event({cursor, Cursor}, _, _) ->
134    wings_io:set_cursor(Cursor),
135    keep;
136forward_event(Ev, Window, _) ->
137    wx_object:cast(Window, Ev),
138    keep.
139
140get_view_state(#st{views=Views}) -> Views.
141
142%%%%%%%% View Window internals %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
143%% Inside window process
144%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
145
146-record(state, {lc, views, drag}).
147
148init([Frame, _Ps, VS]) ->
149    #{bg:=BG, text:=FG} = wings_frame:get_colors(),
150    Panel = wxPanel:new(Frame),
151    wxPanel:setFont(Panel, ?GET(system_font_wx)),
152    Szr = wxBoxSizer:new(?wxVERTICAL),
153    Style = ?wxLC_REPORT bor ?wxLC_NO_HEADER bor ?wxLC_EDIT_LABELS bor
154        ?wxLC_SINGLE_SEL bor wings_frame:get_border(),
155    LC = wxListCtrl:new(Panel, [{style, Style}]),
156    wxListCtrl:setBackgroundColour(LC, BG),
157    wxListCtrl:setForegroundColour(LC, FG),
158    wxSizer:add(Szr, LC, [{proportion,1}, {flag, ?wxEXPAND}]),
159    wxListCtrl:insertColumn(LC, 0, "", [{width, ?wxLIST_AUTOSIZE_USEHEADER}]),
160    wxPanel:setSizer(Panel, Szr),
161    update_views(VS, undefined, LC),
162    Self = self(),
163    IgnoreForPopup = fun(Ev,_) ->
164			     case wx_misc:getMouseState() of
165				 #wxMouseState{rightDown=true} -> ignore;
166				 _ -> Self ! Ev
167			     end
168		     end,
169    wxWindow:connect(LC, command_list_item_selected, [{callback, IgnoreForPopup}]),
170    wxWindow:connect(LC, command_list_item_activated),
171    wxWindow:connect(LC, enter_window, [{userData, {win, Panel}}]),
172    wxWindow:connect(LC, right_up),
173    case os:type() of %% Mouse right_up does not arrive on items in windows
174	{win32,nt} -> wxWindow:connect(LC, command_list_item_right_click);
175	_ -> ok
176    end,
177    wxWindow:connect(LC, command_list_end_label_edit),
178    wxWindow:connect(LC, command_list_begin_drag),
179    wxWindow:connect(LC, left_up, [{skip, true}]),
180    wxWindow:connect(LC, size, [{skip, true}]),
181    wxWindow:connect(LC, char, [callback]),
182    {Panel, #state{lc=LC, views=VS}}.
183
184handle_sync_event(#wx{obj=LC,event=#wxKey{type=char, keyCode=KC}}, EvObj, #state{lc=LC}) ->
185    Indx = wxListCtrl:getNextItem(LC, -1, [{geometry, ?wxLIST_NEXT_ALL}, {state, ?wxLIST_STATE_SELECTED}]),
186    case {key_to_op(KC), validate_param(Indx)} of
187	{Act,Param} when Act =/= ignore andalso Param =/=ignore ->
188	    wings_wm:psend(?WIN_NAME, {action, {view_win, {Act, Param+1}}});
189	_ -> wxEvent:skip(EvObj, [{skip, true}])
190    end,
191    ok.
192
193key_to_op(?WXK_DELETE) -> delete;
194key_to_op(?WXK_F2) -> rename;
195key_to_op(_) -> ignore.
196
197validate_param(-1) -> ignore;
198validate_param(Param) -> Param.
199
200handle_event(#wx{event=#wxList{type=command_list_end_label_edit, itemIndex=Indx}},
201	     #state{views={Curr, Views}, lc=LC} = State) ->
202    NewName = wxListCtrl:getItemText(LC, Indx),
203    if NewName =/= [] ->
204	case element(Indx+1, Views) of
205	    {_, NewName} -> ignore;
206	    {ViewInfo,_} ->
207		Rename = fun(#st{}=St0) ->
208				 St = St0#st{views={Curr, setelement(Indx+1, Views, {ViewInfo,NewName})}},
209				 wings_wm:send(geom, {new_state,St}),
210				 St0 %% Intentional so we get updates to window process
211			 end,
212		wings_wm:psend(?WIN_NAME, {apply, true, Rename})
213	end;
214    true ->
215	{_, Old} = element(Indx+1, Views),
216	wxListCtrl:setItemText(LC, Indx, Old)
217    end,
218    {noreply, State};
219
220handle_event(#wx{event=#wxSize{size={Width,_}}}, #state{lc=LC}=State) ->
221    wxListCtrl:setColumnWidth(LC, 0, Width-20),
222    {noreply, State};
223
224handle_event(#wx{event=#wxList{type=command_list_item_activated, itemIndex=Indx}},
225	     #state{views={_, Vs}}=State) ->
226    Indx >= 0 andalso wings_wm:psend(geom_focused(), {action, {view, {views, {jump,Indx+1}}}}),
227    {noreply, State#state{views={Indx+1, Vs}}};
228
229handle_event(#wx{event=#wxList{type=command_list_item_selected, itemIndex=Indx}},
230	     #state{views={_, Vs}}=State) ->
231    Indx >= 0 andalso wings_wm:psend(geom_focused(), {action, {view, {views, {jump,Indx+1}}}}),
232    {noreply, State#state{views={Indx+1, Vs}}};
233
234handle_event(#wx{event=#wxMouse{type=right_up}}, State) ->
235    invoke_menu(State),
236    {noreply, State};
237handle_event(#wx{event=#wxList{type=command_list_item_right_click}}, State) ->
238    invoke_menu(State),
239    {noreply, State};
240
241handle_event(#wx{event=#wxList{type=command_list_begin_drag, itemIndex=Indx}}, #state{lc=LC}=State) ->
242    wings_io:set_cursor(pointing_hand),
243    wxListCtrl:captureMouse(LC),
244    {noreply, State#state{drag=Indx}};
245handle_event(#wx{event=#wxMouse{type=left_up, x=X,y=Y}},
246	     #state{drag=Drag, views={_, Vs0}, lc=LC}=State) ->
247    case Drag of
248	undefined ->
249	    {noreply, State};
250	Drag ->
251	    Pos = {X,Y},
252	    wxListCtrl:releaseMouse(LC),
253	    wings_io:set_cursor(arrow),
254	    case handle_drop(hitTest(LC,Pos), Drag, Pos, Vs0, LC) of
255		false ->
256		    {noreply, State#state{drag=undefined}};
257		Vs ->
258		    Reorder = fun(#st{}=St0) ->
259				      St = St0#st{views=Vs},
260				      wings_wm:send(geom, {new_state,St}),
261				      St0 %% Intentional so we get updates to window process
262			      end,
263		    wings_wm:psend(?WIN_NAME, {apply, true, Reorder}),
264		    {noreply, State#state{drag=undefined}}
265	    end
266    end;
267
268handle_event(#wx{event=#wxMouse{type=enter_window}}=Ev, State) ->
269    wings_frame ! Ev,
270    {noreply, State};
271
272handle_event(#wx{} = _Ev, State) ->
273    %% io:format("~p:~p Got unexpected event ~p~n", [?WIN_NAME,?LINE, _Ev]),
274    {noreply, State}.
275
276hitTest(LC, Pos) ->
277    try wxListCtrl:hitTest(LC,Pos) of
278        {Index, _, _} -> Index
279    catch error:undef -> apply(wxListCtrl,hitTest, [LC,Pos, 0])
280    end.
281
282%%%%%%%%%%%%%%%%%%%%%%
283
284handle_call(_Req, _From, State) ->
285    %% io:format("~p:~p Got unexpected call ~p~n", [?WIN_NAME,?LINE, _Req]),
286    {reply, ok, State}.
287
288handle_cast({new_state, Views}, #state{lc=LC, views=Old}=State) ->
289    update_views(Views, Old, LC),
290    {noreply, State#state{views=Views}};
291handle_cast(_Req, State) ->
292    %% io:format("~p:~p Got unexpected cast ~p~n", [?WIN_NAME,?LINE, _Req]),
293    {noreply, State}.
294
295handle_info(_Msg, State) ->
296    %% io:format("~p:~p Got unexpected info ~p~n", [?WIN_NAME,?LINE, _Msg]),
297    {noreply, State}.
298
299%%%%%%%%%%%%%%%%%%%%%%
300
301code_change(_From, _To, State) ->
302    State.
303
304terminate(_Reason, _) ->
305    wings ! {wm, {delete, ?WIN_NAME}},
306    normal.
307
308%%%%%%%%%%%%%%%%%%%%%%%%%
309
310update_views(Old, Old, _LC) -> Old;
311update_views({Indx, Tuple}=New, _Old, LC) ->
312    Add = fun({_View,Name}, Id) ->
313		  wxListCtrl:insertItem(LC, Id, Name),
314		  Id+1
315	  end,
316    wxListCtrl:deleteAllItems(LC),
317    wx:foldl(Add, 0, tuple_to_list(Tuple)),
318    wxListCtrl:setItemState(LC, Indx-1, 16#FFFF, ?wxLIST_STATE_SELECTED),
319    New.
320
321get_selection(LC) ->
322    case wxListCtrl:getSelectedItemCount(LC) of
323	0 -> none;
324	1 ->
325	    Opts = [{geometry,?wxLIST_NEXT_ALL}, {state, ?wxLIST_STATE_SELECTED}],
326	    wxListCtrl:getNextItem(LC, -1, Opts)
327    end.
328
329handle_drop(-1, Drop, {X,Y}=Pos, Vs, LC) ->
330    {W,_H} = wxListCtrl:getClientSize(LC),
331    if 0 > X -> false;
332       X > W -> false;
333       0 > Y -> handle_drop(0, Drop, Pos, Vs, LC);
334       true  -> handle_drop(tuple_size(Vs), Drop, Pos, Vs, LC)
335    end;
336handle_drop(Drop, Drop, _Pos, _Vs0, _LC) -> false;
337handle_drop(Hit, Drop, _Pos, Vs0, _LC) ->
338    DroppedItem = element(Drop+1, Vs0),
339    {Hit+1, list_to_tuple(insert_to_list(0, Hit, Drop, DroppedItem, tuple_to_list(Vs0)))}.
340
341insert_to_list(Here, Here, Drop, Item, [Next|Vs]) ->
342    [Item,Next|insert_to_list(Here+1,Here,Drop,Item,Vs)];
343insert_to_list(Drop, Pos, Drop, Item, [_|Vs]) ->
344    insert_to_list(Drop+1, Pos, Drop, Item, Vs);
345insert_to_list(Indx, Here, Drop, Item, [This|Vs]) ->
346    [This|insert_to_list(Indx+1, Here, Drop, Item, Vs)];
347insert_to_list(Indx, Here, _Drop, Item, []) ->
348    case Here =:= Indx of
349	true -> [Item];
350	false -> []
351    end.
352
353invoke_menu(#state{views=Views, lc=LC}) ->
354    Menus = get_menus(get_selection(LC), Views),
355    Pos = wx_misc:getMousePosition(),
356    Cmd = fun(_) -> wings_menu:popup_menu(LC, Pos, view_win, Menus) end,
357    wings_wm:psend(?WIN_NAME, {apply, false, Cmd}).
358
359get_menus(_, {_, {}}) ->
360    views_menu({new,geom_focused()});
361get_menus(none, _) ->
362    views_menu({new,geom_focused()}) ++ [separator|views_menu(delete_all)];
363get_menus(Indx, {_,Views}) ->
364    {_,Legend} = element(Indx+1, Views),
365    views_menu({Indx+1, Legend}) ++ [separator|get_menus(none, undefined)].
366
367views_menu({new,Geom}) ->
368    [{?__(1,"Save New..."),menu_cmd(save,Geom), ?__(2,"Create a new saved view")}];
369views_menu(delete_all) ->
370    [{?__(9,"Delete All..."), menu_cmd(delete_all,all), ?__(10,"Delete all saved views")}];
371views_menu({Idx, Legend}) ->
372    [{?__(3,"Replace "),menu_cmd(replace,Idx),
373      ?__(4,"Replaces \"")++Legend++"\"["++integer_to_list(Idx)++"]\" settings with the current viewing ones"},
374     {?__(5,"Rename..."), menu_cmd(rename,Idx),
375      ?__(6,"Rename \"")++Legend++"\"["++integer_to_list(Idx)++"]\"",
376      [{hotkey,wings_hotkey:format_hotkey({?SDLK_F2,[]},pretty)}]},
377     {?__(7,"Delete"), menu_cmd(delete,Idx),
378      ?__(8,"Delete \"")++Legend++"\"["++integer_to_list(Idx)++"]\"",
379      [{hotkey,wings_hotkey:format_hotkey({?SDLK_DELETE,[]},pretty)}]}].
380
381menu_cmd(Cmd, Id) ->
382    {'VALUE',{Cmd,Id}}.
383
384current() ->
385    wings_wm:get_prop(geom_focused(), current_view).
386
387geom_focused() -> geom.
388