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