%%
%%  wings_camera.erl --
%%
%%     This module handles camera moves (rotation, zooming, and panning).
%%
%%  Copyright (c) 2001-2011 Bjorn Gustavsson
%%
%%  See the file "license.terms" for information on usage and redistribution
%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
%%
%%     $Id$
%%

-module(wings_camera).
-export([init/0,prefs/0,help/0,event/2,event/3]).

-export([tweak_camera_event/4]).

-define(NEED_ESDL, 1).
-include("wings.hrl").

-define(ZOOM_FACTOR, 20).
-define(ZOOM_FACTOR_ALT, 1).

-define(CAMDIV, 4).

-define(CSEP, 160).				%Short space.
-define(NEARZERO, 1.0e-6).
-record(camera,
	{x,y,					%Current mouse position.
	 ox,oy,					%Original mouse position.
	 xt=0,yt=0,				%Last warp length.
         w,h                                    %Win size
	}).

-record(state, {st, func}).

init() ->
    wings_pref:set_default(camera_mode, wings_cam),
    wings_pref:set_default(num_buttons, 3),
    wings_pref:set_default(cam_rotation_speed, 25),
    wings_pref:set_default(pan_speed, 25),
    wings_pref:set_default(pan_speed_arrow_keys, 50),
    wings_pref:set_default(wheel_adds, false),
    wings_pref:set_default(wh_scroll_info,true),
    wings_pref:set_default(wh_pan_spd, 50),
    wings_pref:set_default(wh_rot_spd, 7.50),
    wings_pref:set_default(highlight_zoom_aim, false),
    case {wings_pref:get_value(num_buttons),wings_pref:get_value(camera_mode)} of
	{3,_} -> ok;
	{_,nendo} -> ok;
	{_,blender} -> ok;
	{_,_} -> wings_pref:set_value(camera_mode, nendo)
    end.
    
prefs() ->
    ZoomFlag0 = wings_pref:get_value(wheel_zooms, true),
    ZoomFactor0 = wings_pref:get_value(wheel_zoom_factor, ?ZOOM_FACTOR),
    ZoomFactorAlt = wings_pref:get_value(wheel_zoom_factor_alt, ?ZOOM_FACTOR_ALT),
    CamRotSpeed = wings_pref:get_value(cam_rotation_speed, 25),
    PanSpeed0 = wings_pref:get_value(pan_speed, 25),
    ArrowPanSpeed = wings_pref:get_value(pan_speed_arrow_keys, 50),
    WheelAdds = wings_pref:get_value(wheel_adds,true),
    WhScrollInfo = wings_pref:get_value(wh_scroll_info,true),
    WhPanSpd = wings_pref:get_value(wh_pan_spd, 50),
    WhRotate = wings_pref:get_value(wh_rot_spd, 7.5),
    ZoomAim = wings_pref:get_value(highlight_zoom_aim),

    AddHook = fun(wheel_adds, Enable0, Sto) ->
		      Keys = [wh_scroll_info, wh_pan_spd, wh_rot_spd],
		      Enable = Enable0 andalso wings_dialog:get_value(wheel_zooms, Sto),
		      wings_dialog:enable(Keys, Enable, Sto)
	      end,
    WheelHook = fun(wheel_zooms, Enable, Sto) ->
			Keys = [wheel_zoom_factor,wheel_zoom_factor_alt,wheel_adds],
			wings_dialog:enable(Keys, Enable, Sto),
			Keys2 = [wh_scroll_info, wh_pan_spd, wh_rot_spd],
			Enable2 = Enable andalso wings_dialog:get_value(wheel_adds, Sto),
			wings_dialog:enable(Keys2, Enable2, Sto)
		end,

    {hframe,
     [{vframe,
       [{vframe,[mouse_buttons()],[{title,?__(1,"Mouse Buttons")}]},
	{vframe,[camera_modes()],[{title,?__(2,"Camera Mode")}]},
	{vframe,
	 [{hframe,[{slider,{text,CamRotSpeed,[{key,cam_rotation_speed},{range,{1,100}}]}}]}],
	 [{title,?__(19,"Rotation Speed")}]},
	{vframe,
	 [{hframe,[{slider,{text,PanSpeed0,[{key,pan_speed},{range,{1,100}}]}}]}],
	 [{title,?__(3,"Pan Speed")}]},
	{vframe,
	 [{hframe,[{slider,{text,ArrowPanSpeed,[{key,pan_speed_arrow_keys},{range,{1,100}}]}}]}],
	 [{title,?__(16,"Arrow Key Pan Speed")}]}]},
      {vframe,
       [{vframe,
	 [{hframe,[{?__(4,"Wheel Zooms"),ZoomFlag0,[{key,wheel_zooms}, {hook, WheelHook}]},
		   {?__(20,"Zooming in aims Camera"),ZoomAim,[{key,highlight_zoom_aim}]}]},
	  {hframe,
	   [{label,?__(7,"Zoom Factor")},
	    {text,ZoomFactor0, [{key,wheel_zoom_factor},{range,{1,50}}]},
	    {label,"%"}]},
	  {hframe,
	   [{label,?__(10,"Alternate Zoom Factor(Alt+Scroll)")},
	    {text,ZoomFactorAlt, [{key,wheel_zoom_factor_alt},{range,{1,50}}]},
	    {label,"%"}]}
	 ],[{title,?__(9,"Scroll Wheel")}]},
	{vframe,
	 [{?__(11,"Wheel Pans and Rotates"),WheelAdds,[{key,wheel_adds}, {hook, AddHook}]},
	  {?__(17,"Show Info Line Help String"),WhScrollInfo,[{key,wh_scroll_info}]},
	  {vframe,
	   [{hframe,
	     [{slider,{text,WhPanSpd,[{key, wh_pan_spd},{range,{1,100}}]}}],
	     [{title,?__(12,"Pan Speed")}]},
	    {hframe,
	     [{slider,{text,WhRotate,[{key, wh_rot_spd},{range,{?NEARZERO,180.0}}]}}],
	     [{title,?__(13,"Rotation Step in Degrees")}]}]}],
	 [{title,?__(15,"Unidirectional Camera")}]}]}]}.

mouse_buttons() ->
    {menu,[{desc(1),1,[{info,info(1)}]},
           {desc(2),2,[{info,info(2)}]},
           {desc(3),3,[{info,info(3)}]}],
     wings_pref:get_value(num_buttons),
     [{key,num_buttons},
      {hook,fun (_,Val,Sto) ->
		    Mode0 = wings_dialog:get_value(camera_mode, Sto),
		    Mode  = case {Val,Mode0} of
				{1,_} -> nendo;
				{2,blender} -> blender;
				{2,_} -> nendo;
				{3,_} -> Mode0
			    end,
		    wings_dialog:set_value(camera_mode, {Mode, camera_modes(Val)}, Sto)
	    end}]}.

camera_modes() ->
    {menu,camera_modes(wings_pref:get_value(num_buttons)),
     wings_pref:get_value(camera_mode),
     [{key,camera_mode}]}.

camera_modes(NumButtons) ->
    All = [wings_cam,mirai,nendo,maya,tds,blender,mb,sketchup],
    [{desc(Mode),Mode,[{info,info(Mode)}]} || Mode <- All -- disabled(NumButtons)].

disabled(1) -> [mirai,maya,tds,blender,mb,sketchup,wings_cam];
disabled(2) -> [mirai,maya,tds,mb,sketchup,wings_cam];
disabled(3) -> [].

desc(1) -> ?__(1,"One");
desc(2) -> ?__(2,"Two");
desc(3) -> ?__(3,"Three");
desc(Other) -> wings_s:camera_mode(Other).

info(1) ->
    ?__(1,"Note: Only the Nendo camera mode can be used with a one-button mouse");
info(2) ->
    ?__(2,"Note: Only the Nendo and Blender camera modes can be used with a two-button mouse");
info(3) -> "";
info(nendo) -> "";
info(blender) ->
    [?__(3,"Note: The "),desc(blender),
     ?__(4," camera mode requires at least 2 mouse buttons")];
info(Mode) ->
    [?__(5,"Note: The "),desc(Mode),
     ?__(6," camera mode requires 3 mouse buttons")].

help() ->
	Help = case wings_pref:get_value(camera_mode) of
	  blender -> blender_help();
	  nendo -> wings_msg:button_format([], ?__(1,"Start camera"));
	  mirai -> wings_msg:button_format([], ?__(1,"Start camera"));
	  wings_cam -> wings_msg:button_format([], ?__(1,"Start camera"));
	  tds -> tds_help();
	  maya -> maya_help();
	  mb -> mb_help();
	  sketchup -> sketchup_help()
	end,
	WheelAdds = wings_pref:get_value(wheel_adds),
	WhScrollHelp = wings_pref:get_value(wh_scroll_info),
	case {WheelAdds,WhScrollHelp} of
	  {true,true} -> [Help|scroll_help()];
	  {_,_} -> Help
	end.
scroll_help() ->
    ?__(1,"  [Ctrl](+[Alt])+Scroll: Pan  [Shift](+[Alt])+Scroll: Rotate").

%% Event handler.
event(Ev, St=#st{}) -> 
    event(Ev,St,none).
%% Scroll wheel camera events
event(#mousewheel{}=Ev, _St, _Redraw) ->
    generic_event(Ev,_St,_Redraw);
% Camera mode specific events
event(Ev, St, Redraw) ->
    case wings_pref:get_value(camera_mode) of
	wings_cam -> wings_cam(Ev, #state{st=St, func=Redraw});
	blender -> blender(Ev, #state{st=St, func=Redraw});
	nendo -> nendo(Ev, #state{st=St, func=Redraw});
	mirai -> mirai(Ev, #state{st=St, func=Redraw});
	tds -> tds(Ev, #state{st=St, func=Redraw});
	maya -> maya(Ev, #state{st=St, func=Redraw});
	mb -> mb(Ev, #state{st=St, func=Redraw});
	sketchup -> sketchup(Ev, #state{st=St, func=Redraw})
    end.

%%%
%%% Tweak Camera
%%%

tweak_camera_event(Sym, X, Y, St) when Sym =:= $c; Sym =:= $s; Sym =:= $d ->
    Camera = start_camera(X,Y),
    {seq,push,get_tweak_cam_event(Sym, Camera, St)};
tweak_camera_event(Sym, _, _, _) ->
    arrow_key_pan(Sym).

get_tweak_cam_event(Sym, Camera, St) ->
    {replace,fun(Ev) -> tweak_cam_event(Ev, Sym, Camera, St) end}.

tweak_cam_event(#mousemotion{x=X0,y=Y0}, Sym, Camera0, St) ->
    case wings_io:is_key_pressed(Sym) of
      true  ->
          {Dx,Dy,Camera} = camera_mouse_range(X0, Y0, Camera0),
          case Sym of
              $c -> rotate(Dx,Dy);
              $s -> pan(Dx,Dy);
              $d -> zoom(Dy)
          end,
          wings_wm:dirty(),
          get_tweak_cam_event(Sym, Camera, St);
      false ->
        quit_tweak_cam()
     end;
tweak_cam_event(#mousebutton{button=B,state=?SDL_RELEASED}=Ev, Sym, Camera, St)
  when B =< 3->
    case wings_io:is_key_pressed(Sym) of
      true ->
        generic_event(Ev, Camera, #state{st=St, func=none});
      false ->
        quit_tweak_cam()
     end;
tweak_cam_event(Ev, Sym, Camera, St) ->
    case wings_io:is_key_pressed(Sym) of
      true ->
        generic_event(Ev, Camera, #state{st=St, func=none});
      false ->
        quit_tweak_cam()
     end.

quit_tweak_cam() ->
    case wings_io:get_mouse_state() of
	{0,X0,Y0} ->
	    {X,Y} = wings_wm:screen2local({X0,Y0}),
	    wings_wm:later(#mousebutton{which=wings_wm:this_win(),
					button=1,x=X,y=Y,mod=0,state=?SDL_RELEASED}),
	    pop;
	_ ->
	    pop
    end.

%%%
%%% Blender style camera.
%%%

blender(#mousebutton{button=2,state=?SDL_PRESSED,x=X,y=Y,mod=Mod}, Redraw)
  when Mod band ?ALT_BITS =:= 0 ->
    Camera = start_camera(X,Y),
    grab(),
    message(blender_help()),
    {seq,push,get_blender_event(Camera, Redraw)};
blender(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
blender(_, _) -> next.

blender_event(#mousebutton{button=2,state=?SDL_RELEASED}, Camera, _Redraw) ->
    stop_camera(Camera);
blender_event(#mousemotion{x=X,y=Y,mod=Mod}, Camera0, Redraw) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    case Mod of
      Mod when Mod band ?SHIFT_BITS =/= 0 ->
          pan(Dx, Dy);
      Mod when Mod band ?CTRL_BITS =/= 0 ->
          zoom(Dy);
      _Other ->
          rotate(Dx, Dy)
    end,
    wings_wm:dirty(),
    get_blender_event(Camera, Redraw);
blender_event(Other, Camera, Redraw) ->
    generic_event(Other, Camera, Redraw).

get_blender_event(Camera, Redraw) ->
    {replace,fun(Ev) -> blender_event(Ev, Camera, Redraw) end}.

blender_help() ->
    TrackDolly = [{?SHIFT_BITS,2,?STR(mode_help,1,"Track")},
		  {?CTRL_BITS,2,?STR(mode_help,2,"Dolly")}],
    case allow_rotation() of
      false -> format(TrackDolly);
      true -> format([{0,2,?STR(mode_help,3,"Tumble")}|TrackDolly])
    end.

%%%
%%% Nendo style camera.
%%%

nendo(#mousebutton{button=2,x=X,y=Y,mod=Mod,state=?SDL_RELEASED}, Redraw)
  when Mod band ?CTRL_BITS =:= 0 ->
    Camera = start_camera(X,Y),
    grab(),
    MoveTumbles = allow_rotation(),
    nendo_message(MoveTumbles),
    {seq,push,get_nendo_event(Camera, Redraw, MoveTumbles)};
nendo(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
nendo(_, _) -> next.

nendo_event(#mousebutton{button=1,state=?SDL_RELEASED}, Camera, _, _) ->
    stop_camera(Camera);
nendo_event(#mousemotion{x=X,y=Y,state=Buttons}, Camera0, Redraw, true) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    case Buttons band 6 of
	0 ->
	    rotate(-Dx, -Dy);
	_Other ->
	    zoom(Dy)
    end,
    get_nendo_event(Camera, Redraw, true);
nendo_event(#mousemotion{x=X,y=Y,state=Buttons}, Camera0, Redraw, false) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    case Buttons band 6 of
	0 ->
	    pan(-Dx, -Dy);
	_Other ->
	    zoom(Dy)
    end,
    get_nendo_event(Camera, Redraw, false);
nendo_event(#keyboard{unicode=$q}, Camera, Redraw, MR0) ->
    MR = MR0 xor allow_rotation(),
    nendo_message(MR),
    get_nendo_event(Camera, Redraw, MR);
nendo_event(#keyboard{sym=Sym}=Event, Camera, Redraw, _) ->
    case arrow_key_pan(Sym) of
	keep -> keep;
	next -> view_hotkey(Event, Camera, Redraw)
    end;
nendo_event(Event, Camera, Redraw, _) ->
    generic_event(Event, Camera, Redraw).
    
get_nendo_event(Camera, Redraw, MouseRotates) ->
    wings_wm:dirty(),
    {replace,fun(Ev) -> nendo_event(Ev, Camera, Redraw, MouseRotates) end}.

nendo_message(true) ->
    Help = wings_msg:join([wings_msg:button_format(wings_s:accept(),[],
                                                   ?STR(message,2,"Drag to Dolly")),
                           ?STR(message,3,"Move mouse to tumble"),
                           [?STR(message,4,"[Q]"),?CSEP,
                            ?STR(message,5,"Move mouse to track")]]),
    message(Help);
nendo_message(false) ->
    QText = case allow_rotation() of
		false -> [];
		true -> [?STR(message,4,"[Q]"),?CSEP,
			 ?STR(message,3,"Move mouse to tumble")]
	    end,
    Help = wings_msg:join([wings_msg:button_format(wings_s:accept(),[],
						   ?STR(message,2,"Drag to Dolly")),
			         QText]),
    message(Help).

%%%
%%% Mirai style camera.
%%%

mirai(#mousebutton{button=2,x=X,y=Y,mod=Mod,state=?SDL_RELEASED}, Redraw)
  when Mod band ?CTRL_BITS =:= 0 ->
    Camera = start_camera(X,Y),
    grab(),
    MoveTumbles = allow_rotation(),
    mirai_message(MoveTumbles),
    View = wings_view:current(),
    {seq,push,get_mirai_event(Camera, Redraw, MoveTumbles, View)};
mirai(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
mirai(_, _) -> next.

mirai_event(#mousebutton{button=1,state=?SDL_RELEASED}, Camera, _, _, _) ->
    stop_camera(Camera);
mirai_event(#mousebutton{button=3,state=?SDL_RELEASED}, Camera, _, _, View) ->
    wings_view:set_current(View),
    stop_camera(Camera);
mirai_event(#mousemotion{x=X,y=Y,state=Buttons}, Camera0, Redraw, true, View) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    case Buttons band 2 of
	0 ->					%MMB not pressed.
	    rotate(-Dx, -Dy);
	_Other ->				%MMB pressed.
	    zoom(Dy)
    end,
    get_mirai_event(Camera, Redraw, true, View);
mirai_event(#mousemotion{x=X,y=Y,state=Buttons}, Camera0, Redraw, false, View) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    case Buttons band 2 of
	0 ->					%MMB pressed.
	    pan(-Dx, -Dy);
	_Other ->				%MMB pressed.
	    zoom(Dy)
    end,
    get_mirai_event(Camera, Redraw, false, View);
mirai_event(#keyboard{unicode=$q}, Camera, Redraw, MR0, View) ->
    MR = MR0 xor allow_rotation(),
    mirai_message(MR),
    get_mirai_event(Camera, Redraw, MR, View);
mirai_event(#keyboard{sym=Sym}=Event, Camera, Redraw, _, _) ->
    case arrow_key_pan(Sym) of
	keep -> keep;
	next -> view_hotkey(Event, Camera, Redraw)
    end;
mirai_event(Event, Camera, Redraw, _, _) ->
    generic_event(Event, Camera, Redraw).

get_mirai_event(Camera, Redraw, MouseRotates, View) ->
    wings_wm:dirty(),
    {replace,fun(Ev) -> mirai_event(Ev, Camera, Redraw, MouseRotates, View) end}.

mirai_message(true) ->
    Help = wings_msg:join([wings_msg:button_format(wings_s:accept(),
				   ?STR(message,2,"Drag to Dolly"),
				   ?STR(message,6,"Cancel/restore view")),
				   ?STR(message,3,"Move mouse to tumble"),
				  [?STR(message,4,"[Q]"),?CSEP,
				   ?STR(message,5,"Move mouse to track")]]),
    message(Help);
mirai_message(false) ->
    QText = case allow_rotation() of
		false -> [];
		true -> [?STR(message,4,"[Q]"),?CSEP,?STR(message,3,"Move mouse to tumble")]
	    end,
    Help = wings_msg:join([wings_msg:button_format(wings_s:accept(),
				   ?STR(message,2,"Drag to Dolly"),
				   ?STR(message,6,"Cancel/restore view")),
				   ?STR(message,5,"Move mouse to track"),QText]),
    message(Help).

%%%
%%% 3ds max style camera.
%%%

tds(#mousebutton{button=2,x=X,y=Y,state=?SDL_PRESSED}, Redraw) ->
    Camera = #camera{x=X,y=Y,ox=X,oy=Y},
    grab(),
    message(wings_msg:join(tds_help(), 
			   wings_msg:button_format([], [],
			   ?STR(message,7,"Restore view")))),
    View = wings_view:current(),
    {seq,push,get_tds_event(Camera, Redraw, View)};
tds(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
tds(_, _) -> next.

tds_event(#mousebutton{button=1,state=?SDL_RELEASED}=Mb, Camera, Redraw, View) ->
    tds_event(Mb#mousebutton{button=2}, Camera, Redraw, View);
tds_event(#mousebutton{button=2,state=?SDL_RELEASED}, Camera, _, _) ->
    stop_camera(Camera);
tds_event(#mousebutton{button=3,state=?SDL_RELEASED}, Camera, _, View) ->
    wings_view:set_current(View),
    stop_camera(Camera);
tds_event(#mousemotion{x=X,y=Y,mod=Mod}, Camera0, Redraw, View) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    if
	Mod band ?CTRL_BITS =/= 0, Mod band ?ALT_BITS =/= 0 ->
	    zoom(Dy);
	Mod band ?ALT_BITS =/= 0 ->
	    rotate(Dx, Dy);
	true ->
	    pan(Dx, Dy)
    end,
    get_tds_event(Camera, Redraw, View);
tds_event(Event, Camera, Redraw, _) ->
    generic_event(Event, Camera, Redraw).

get_tds_event(Camera, Redraw, View) ->
    wings_wm:dirty(),
    {replace,fun(Ev) -> tds_event(Ev, Camera, Redraw, View) end}.

tds_help() ->
    TrackDolly = [{0,2,?STR(mode_help,1,"Track")},
		  {?CTRL_BITS bor ?ALT_BITS,2,?STR(mode_help,2,"Dolly")}],
    case allow_rotation() of
	false -> format(TrackDolly);
	true -> format([{?ALT_BITS,2,?STR(mode_help,3,"Tumble")}|TrackDolly])
    end.

%%%
%%% Maya style camera.
%%%

maya(#mousebutton{x=X,y=Y,mod=Mod,state=?SDL_PRESSED}, Redraw)
  when Mod band ?ALT_BITS =/= 0, Mod band ?CTRL_BITS =:= 0, Mod band ?SHIFT_BITS =:= 0 ->
    wings_io:change_event_handler(?SDL_KEYUP, true),
    Camera = #camera{x=X,y=Y,ox=X,oy=Y},
    grab(),
    message(maya_help()),
    {seq,push,get_maya_event(Camera, Redraw)};
maya(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
maya(_, _) -> next.

maya_event(#keyboard{sym=Alt,state=?SDL_RELEASED},
	   Camera, _Redraw) when Alt =:= ?SDLK_LALT; Alt =:= ?SDLK_RALT ->
    maya_stop_camera(Camera);
maya_event(#mousebutton{button=B,state=?SDL_RELEASED}, Camera, _)
  when B < 4 ->
    case wings_io:get_mouse_state() of
	{0,_,_} ->
	    %% Exit camera mode if all mouse buttons released.
	    maya_stop_camera(Camera);
	_ ->
	    %% Some mouse button still pressed.
	    keep
    end;
maya_event(#mousemotion{x=X,y=Y,state=Buttons}, Camera0, Redraw) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    if
	Buttons band 4 =:= 4 ->			%RMB
	    zoom(-Dx);
	Buttons band 3 =:= 3 ->			%LMB+MMB
	    zoom(-Dx);
	Buttons band 1 =:= 1 ->			%LMB
	    rotate(Dx, Dy);
	Buttons band 2 =:= 2 ->			%MMB
	    pan(Dx, Dy);
	true -> ok
    end,
    get_maya_event(Camera, Redraw);
maya_event(Event, Camera, Redraw) ->
    generic_event(Event, Camera, Redraw).

get_maya_event(Camera, Redraw) ->
    wings_wm:dirty(),
    {replace,fun(Ev) -> maya_event(Ev, Camera, Redraw) end}.

maya_stop_camera(Camera) ->
    wings_io:change_event_handler(?SDL_KEYUP, false),
    stop_camera(Camera).

maya_help() ->
    TrackDolly = [{?ALT_BITS,2,?STR(mode_help,1,"Track")},
		  {?ALT_BITS,3,?STR(mode_help,2,"Dolly")}],
    case allow_rotation() of
	false -> format(TrackDolly);
	true -> format([{?ALT_BITS,1,?STR(mode_help,3,"Tumble")}|TrackDolly])
    end.

%%%
%%% Motionbuilder style camera.
%%%

mb(#mousebutton{button=1,mod=Mod,x=X,y=Y,state=?SDL_PRESSED}, Redraw)
  when Mod band (?SHIFT_BITS bor ?CTRL_BITS) =/= 0 ->
    Camera = #camera{x=X,y=Y,ox=X,oy=Y},
    grab(),
    message(mb_help()),
    {seq,push,get_mb_event(Camera, Redraw)};
mb(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
mb(_, _) -> next.

mb_event(#mousebutton{button=1,state=?SDL_RELEASED}, Camera, _) ->
    stop_camera(Camera);
mb_event(#mousemotion{x=X,y=Y,mod=Mod}, Camera0, Redraw) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    if
	Mod band ?CTRL_BITS =/= 0, Mod band ?SHIFT_BITS =/= 0 ->
	    rotate(Dx, Dy),
	    get_mb_event(Camera, Redraw);
	Mod band ?CTRL_BITS =/= 0 ->
	    zoom(Dy),
	    get_mb_event(Camera, Redraw);
	Mod band ?SHIFT_BITS =/= 0 ->
	    pan(Dx, Dy),
	    get_mb_event(Camera, Redraw);
	true ->
	    stop_camera(Camera)
    end;
mb_event(Event, Camera, Redraw) ->
    generic_event(Event, Camera, Redraw).

get_mb_event(Camera, Redraw) ->
    wings_wm:dirty(),
    {replace,fun(Ev) -> mb_event(Ev, Camera, Redraw) end}.

mb_help() ->
    TrackDolly = [{?SHIFT_BITS,1,?STR(mode_help,1,"Track")},
		  {?CTRL_BITS,1,?STR(mode_help,2,"Dolly")}],
    case allow_rotation() of
	false -> format(TrackDolly);
	true -> format([{?SHIFT_BITS bor ?CTRL_BITS,1,?STR(mode_help,3,"Tumble")}|TrackDolly])
    end.
    
%%%
%%% Sketchup style camera.
%%%

sketchup(#mousebutton{button=2,state=?SDL_PRESSED,x=X,y=Y,mod=Mod}, Redraw)
  when (Mod band (?ALT_BITS  bor ?CTRL_BITS)) =:= 0  ->
    Camera = #camera{x=X,y=Y,ox=X,oy=Y},
    grab(),
    message(sketchup_help()),
    {seq,push,get_sketchup_event(Camera, Redraw)};
sketchup(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
sketchup(_, _) -> next.

sketchup_event(#mousebutton{button=2,state=?SDL_RELEASED}, Camera, _Redraw) ->
    stop_camera(Camera);
sketchup_event(#mousemotion{x=X,y=Y,mod=Mod}, Camera0, Redraw) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    case Mod of
      Mod when Mod band ?SHIFT_BITS =/= 0 ->
          pan(Dx, Dy);
      _Other ->
          rotate(Dx, Dy)
    end,
    wings_wm:dirty(),
    get_sketchup_event(Camera, Redraw);
sketchup_event(Other, Camera, Redraw) ->
    generic_event(Other, Camera, Redraw).

get_sketchup_event(Camera, Redraw) ->
    {replace,fun(Ev) -> sketchup_event(Ev, Camera, Redraw) end}.

sketchup_help() ->
    TrackDolly = [{?SHIFT_BITS,2,?STR(mode_help,1,"Track")},
        {0,?CSEP,?STR(mode_help,4,"Scroll: Dolly")}],
    case allow_rotation() of
      false -> format(TrackDolly);
      true -> format([{0,2,?STR(mode_help,3,"Tumble")}|TrackDolly])
    end.

%%%
%%% Wings 3D camera suggested by oort
%%%

wings_cam(#mousebutton{button=2,x=X,y=Y,mod=Mod,state=?SDL_RELEASED}, Redraw)
  when Mod band (?CTRL_BITS bor ?SHIFT_BITS bor ?ALT_BITS) =:= 0 ->
    Camera = start_camera(X,Y),
    grab(),
    wings_cam_message(),
    View = wings_view:current(),
    {seq,push,get_wings_cam_event(Camera, Redraw, View)};
wings_cam(#keyboard{sym=Sym}, _Redraw) ->
    arrow_key_pan(Sym);
wings_cam(_, _) -> next.

wings_cam_event(#mousebutton{button=1,state=?SDL_RELEASED}, Camera, _, _) ->
    stop_camera(Camera);
wings_cam_event(#mousebutton{button=3,state=?SDL_RELEASED}, Camera, _, View) ->
    wings_view:set_current(View),
    stop_camera(Camera);
wings_cam_event(#mousemotion{x=X,y=Y,mod=Mod,state=Buttons}, Camera0, Redraw, View) ->
    {Dx,Dy,Camera} = camera_mouse_range(X, Y, Camera0),
    if
        (Mod band ?CTRL_BITS) =/= 0 ->
            zoom(Dy);
        (Buttons band 2) =:= 0 ->  %MMB not pressed.
            rotate(-Dx, -Dy);
        true ->			   %MMB pressed.
            pan(-Dx, -Dy)
    end,
    get_wings_cam_event(Camera, Redraw, View);
wings_cam_event(#keyboard{sym=Sym}=Event, Camera, Redraw, _) ->
    case arrow_key_pan(Sym) of
        keep -> keep;
        next -> view_hotkey(Event, Camera, Redraw)
    end;
wings_cam_event(Event, Camera, Redraw, _) ->
    generic_event(Event, Camera, Redraw).

get_wings_cam_event(Camera, Redraw, View) ->
    wings_wm:dirty(),
    {replace,fun(Ev) -> wings_cam_event(Ev, Camera, Redraw, View) end}.

wings_cam_message() ->
    Help = wings_msg:join([wings_msg:button_format(wings_s:accept(),
                                                   ?STR(message,8,"Drag to Pan"),
                                                   ?STR(message,6,"Cancel/restore view")),
                           ?STR(message,3,"Move mouse to tumble"),
                           wings_msg:mod_name(?CTRL_BITS) ++ " " ++
                               ?STR(message,2,"Drag to Dolly")]),
    message(Help).

%%%
%%% Common utilities.
%%%

generic_event(redraw, _Camera, #state{st=St, func=none}) ->
    wings:redraw(St),
    keep;
generic_event(redraw, _Camera, #state{func=Redraw}) when is_function(Redraw) ->
    Redraw(),
    keep;

generic_event(#mousewheel{dir=ver, wheel=N, mod=Mod}, Camera, Redraw) ->
    %% ?dbg("Wheel: ~w  shift: ~p   alt ~p   ctrl: ~p~n",
    %%      [N,Mod band ?SHIFT_BITS =/= 0,Mod band ?ALT_BITS =/= 0, Mod band ?CTRL_BITS =/= 0]),
    if Mod band ?SHIFT_BITS =/= 0 andalso Mod band ?ALT_BITS =/= 0 ->
            whrotate(N/2.0,0.0);
       Mod band ?SHIFT_BITS =/= 0 ->
            whrotate(0.0,N/2.0);
       Mod band ?CTRL_BITS =/= 0 andalso Mod band ?ALT_BITS =/= 0 ->
            whpan(N/20.0,0.0);
       Mod band ?CTRL_BITS =/= 0 ->
            whpan(0.0,N/20.0);
       Mod band ?ALT_BITS =/= 0 ->
            zoom_step_alt(-N);
       is_record(Camera, st), Redraw =:= none ->
            %% Matching 'none' stops zoom aim from being activated during a drag sequence.
            %% Zoom aim warps the mouse to the screen's centre, and this can cause a crash
            %% in since drag events also depend on cursor position.
            aim_zoom(-N, Camera);
       true ->
            zoom_step(-N)
    end;
generic_event(#mousewheel{dir=hor, wheel=N, mod=Mod}, _Camera, _Redraw) ->
    %% ?dbg("Wheel: ~w  shift: ~p   alt ~p   ctrl: ~p~n",
    %%      [N,Mod band ?SHIFT_BITS =/= 0,Mod band ?ALT_BITS =/= 0, Mod band ?CTRL_BITS =/= 0]),
    if
        Mod band ?CTRL_BITS =/= 0 andalso Mod band ?ALT_BITS =/= 0 ->
            whpan(0.0,N/20.0);
        Mod band ?CTRL_BITS =/= 0 ->
            whpan(N/20.0,0.0);
        Mod band ?ALT_BITS =/= 0 ->
            whrotate(0.0,N/2.0);
        true ->  %% rotate X whether SHIFT is pressed or not
            do_whrotate(N/2.0,0.0)
    end;
generic_event(grab_lost, Camera, _Redraw) ->
    stop_camera(Camera);
generic_event(_, _, _) -> keep.

aim_zoom(Dir, St0) ->
    case wings_pref:get_value(highlight_zoom_aim) of
      true ->
        #view{origin=OriginB}=Before = wings_view:current(),
        {{_,Cmd},_} = wings:highlight_aim_setup(St0),
        wings_view:command(Cmd,St0),
        #view{origin=OriginA} = wings_view:current(),
        O = e3d_vec:zero(),
        if OriginA =:= O, Cmd =:= aim ->
              wings_view:set_current(Before),
              zoom_step(Dir);
            OriginA =:= OriginB ->
              zoom_step(Dir);
            true ->
              Client = wings_wm:this(),
              {X0,Y0} = wings_wm:win_size(Client),
              {X,Y} = {X0 div 2, Y0 div 2},
              wings_io:warp(X,Y),
              zoom_step(Dir)
        end;
      false ->
        zoom_step(Dir)
    end.

rotate(Dx, Dy) ->
    Speed = wings_pref:get_value(cam_rotation_speed,25)/25,
    case allow_rotation() of
	false -> ok;
	true ->
	    View0= wings_view:current(),
	    #view{azimuth=Az0,elevation=El0} = View0,
	    Az = Az0 + Dx * Speed,
	    El = El0 + Dy * Speed,
	    View = View0#view{azimuth=Az,elevation=El,along_axis=none},
	    wings_view:set_current(View)
    end.

whrotate(Dx, Dy) ->
    case wings_pref:get_value(wheel_zooms, true) of
      false -> keep;
      true ->
        case wings_pref:get_value(wheel_adds, true) of
          false -> keep;
          true -> do_whrotate(Dx,Dy)
        end
    end.

do_whrotate(Dx,Dy) ->
    case allow_rotation() of
	false ->
            keep;
        true ->
            wings_wm:dirty(),
            View0= wings_view:current(),
            #view{azimuth=Az0,elevation=El0} = View0,
            S = 2 * wings_pref:get_value(wh_rot_spd),
            Az = Az0 + Dx*S,
            El = El0 + Dy*S,
            View = View0#view{azimuth=Az,elevation=El,along_axis=none},
            wings_view:set_current(View),
            keep
    end.

zoom_step_alt(Dir) ->
    case wings_pref:get_value(wheel_zooms, true) of
	false -> keep;
	true ->
	    ZoomFactor = wings_pref:get_value(wheel_zoom_factor_alt, ?ZOOM_FACTOR_ALT),
		wheel_zoom(ZoomFactor,Dir)
    end.

zoom_step(Dir) ->
    case wings_pref:get_value(wheel_zooms, true) of
	false -> keep;
	true ->
	    ZoomFactor = wings_pref:get_value(wheel_zoom_factor, ?ZOOM_FACTOR),
		wheel_zoom(ZoomFactor,Dir)
    end.

wheel_zoom(Factor, Dir) ->
    wings_wm:dirty(),
    #view{distance=Dist} = View = wings_view:current(),
    ZoomPercent = Factor/100,
    Delta = dist_factor(Dist)*Dir*ZoomPercent,
    wings_view:set_current(View#view{distance=Dist+Delta}),
    keep.

zoom(Delta0) ->
    #view{distance=Dist} = View = wings_view:current(),
    Delta = dist_factor(Dist)*Delta0/80,
    wings_view:set_current(View#view{distance=Dist+Delta}).

pan(Dx0, Dy0) ->
    #view{pan_x=PanX0,pan_y=PanY0,distance=D} = View = wings_view:current(),
    S = D*(1/20)/(101-wings_pref:get_value(pan_speed)),
    Dx = Dx0*S,
    Dy = Dy0*S,
    PanX = PanX0 + Dx,
    PanY = PanY0 - Dy,
    wings_view:set_current(View#view{pan_x=PanX,pan_y=PanY}).

arrow_key_pan(?SDLK_LEFT) -> arrow_key_pan(0.05, 0.0);
arrow_key_pan(?SDLK_RIGHT) -> arrow_key_pan(-0.05, 0.0);
arrow_key_pan(?SDLK_UP) -> arrow_key_pan(0.0, 0.05);
arrow_key_pan(?SDLK_DOWN) -> arrow_key_pan(0.0, -0.05);
arrow_key_pan(_) -> next.

arrow_key_pan(Dx, Dy) ->
    key_pan(Dx, Dy),
    wings_wm:dirty(),
    keep.

key_pan(Dx0, Dy0) ->
    #view{pan_x=PanX0,pan_y=PanY0,distance=D} = View = wings_view:current(),
    S = D * (wings_pref:get_value(pan_speed_arrow_keys)/100),
    Dx = Dx0*S,
    Dy = Dy0*S,
    PanX = PanX0 + Dx,
    PanY = PanY0 - Dy,
    wings_view:set_current(View#view{pan_x=PanX,pan_y=PanY}).

whpan(Dx0, Dy0) ->
    case wings_pref:get_value(wheel_zooms, true) of
  false -> keep;
  true ->
      case wings_pref:get_value(wheel_adds, true) of
    false -> keep;
    true ->
        wings_wm:dirty(),
        #view{pan_x=PanX0,pan_y=PanY0,distance=D} = View = wings_view:current(),
        S = D * (wings_pref:get_value(wh_pan_spd)/100),
        Dx = Dx0*S,
        Dy = Dy0*S,
        PanX = PanX0 + Dx,
        PanY = PanY0 - Dy,
        wings_view:set_current(View#view{pan_x=PanX,pan_y=PanY}),
        keep
       end
    end.
dist_factor(Dist) ->
    max(abs(Dist), 0.2).

start_camera(X,Y) ->
    {0,0,W,H} = wings_wm:viewport(),
    #camera{x=X,y=Y,ox=X,oy=Y,w=W,h=H}.

stop_camera(#camera{ox=Ox,oy=Oy}) ->
    wings_wm:release_focus(),
    case wings_io:ungrab(Ox, Oy) of
	still_grabbed ->
	    wings_wm:later(view_changed);
	no_grab ->
	    wings_wm:dirty()
    end,
    update_sel(fun show_sel_fun/2),
    pop.

camera_mouse_range(X1, Y1, #camera{x=OX,y=OY, xt=Xt0, yt=Yt0}=Camera) ->
    %% ?dbg("Camera ~w ~w~n", [X1,Y1]),
    XD0 = (X1 - OX),
    YD0 = (Y1 - OY),
    {XD,YD} = wings_pref:lowpass(XD0 + Xt0, YD0 + Yt0),
    if
	XD0 =:= 0, YD0 =:= 0 ->
            case wings_pref:get_value(no_warp, false) of
                false ->
                    {0.0,0.0,Camera#camera{xt=0,yt=0}};
                _ ->
                    #camera{w=W, h=H} = Camera,
                    Cx = W div 2, Cy = H div 2,
                    wings_io:warp(Cx, Cy),
                    {0.0,0.0,Camera#camera{xt=0,yt=0,x=Cx,y=Cy}}
            end;
	true ->
            case wings_pref:get_value(no_warp, false) of
                false ->
                    wings_io:warp(OX, OY),
                    {XD/?CAMDIV, YD/?CAMDIV, Camera#camera{xt=XD0, yt=YD0}};
                true ->
                    %% Warp as few times as possible
                    #camera{w=W, h=H} = Camera,
                    case X1 < 10 orelse Y1 < 10 orelse X1 > (W-10) orelse Y1 > (H-10) of
                        true ->
                            #camera{w=W, h=H} = Camera,
                            Cx = W div 2, Cy = H div 2,
                            wings_io:warp(Cx, Cy),
                            {XD/?CAMDIV, YD/?CAMDIV, Camera#camera{xt=XD0,yt=YD0,x=Cx,y=Cy}};
                        false ->
                            {XD/?CAMDIV, YD/?CAMDIV, Camera#camera{xt=XD0,yt=YD0,x=X1,y=Y1}}
                    end
            end
    end.

view_hotkey(Ev, Camera, #state{st=St}) ->
    case wings_hotkey:event(Ev,St) of
	next -> keep;
	{view,smooth_proxy} -> keep;
	{view,quick_preview} -> keep;
	{view,Cmd} -> 
	    wings_view:command(Cmd, St),
	    keep;	
	_Other -> %% Hotkey pressed, Quit camera mode
	    wings_wm:later(Ev),
	    stop_camera(Camera)
    end.

message(Message) ->
    wings_wm:message(Message),
    wings_wm:message_right([]).
    
grab() ->
    wings_io:grab(),
    wings_wm:grab_focus(),
    update_sel(fun hide_sel_fun/2).

hide_sel_fun(#dlo{sel=Sel}=D, _) ->
    D#dlo{sel={call,none,Sel}}.

show_sel_fun(#dlo{sel={call,none,Sel}}=D, _) ->
    D#dlo{sel=Sel};
show_sel_fun(D, _) -> D.

allow_rotation() ->
    wings_wm:get_prop(allow_rotation).

update_sel(Fun) ->
    case wings_pref:get_value(hide_sel_in_camera_moves) of
	false -> ok;
	true -> wings_dl:map(Fun, [])
    end.

format([{Mod,But,Msg}|T]) ->
    wings_msg:join(wings_msg:mod_format(Mod, But, Msg), format(T));
format([]) -> [].
