1%%
2%%  wings_file.erl --
3%%
4%%     This module contains the commands in the File menu.
5%%
6%%  Copyright (c) 2001-2011 Bjorn Gustavsson
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(wings_file).
15-export([init/0,init_autosave/0,menu/0,command/2]).
16-export([import_filename/2,export_filename/2,export_filename/3]).
17-export([unsaved_filename/0,del_unsaved_file/0,autosave_filename/1]).
18-export([file_filters/1]).
19
20-include("wings.hrl").
21-include_lib("e3d/e3d.hrl").
22-include_lib("e3d/e3d_image.hrl").
23-include_lib("kernel/include/file.hrl").
24
25-import(lists, [foldl/3,foreach/2,keymember/3,reverse/1]).
26-import(filename, [dirname/1]).
27
28-define(WINGS, ".wings").
29-define(UNSAVED_NAME, "unsaved" ++ ?WINGS).
30
31%% export_filename([Prop], St, Continuation).
32%%   The St will only be used to setup the default filename.
33%%   The Continuation fun will be called like this: Continuation(Filename).
34export_filename(Prop0, #st{file=File}, Cont) ->
35    Prop = case proplists:get_value(ext, Prop0) of
36	       undefined -> Prop0;
37	       Ext ->
38		   BaseName =
39		       case File of
40                           undefined -> "untitled";
41                           _ -> filename:basename(File)
42		       end,
43		   Def = filename:rootname(BaseName, ?WINGS) ++ Ext,
44		   [{default_filename,Def}|Prop0]
45	   end,
46    export_filename(Prop, Cont).
47
48%% import_filename([Prop], Continuation).
49%%   The Continuation fun will be called like this: Continuation(Filename).
50import_filename(Prop, Cont) ->
51    case get(wings_not_running) of
52	undefined ->
53	    import_filename_1(Prop, Cont);
54	{import, FileName} ->
55	    Cont(FileName)
56    end.
57import_filename_1(Ps0, Cont) ->
58    This = wings_wm:this(),
59    Dir = wings_pref:get_value(current_directory),
60    String = case os:type() of
61		 {win32,_} -> "Import";
62		 _Other    -> ?__(1,"Import")
63	     end,
64    Ps = Ps0 ++ [{title,String},{directory,Dir}],
65    Fun = fun(Name0) ->
66		  Name=test_unc_path(Name0),
67		  case catch Cont(Name) of
68		      {command_error,Error} ->
69			  wings_u:message(Error);
70		      #st{}=St ->
71			  set_cwd(dirname(Name)),
72			  wings_wm:send(This, {new_state,St});
73		      Tuple when is_tuple(Tuple) ->
74			  wings_wm:send(This, {action,Tuple});
75		      ignore -> keep;
76		      keep -> keep
77		  end
78	  end,
79    wings_plugin:call_ui({file,open_dialog,Ps,Fun}).
80
81%% export_filename([Prop], Continuation).
82%%   The Continuation fun will be called like this: Continuation(Filename).
83export_filename(Prop, Cont) ->
84    case get(wings_not_running) of
85	undefined ->
86	    export_filename_1(Prop, Cont);
87	{export, FileName} ->
88	    Cont(FileName)
89    end.
90
91export_filename_1(Prop0, Cont) ->
92    This = wings_wm:this(),
93    Dir = wings_pref:get_value(current_directory),
94    Prop = Prop0 ++ [{directory,Dir}],
95    Fun = fun(Name0) ->
96		  Name=test_unc_path(Name0),
97		  case catch Cont(Name) of
98		      {command_error,Error} ->
99			  wings_u:message(Error);
100		      #st{}=St ->
101			  set_cwd(dirname(Name)),
102			  wings_wm:send(This, {new_state,St});
103		      Tuple when is_tuple(Tuple) ->
104			  wings_wm:send(This, {action,Tuple});
105		      ignore -> keep;
106		      keep -> keep;
107		      ok -> keep
108		  end
109	  end,
110    String = case os:type() of
111		 {win32,_} -> "Export";
112		 _Other    -> ?__(1,"Export")
113	     end,
114    wings_plugin:call_ui({file,save_dialog,Prop++[{title,String}],Fun}).
115
116init() ->
117    wings_pref:set_default(save_unused_materials,false),
118    case wings_pref:get_value(current_directory) of
119	undefined ->
120	    case file:get_cwd() of
121		{ok,Cwd} -> wings_pref:set_value(current_directory, Cwd);
122		{error,_} -> wings_pref:set_value(current_directory, "/")
123	    end;
124	Cwd ->
125	    case filelib:is_dir(Cwd) of
126		false ->
127		    wings_pref:delete_value(current_directory),
128		    init();
129		true -> ok
130	    end
131    end.
132
133menu() ->
134    ImpFormats = [{"Nendo (.ndo)...",ndo}],
135    ExpFormats = [{"Nendo (.ndo)...",ndo}],
136    Tail = [{?__(25,"Exit"),quit,?__(28,"Exit Wings 3D")}],
137    [{?__(3,"New"),new,
138      ?__(4,"Create a new, empty scene")},
139     {?__(5,"Open..."),open,
140      ?__(6,"Open a previously saved scene")},
141     {?__(7,"Merge..."),merge,
142      ?__(8,"Merge a previously saved scene into the current scene")},
143      separator,
144     {?__(9,"Save"),save,
145      ?__(10,"Save the current scene")},
146     {?__(11,"Save As..."),save_as,
147      ?__(12,"Save the current scene under a new name")},
148     {?__(13,"Save Selected..."),save_selected,
149      ?__(14,"Save only the selected objects or faces")},
150     {?__(15,"Save Incrementally"),save_incr,
151      ?__(26,"Generate new filename and save")},
152      %% if there are more options we'll make a panel
153     {?__(29,"Save Unused Materials"),save_unused_materials,
154      ?__(30,"Include unused materials when saving a .wings file"),
155      save_unused_mats()},
156     separator,
157     {?__(16,"Revert"),revert,
158      ?__(17,"Revert current scene to the saved contents")},
159      separator,
160     {?__(18,"Import"),{import,ImpFormats}},
161     {?__(19,"Export"),{export,ExpFormats}},
162     {?__(20,"Export Selected"),{export_selected,ExpFormats}},
163     separator,
164     {?__(21,"Import Image..."),import_image,?__(22,"Import an image file")},
165     separator,
166     {?__(23,"Render"),{render,[]}},
167     separator,
168     {?__(24,"Install Plug-In or Patch"),install_plugin,
169      ?__(27,"Install a plug-in or a wings patch file")},
170     separator,
171     {?__(31,"Save Preference Subset..."),save_pref,
172      ?__(32,"Save a preference subset from your current settings")},
173     {?__(33,"Load Preference Subset"),
174       {load_pref,
175         [{?__(35,"Load..."),custom_theme,
176           ?__(36,"Load a previously saved preference subset")}]
177          ++wings_pref:recent_prefs()}},
178     separator|recent_files(Tail)].
179
180save_unused_mats() ->
181    wings_menu_util:crossmark(save_unused_materials).
182
183command(new, St) ->
184    new(St);
185command(confirmed_new, St) ->
186    del_unsaved_file(),
187    confirmed_new(St);
188command(open, St) ->
189    open(St);
190command(confirmed_open_dialog, _) ->
191    confirmed_open_dialog();
192command({confirmed_open,Filename}, St) ->
193    del_unsaved_file(),
194    confirmed_open(Filename, St);
195command({confirmed_open,Next,Filename}, _) ->
196    Next(Filename);
197command(merge, _) ->
198    merge();
199command({merge,Filename}, St) ->
200    merge(Filename, St);
201command(save, St) ->
202    save(ignore, St);
203command({save,Next}, St) ->
204    save(Next, St);
205command(save_as, St) ->
206    save_as(ignore, St);
207command({save_as,{Filename,Next}}, St) ->
208    save_now(Next, St#st{file=Filename});
209command(save_selected, St) ->
210    save_selected(St);
211command({save_selected,Filename}, St) ->
212    save_selected(Filename, St);
213command(save_incr, St) ->
214    save_incr(St);
215command(revert, St) ->
216    revert(St);
217command(confirmed_revert, St) ->
218    confirmed_revert(St);
219command(save_unused_materials, St) ->
220    Bool = wings_pref:get_value(save_unused_materials),
221    wings_pref:set_value(save_unused_materials, not Bool),
222	St;
223command({import,ndo}, _St) ->
224    import_ndo();
225command({import,{ndo,Filename}}, St) ->
226    import_ndo(Filename, St);
227command(import_image, _St) ->
228    import_image();
229command({import_image,Name}, _) ->
230    import_image(Name);
231command({import_files, Fs}, St) ->
232    {save_state, wpa:import(Fs, St)};
233command({export,ndo}, St) ->
234    String = case os:type() of
235        {win32,_} -> "Export";
236        _Other    -> ?__(2,"Export")
237    end,
238    export_ndo(export, String, St);
239command({export_selected,ndo}, St) ->
240    String = case os:type() of
241        {win32,_} -> "Export Selected";
242        _Other    -> ?__(3,"Export Selected")
243    end,
244    export_ndo(export_selected, String, St);
245command({export,{ndo,Filename}}, St) ->
246    do_export_ndo(Filename, St);
247command({export_selected,{ndo,Filename}}, St0) ->
248    St = delete_unselected(St0),
249    do_export_ndo(Filename, St);
250command(install_plugin, _St) ->
251    install_plugin();
252command({install_plugin,Filename}, _St) ->
253    wings_plugin:install(Filename);
254
255command(save_pref, _St) ->
256    wings_pref:pref(save);
257command({load_pref,Request}, St) ->
258    wings_pref:pref({load,Request,St});
259command({pref,Request}, St) ->
260    wings_pref:pref(Request,St),
261    keep;
262
263command(quit, #st{saved=true}) ->
264    quit;
265command(quit, _) ->
266    wings_u:yes_no_cancel(?__(4,"Do you want to save your changes before quitting?"),
267			  fun() -> {file,{save,{file,quit}}} end,
268			  fun() -> {file,confirmed_quit} end);
269command(confirmed_quit, _) ->
270    del_unsaved_file(),
271    quit;
272command({recent_file,Key}, St) when is_integer(Key), 1 =< Key ->
273    Recent0 = wings_pref:get_value(recent_files, []),
274    {_,File} = lists:nth(Key, Recent0),
275    case filelib:is_file(File) of
276	true ->
277	    named_open(File, St);
278	false ->
279	    Last = length(Recent0),
280	    Recent = delete_nth(Recent0, Key),
281	    wings_pref:set_value(recent_files, Recent),
282	    wings_menu:update_menu(file, {recent_file, Last}, delete, []),
283	    lists:foreach(fun({Str, RKey, Help}) ->
284				  wings_menu:update_menu(file, RKey, Str, Help);
285			     (separator) -> ok
286			  end, recent_files(Recent, [])),
287	    wings_u:error_msg(?__(5,"This file has been moved or deleted."))
288    end.
289
290delete_nth([_|T], 1) -> T;
291delete_nth([H|T], N) -> [H|delete_nth(T, N-1)];
292delete_nth([], _) -> [].
293
294confirmed_new(#st{file=File}=St) ->
295    %% Remove autosaved file; user has explicitly said so.
296    catch file:delete(autosave_filename(File)),
297    new(St#st{saved=true}).
298
299new(#st{saved=true}=St0) ->
300    St1 = clean_st(St0#st{file=undefined}),
301    %% clean_st/1 will remove all saved view, but will not reset the view. For a new project we should reset it.
302    wings_frame:reinit_layout(),
303    wings_view:reset(),
304    St2 = clean_images(wings_undo:init(St1)),
305    St = wings_obj:create_folder_system(St2),
306    wings_u:caption(St),
307    {new,St#st{saved=true}};
308new(#st{}=St0) ->		      %File is not saved or autosaved.
309    wings_u:caption(St0#st{saved=false}),
310    wings_u:yes_no_cancel(str_save_changes(),
311			  fun() -> {file,{save,{file,new}}} end,
312			  fun() -> {file,confirmed_new} end).
313
314open(#st{saved=Saved}=St) ->
315    case Saved =:= true orelse wings_obj:num_objects(St) =:= 0 of
316        true ->
317            confirmed_open_dialog();
318        false ->
319            %% Clear any autosave flag.
320            wings_u:caption(St#st{saved=false}),
321            Confirmed = {file,confirmed_open_dialog},
322            wings_u:yes_no_cancel(str_save_changes(),
323                                  fun() -> {file,{save,Confirmed}} end,
324                                  fun() -> Confirmed end)
325    end.
326
327confirmed_open_dialog() ->
328    %% All confirmation questions asked. The former contents has either
329    %% been saved, or the user has accepted that it will be discarded.
330    %% Go ahead and ask for the filename.
331
332    Cont = fun(Filename) -> {file,{confirmed_open,Filename}} end,
333    Dir = wings_pref:get_value(current_directory),
334    String = case os:type() of
335        {win32,_} -> "Open";
336        _Other    -> ?__(1,"Open")
337    end,
338    Ps = [{directory,Dir},
339	  {title,String}|wings_prop()],
340    import_filename(Ps, Cont).
341
342confirmed_open(Name, St0) ->
343    Fun = fun(File) ->
344		  %% We now have:
345		  %%   Name: Original name of file to be opened.
346		  %%   File: Either original file or the autosave file
347		  St1 = clean_st(St0#st{file=undefined}),
348		  wings_frame:reinit_layout(),
349		  St2 = wings_obj:create_folder_system(wings_undo:init(St1)),
350		  case ?SLOW(wings_ff_wings:import(File, St2)) of
351		      #st{}=St3 ->
352			  set_cwd(dirname(File)),
353			  St4 = clean_images(St3),
354			  St = wings_obj:recreate_folder_system(St4),
355			  add_recent(Name),
356			  wings_u:caption(St#st{saved=true,file=Name});
357		      {error,Reason} ->
358			  clean_new_images(St2),
359			  wings_u:error_msg(?__(1,"Read failed: ") ++ Reason)
360		  end
361	  end,
362    use_autosave(Name, Fun).
363
364named_open(Name, #st{saved=Saved}=St) ->
365    case Saved =:= true orelse wings_obj:num_objects(St) =:= 0 of
366        true ->
367            confirmed_open(Name, St);
368        false ->
369            %%Clear any autosave flag.
370            wings_u:caption(St#st{saved=false}),
371            Confirmed = {file,{confirmed_open,Name}},
372            wings_u:yes_no_cancel(str_save_changes(),
373                                  fun() -> {file,{save,Confirmed}} end,
374                                  fun() -> Confirmed end)
375    end.
376
377str_save_changes() ->
378    ?__(1,"Do you want to save your changes?").
379
380merge() ->
381    Cont = fun(Filename) -> {file,{merge,Filename}} end,
382    Dir = wings_pref:get_value(current_directory),
383    String = case os:type() of
384        {win32,_} -> "Merge";
385        _Other    -> ?__(1,"Merge")
386    end,
387    Ps = [{title,String},{directory,Dir}|wings_prop()],
388    import_filename(Ps, Cont).
389
390merge(Name, St0) ->
391    Fun = fun(File) ->
392		  %% We now have:
393		  %%   Name: Original name of file to be opened.
394		  %%   File: Either original file or the autosave file
395		  St1 = St0#st{saved=wings_image:next_id()},
396		  case wings_ff_wings:merge(File, St0) of
397		      {error,Reason} ->
398			  clean_new_images(St1),
399			  wings_u:error_msg(?__(2,"Read failed: ") ++ Reason);
400		      #st{}=St ->
401			  set_cwd(dirname(Name)),
402			  wings_u:caption(St#st{saved=false}),
403			  wings_obj:recreate_folder_system(St#st{saved=false})
404		  end
405	  end,
406    use_autosave(Name, Fun).
407
408save(Next, #st{saved=true}) ->
409    maybe_send_action(Next);
410save(Next, #st{file=undefined}=St) ->
411    save_as(Next, St);
412save(Next, St) ->
413    save_now(Next, St).
414
415save_as(Next, St) ->
416    Cont = fun(Name) ->
417       set_cwd(dirname(Name)),
418       {file,{save_as,{Name,Next}}}
419    end,
420    Title = case os:type() of
421        {win32,_} -> "Save";
422        _Other    -> ?__(1,"Save")
423    end,
424    Ps = [{title,Title}|wings_prop()],
425    export_filename(Ps, St, Cont).
426
427save_now(Next, #st{file=Name0}=St) ->
428    Name=test_unc_path(Name0),
429    Backup = backup_filename(Name),
430    case wings_pref:get_value(file_recovered, false) of
431        true -> del_unsaved_file();
432        _ -> ok
433    end,
434    file:rename(Name, Backup),
435    file:delete(autosave_filename(Name)),
436    case ?SLOW(wings_ff_wings:export(Name, false, St)) of
437	ok ->
438	    set_cwd(dirname(Name)),
439	    add_recent(Name),
440	    maybe_send_action(Next),
441	    {saved,wings_u:caption(St#st{saved=true})};
442	{error,Reason} ->
443	    wings_u:error_msg(?__(1,"Save failed: ") ++ Reason)
444    end.
445
446del_unsaved_file() ->
447    File = autosave_filename(unsaved_filename()),
448    catch file:delete(File),
449    wings_pref:set_value(file_recovered, false).
450
451test_unc_path([H|_]=FileName) when H=:=47 ->
452	case string:str(FileName, "//") of
453		1 -> FileName;
454		_ -> "/"++FileName  % 47 is ascii code for "/"
455	end;
456test_unc_path(FileName) -> FileName.
457
458maybe_send_action(ignore) -> keep;
459maybe_send_action(Action) -> wings_wm:later({action,Action}).
460
461save_selected(#st{sel=[]}) ->
462    wings_u:error_msg(?__(1,"This command requires a selection."));
463save_selected(St) ->
464    String = case os:type() of
465        {win32,_} -> "Save Selected";
466        _Other    -> ?__(2,"Save Selected")
467    end,
468    Ps = [{title,String}|wings_prop()],
469    Cont = fun(Name) -> {file,{save_selected,Name}} end,
470    export_filename(Ps, St, Cont).
471
472save_selected(Name, St0) ->
473    St = delete_unselected(St0),
474    case ?SLOW(wings_ff_wings:export(Name, true, St)) of
475	ok -> keep;
476	{error,Reason} -> wings_u:error_msg(Reason)
477    end.
478
479%%%
480%%% Save incrementally. Original code submitted by Clacos.
481%%%
482
483save_incr(#st{saved=true}=St) -> St;
484save_incr(#st{file=undefined}=St0) ->
485    save_as(ignore, St0);
486save_incr(#st{file=Name0}=St) ->
487    Name = increment_name(Name0),
488    save_now(ignore, St#st{file=Name}).
489
490increment_name(Name0) ->
491    Name1 = reverse(filename:rootname(Name0)),
492    Name = case find_digits(Name1)  of
493	       {[],Base} ->
494		   Base ++ "_01" ++ ?WINGS;
495	       {Digits0,Base} ->
496		   Number = list_to_integer(Digits0),
497		   Digits = integer_to_list(Number+1),
498		   Zs = case length(Digits0)-length(Digits) of
499			    Neg when Neg =< 0 -> [];
500			    Nzs -> lists:duplicate(Nzs, $0)
501			end,
502		   Base ++ Zs ++ Digits ++ ?WINGS
503	   end,
504    update_recent(Name0, Name),
505    Name.
506
507find_digits(List) ->
508    find_digits1(List, []).
509
510find_digits1([H|T], Digits) when $0 =< H, H =< $9 ->
511    find_digits1(T, [H|Digits]);
512find_digits1([_|_]=Rest, Digits) ->
513    {Digits,reverse(Rest)};
514find_digits1([], Digits) ->
515    {Digits,[]}.
516
517wings_prop() ->
518    %% Should we add autosaved wings files ??
519    %% It's pretty nice to NOT see them in the file chooser /Dan
520
521    %% Disabled translations for win32
522    String = case os:type() of
523        {win32,_} -> "Wings File";
524        _Other    -> ?__(1,"Wings File")
525    end,
526    [{ext,?WINGS},{ext_desc, String}].
527
528use_autosave(File, Body) ->
529    case file:read_file_info(File) of
530	{ok,SaveInfo} ->
531	    use_autosave_1(SaveInfo, File, Body);
532	{error, _} ->			     % use autosaved file if it exists
533	    Auto = autosave_filename(File),
534	    Body(case filelib:is_file(Auto) of
535		     true -> Auto;
536		     false -> File			%Let reader handle error.
537		 end)
538    end.
539
540use_autosave_1(#file_info{mtime=SaveTime0}, File, Body) ->
541    Auto = autosave_filename(File),
542    case file:read_file_info(Auto) of
543	{ok,#file_info{mtime=AutoInfo0}} ->
544	    SaveTime = calendar:datetime_to_gregorian_seconds(SaveTime0),
545	    AutoTime = calendar:datetime_to_gregorian_seconds(AutoInfo0),
546            FastStart = wings:is_fast_start(),
547	    if
548                FastStart -> %% Ignore autosave
549                    Body(File);
550		AutoTime > SaveTime ->
551		    Msg = ?__(1,"An autosaved file with a later time stamp exists;"
552                              " do you want to load the autosaved file instead?"),
553		    wings_u:yes_no(Msg, autosave_fun(Body, Auto),
554				   autosave_fun(Body, File));
555		true ->
556		    Body(File)
557	    end;
558	{error, _} ->				% No autosave file
559	    Body(File)
560    end.
561
562autosave_fun(Next, Filename) ->
563    fun() -> {file,{confirmed_open,Next,Filename}} end.
564
565set_cwd(Cwd) ->
566    wings_pref:set_value(current_directory, Cwd).
567
568init_autosave() ->
569    Name = autosaver,
570    case wings_wm:is_window(Name) of
571	true -> ok;
572	false ->
573	    Op = {seq,push,get_autosave_event(make_ref(), #st{saved=auto})},
574	    wings_wm:new(Name, {0,0,1}, {0,0}, Op),
575	    wings_wm:hide(Name),
576	    wings_wm:set_dd(Name, geom_display_lists)
577    end,
578    wings_wm:send(Name, start_timer).
579
580get_autosave_event(Ref, St) ->
581    {replace,fun(Ev) -> autosave_event(Ev, Ref, St) end}.
582
583autosave_event(start_timer, OldTimer, St) ->
584    wings_wm:cancel_timer(OldTimer),
585    case {wings_pref:get_value(autosave),wings_pref:get_value(autosave_time)} of
586	{false,_} -> delete;
587	{true,0} ->
588	    N = 24*60,
589	    wings_pref:set_value(autosave_time, N),
590	    Timer = wings_wm:set_timer(N*60000, autosave),
591	    get_autosave_event(Timer, St);
592	{true,N} ->
593	    Timer = wings_wm:set_timer(N*60000, autosave),
594	    get_autosave_event(Timer, St)
595    end;
596autosave_event(autosave, _, St) ->
597    autosave(St),
598    wings_wm:later(start_timer);
599autosave_event({current_state,St}, Timer, _) ->
600    get_autosave_event(Timer, St);
601autosave_event(_, _, _) -> keep.
602
603autosave(#st{file=undefined} = St) ->
604	autosave(St#st{file=unsaved_filename()});
605autosave(#st{saved=true} = St) -> St;
606autosave(#st{saved=auto} = St) -> St;
607autosave(#st{file=Name}=St) ->
608    Auto = autosave_filename(Name),
609    %% Maybe this should be spawned to another process
610    %% to let the autosaving be done in the background.
611    %% But I don't want to copy a really big model either.
612
613    %% Set the current view export views read it..
614    %% Fix this later
615    View = wings_wm:get_prop(geom, current_view),
616    wings_view:set_current(View),
617    filelib:ensure_dir(Auto),
618    case ?SLOW(wings_ff_wings:export(Auto, false, St)) of
619	ok ->
620	    wings_u:caption(St#st{saved=auto});
621	{error,Reason} ->
622	    F = ?__(1,"Autosaving \"~s\" failed: ~s"),
623	    Msg = lists:flatten(wings_util:format(F, [Auto,Reason])),
624	    wings_u:message(Msg)
625    end.
626
627autosave_filename(File) ->
628    Base = filename:basename(File),
629    Dir = filename:dirname(File),
630    filename:join(Dir, "#" ++ Base ++ "#").
631
632unsaved_filename() ->
633    Dir = wings_pref:get_dir(),
634    filename:join(Dir, ?UNSAVED_NAME).
635
636backup_filename(File) ->
637    File ++ "~".
638
639add_recent(Name) ->
640    Base = filename:basename(Name),
641    case filename:extension(Base) of
642	?WINGS ->
643	    File = {Base,Name},
644	    Recent0 = wings_pref:get_value(recent_files, []),
645	    Recent1 = Recent0 -- [File],
646	    Recent = add_recent(File, Recent1),
647	    wings_pref:set_value(recent_files, Recent);
648	_Other -> ok
649    end.
650
651update_recent(Old, New) ->
652    OldFile = {filename:basename(Old),Old},
653    NewFile = {filename:basename(New),New},
654    Recent0 = wings_pref:get_value(recent_files, []),
655    Recent1 = Recent0 -- [OldFile,NewFile],
656    Recent = add_recent(NewFile, Recent1),
657    wings_pref:set_value(recent_files, Recent).
658
659add_recent(NewFile, Present) ->
660    Recent = add_recent_1(NewFile, Present),
661    lists:foreach(fun({Str, Key, Help}) ->
662			  wings_menu:update_menu(file, Key, Str, Help);
663		     (separator) -> ok
664		  end, recent_files(Recent, [])),
665    Recent.
666
667add_recent_1(File, [A,B,C,D,E|_]) -> [File,A,B,C,D,E];
668add_recent_1(File, Recent) -> [File|Recent].
669
670recent_files(Tail) ->
671    recent_files(wings_pref:get_value(recent_files, []), Tail).
672
673recent_files([], Tail) -> Tail;
674recent_files(Files, Tail) ->
675    Help = ?__(1,"Open this recently used file"),
676    recent_files_1(Files, 1, Help, [separator|Tail]).
677
678recent_files_1([{Base0,Base1}|T], I, Help0, Tail) ->
679    Base = wings_u:pretty_filename(Base0),
680    Help = lists:flatten([Help0," -- "|wings_u:pretty_filename(Base1)]),
681    [{Base,{recent_file,I},Help}|recent_files_1(T, I+1, Help0, Tail)];
682recent_files_1([], _, _, Tail) -> Tail.
683
684%%
685%% The Revert command.
686%%
687revert(#st{file=undefined}=St) -> St;
688revert(St0) ->
689    wings_u:yes_no(?__(2,"All changes made since the last save will be lost.\n" ++
690                         "Do you want to continue?"),
691                          fun() -> {file,confirmed_revert} end,
692                          fun() -> St0 end).
693
694confirmed_revert(St0) ->
695    case confirmed_revert_1(St0) of
696        {error,Reason} ->
697            wings_u:error_msg(?__(1,"Revert failed: ") ++ Reason),
698            St0;
699        #st{}=St -> {save_state,St}
700    end.
701
702confirmed_revert_1(#st{file=File}=St0) ->
703    St1 = wings_obj:create_folder_system(clean_st(St0)),
704    case ?SLOW(wings_ff_wings:import(File, St1)) of
705	#st{}=St2 ->
706	    St = wings_obj:recreate_folder_system(St2),
707	    clean_images(St);
708	{error,_}=Error ->
709	    Error
710    end.
711
712%%
713%% Import.
714%%
715
716import_ndo() ->
717    Ps = [{ext,".ndo"},{ext_desc,"Nendo File"}],
718    Cont = fun(Name) -> {file,{import,{ndo,Name}}} end,
719    import_filename(Ps, Cont).
720
721import_ndo(Name, St0) ->
722    case ?SLOW(wings_ff_ndo:import(Name, St0)) of
723	#st{}=St ->
724	    {save_state,St};
725	{error,Reason} ->
726	    wings_u:error_msg(?__(1,"Import failed: ") ++ Reason),
727	    St0
728    end.
729
730import_image() ->
731    Ps = [{extensions,wings_image:image_formats()},{multiple,true}],
732    Cont = fun(Name) -> {file,{import_image,Name}} end,
733    import_filename(Ps, Cont).
734
735import_image(Name) ->
736    case wings_image:from_file(Name) of
737	Im when is_integer(Im) ->
738	    keep;
739	{error,100902=Error} ->  % GLU_OUT_OF_MEMORY
740            wings_u:error_msg(?__(2,"The image cannot be loaded.~nFile: "
741                                  "\"~ts\"~n GLU Error: ~p - ~s~n"),
742                              [Name, Error, wings_gl:error_string(Error)]);
743        {error,Error} ->
744            case file:format_error(Error) of
745                "unknown" ++ _ ->
746                    wings_u:error_msg(?__(1,"Failed to load") ++ " \"~ts\": ~p\n",
747                                      [Name,Error]);
748                ErrStr ->
749                    wings_u:error_msg(?__(1,"Failed to load") ++ " \"~ts\": ~s\n",
750                                      [Name,ErrStr])
751            end
752    end.
753
754%%
755%% Export.
756%%
757
758export_ndo(Cmd, Title, St) ->
759    Ps = [{title,Title},{ext,".ndo"},{ext_desc,"Nendo File"}],
760    Cont = fun(Name) -> {file,{Cmd,{ndo,Name}}} end,
761    export_filename(Ps, St, Cont).
762
763do_export_ndo(Name, St) ->
764    case wings_ff_ndo:export(Name, St) of
765	ok -> keep;
766	{error,Reason} -> wings_u:error_msg(Reason)
767    end.
768
769%%%
770%%% Install a plug-in.
771%%%
772
773install_plugin() ->
774    Props = case os:type() of
775		{win32,_} ->
776		    [{title,"Install Plug-In"},
777		     {extensions,
778		      [{".gz", "GZip Compressed File"},
779		       {".tar", "Tar File"},
780		       {".tgz", "Compressed Tar File"},
781		       {".beam", "Beam File"}]}];
782		_Other    ->
783		    [{title,?__(1,"Install Plug-In")},
784		     {extensions,
785		      [{".gz",?__(2,"GZip Compressed File")},
786		       {".tar",?__(3,"Tar File")},
787		       {".tgz",?__(4,"Compressed Tar File")},
788		       {".beam",?__(5,"Beam File")}]}]
789	    end,
790    Cont = fun(Name) -> {file,{install_plugin,Name}} end,
791    import_filename(Props, Cont).
792
793file_filters(Prop) ->
794    Exts = case proplists:get_value(extensions, Prop, none) of
795	       none ->
796		   Ext = proplists:get_value(ext, Prop, ".wings"),
797		   ExtDesc = proplists:get_value(ext_desc, Prop,
798						 ?__(1,"Wings File")),
799		   [{Ext,ExtDesc}];
800	       Other -> Other
801	   end,
802    lists:flatten([file_add_all(Exts),
803		   file_filters_0(Exts++[{".*", ?__(2,"All Files")}])]).
804
805file_filters_0(Exts) ->
806    file_filters_1(lists:reverse(Exts),[]).
807
808file_filters_1([{Ext,Desc}|T], Acc) ->
809    Wildcard = "*" ++ Ext,
810    ExtString = [Desc," (",Wildcard,")","|",Wildcard|Acc],
811    case T of
812	[] -> ExtString;
813	_  ->
814	    file_filters_1(T, ["|"|ExtString])
815    end.
816
817file_add_all([_]) -> [];
818file_add_all(Exts) ->
819    All0 = ["*"++E || {E,_} <- Exts],
820    All = file_add_semicolons(All0),
821    [?__(1,"All Formats")++" (",All,")", "|", All, "|"].
822
823file_add_semicolons([E1|[_|_]=T]) ->
824    [E1,";"|file_add_semicolons(T)];
825file_add_semicolons(Other) -> Other.
826
827
828%%% Utilities.
829%%%
830
831clean_st(St) ->
832    foreach(fun(Win) ->
833		    wings_wm:set_prop(Win, wireframed_objects, gb_sets:empty())
834	    end, wings_u:geom_windows()),
835    DefMat = wings_material:default(),
836    Empty = gb_trees:empty(),
837    Limit = wings_image:next_id(),
838    wings_pref:delete_scene_value(),
839    wings_view:delete_all(St#st{onext=1,shapes=Empty,mat=DefMat,pst=Empty,
840				sel=[],ssels=Empty,saved=Limit}).
841
842clean_images(#st{saved=Limit}=St) when is_integer(Limit) ->
843    wings_dl:map(fun(D, _) -> D#dlo{proxy_data=none, proxy=false} end, []),
844    wings_image:delete_older(Limit),
845    St#st{saved=false}.
846
847clean_new_images(#st{saved=Limit}) when is_integer(Limit) ->
848    wings_image:delete_from(Limit).
849
850delete_unselected(St) ->
851    Unselected = wings_sel:unselected_ids(St),
852    foldl(fun wings_obj:delete/2, St, Unselected).
853