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