2424package processing .app ;
2525
2626import processing .app .ui .Editor ;
27- import processing .app .ui .ProgressFrame ;
2827import processing .app .ui .Recent ;
2928import processing .app .ui .Toolkit ;
3029import processing .core .*;
3130
31+ import java .awt .Color ;
3232import java .awt .Component ;
3333import java .awt .Container ;
34+ import java .awt .EventQueue ;
3435import java .awt .FileDialog ;
3536import java .awt .event .ActionListener ;
3637import java .awt .event .KeyAdapter ;
3738import java .awt .event .KeyEvent ;
39+ import java .beans .PropertyChangeEvent ;
40+ import java .beans .PropertyChangeListener ;
3841import java .io .*;
3942import java .util .List ;
4043
4144import javax .swing .*;
45+ import javax .swing .border .EmptyBorder ;
4246
4347
4448/**
@@ -792,7 +796,7 @@ public boolean saveAs() throws IOException {
792796 String newParentDir = null ;
793797 String newName = null ;
794798
795- final String oldName2 = folder .getName ();
799+ String oldName = folder .getName ();
796800 // TODO rewrite this to use shared version from PApplet
797801 final String PROMPT = Language .text ("save" );
798802 if (Preferences .getBoolean ("chooser.files.native" )) {
@@ -805,8 +809,8 @@ public boolean saveAs() throws IOException {
805809 // default to the parent folder of where this was
806810 fd .setDirectory (folder .getParent ());
807811 }
808- String oldName = folder .getName ();
809- fd .setFile (oldName );
812+ String oldFolderName = folder .getName ();
813+ fd .setFile (oldFolderName );
810814 fd .setVisible (true );
811815 newParentDir = fd .getDirectory ();
812816 newName = fd .getFile ();
@@ -868,7 +872,7 @@ public boolean saveAs() throws IOException {
868872 // just use "save" here instead, because the user will have received a
869873 // message (from the operating system) about "do you want to replace?"
870874 return save ();
871- }
875+ }
872876
873877 // check to see if the user is trying to save this sketch inside itself
874878 try {
@@ -927,21 +931,21 @@ public boolean accept(File file) {
927931 }
928932 });
929933
930-
931- final File newFolder2 = newFolder ;
932- final File [] copyItems2 = copyItems ;
933- final String newName2 = newName ;
934-
935- // Create a new event dispatch thread- to display ProgressBar
936- // while Saving As
937- javax . swing . SwingUtilities . invokeLater ( new Runnable () {
938- public void run () {
939- new ProgressFrame ( copyItems2 , newFolder2 , oldName2 , newName2 , editor );
940- }
941- });
942-
943-
944- // save the other tabs to their new location
934+ // Kick off a background thread to copy everything *but* the .pde files.
935+ // Due to the poor way (dating back to the late 90s with DBN) that our
936+ // save() and saveAs() methods have been implemented to return booleans,
937+ // there isn't a good way to return a value to the calling thread without
938+ // a good bit of refactoring (that should be done at some point).
939+ // As a result, this method will return 'true' before the full "Save As"
940+ // has completed, which will cause problems in weird cases.
941+ // For instance, saving an untitled sketch that has an enormous data
942+ // folder while quitting. The save thread to move those data folder files
943+ // won't have finished before this returns true, and the PDE may quit
944+ // before the SwingWorker completes its job.
945+ // https://github.com/processing/processing/issues/3843
946+ startSaveAsThread ( oldName , newName , newFolder , copyItems );
947+
948+ // save the other tabs to their new location (main tab saved below)
945949 for (int i = 1 ; i < codeCount ; i ++) {
946950 File newFile = new File (newFolder , code [i ].getFileName ());
947951 code [i ].saveAs (newFile );
@@ -971,6 +975,171 @@ public void run() {
971975 }
972976
973977
978+ void startSaveAsThread (final String oldName , final String newName ,
979+ final File newFolder , final File [] copyItems ) {
980+ EventQueue .invokeLater (new Runnable () {
981+ public void run () {
982+ final JFrame frame = new JFrame ("Saving \u201C " + newName + "\u201C ..." );
983+ // the UI of the progress bar follows
984+ frame .setDefaultCloseOperation (WindowConstants .HIDE_ON_CLOSE );
985+ // frame.setBounds(200, 200, 400, 140);
986+ // frame.setResizable(false);
987+
988+ Box box = Box .createVerticalBox ();
989+ box .setBorder (new EmptyBorder (16 , 16 , 16 , 16 ));
990+ // JPanel panel = new JPanel(null);
991+ // frame.add(panel);
992+ // frame.setContentPane(panel); // uh, both?
993+
994+ if (Platform .isMacOS ()) {
995+ frame .setBackground (Color .WHITE );
996+ }
997+
998+ JLabel label = new JLabel ("Saving additional files from the sketch folder..." );
999+ // JLabel label = new JLabel("Saving " + oldName + " as " + newName + "...");
1000+ // label.setBounds(40, 20, 300, 20);
1001+ box .add (label );
1002+
1003+ final JProgressBar progressBar = new JProgressBar (0 , 100 );
1004+ // no luck, stuck with ugly on OS X
1005+ //progressBar.putClientProperty("JComponent.sizeVariant", "regular");
1006+ progressBar .setValue (0 );
1007+ // progressBar.setBounds(40, 50, 300, 30);
1008+ progressBar .setStringPainted (true );
1009+ box .add (progressBar );
1010+
1011+ // panel.add(progressBar);
1012+ // panel.add(label);
1013+
1014+ frame .getContentPane ().add (box );
1015+ frame .pack ();
1016+ frame .setLocationRelativeTo (editor );
1017+ Toolkit .setIcon (frame );
1018+ frame .setVisible (true );
1019+
1020+ new SwingWorker <Void , Void >() {
1021+
1022+ @ Override
1023+ protected Void doInBackground () throws Exception {
1024+ addPropertyChangeListener (new PropertyChangeListener () {
1025+ public void propertyChange (PropertyChangeEvent evt ) {
1026+ if ("progress" .equals (evt .getPropertyName ())) {
1027+ System .out .println (evt .getNewValue ());
1028+ progressBar .setValue ((Integer ) evt .getNewValue ());
1029+ }
1030+ }
1031+ });
1032+
1033+ long totalSize = 0 ;
1034+ for (File copyable : copyItems ) {
1035+ totalSize += Util .calcSize (copyable );
1036+ }
1037+
1038+ long progress = 0 ;
1039+ setProgress (0 );
1040+ for (File copyable : copyItems ) {
1041+ if (copyable .isDirectory ()) {
1042+ copyDir (copyable ,
1043+ new File (newFolder , copyable .getName ()),
1044+ progress , totalSize );
1045+ progress += Util .calcSize (copyable );
1046+ } else {
1047+ copyFile (copyable ,
1048+ new File (newFolder , copyable .getName ()),
1049+ progress , totalSize );
1050+ if (Util .calcSize (copyable ) < 512 * 1024 ) {
1051+ // If the file length > 0.5MB, the copyFile() function has
1052+ // been redesigned to change progress every 0.5MB so that
1053+ // the progress bar doesn't stagnate during that time
1054+ progress += Util .calcSize (copyable );
1055+ setProgress ((int ) (progress * 100L / totalSize ));
1056+ }
1057+ }
1058+ }
1059+ return null ;
1060+ }
1061+
1062+
1063+ /**
1064+ * Overloaded copyFile that is called whenever a Save As is being done,
1065+ * so that the ProgressBar is updated for very large files as well.
1066+ */
1067+ void copyFile (File sourceFile , File targetFile ,
1068+ long progress , long totalSize ) throws IOException {
1069+ BufferedInputStream from =
1070+ new BufferedInputStream (new FileInputStream (sourceFile ));
1071+ BufferedOutputStream to =
1072+ new BufferedOutputStream (new FileOutputStream (targetFile ));
1073+ byte [] buffer = new byte [16 * 1024 ];
1074+ int bytesRead ;
1075+ int progRead = 0 ;
1076+ while ((bytesRead = from .read (buffer )) != -1 ) {
1077+ to .write (buffer , 0 , bytesRead );
1078+ progRead += bytesRead ;
1079+ if (progRead >= 512 * 1024 ) { // to update progress bar every 0.5MB
1080+ progress += progRead ;
1081+ //progressBar.setValue((int) Math.min(Math.ceil(progress * 100.0 / totalSize), 100));
1082+ setProgress ((int ) (100L * progress / totalSize ));
1083+ progRead = 0 ;
1084+ }
1085+ }
1086+ // Final update to progress bar
1087+ setProgress ((int ) (100L * progress / totalSize ));
1088+
1089+ from .close ();
1090+ from = null ;
1091+ to .flush ();
1092+ to .close ();
1093+ to = null ;
1094+
1095+ targetFile .setLastModified (sourceFile .lastModified ());
1096+ targetFile .setExecutable (sourceFile .canExecute ());
1097+ }
1098+
1099+
1100+ long copyDir (File sourceDir , File targetDir ,
1101+ long progress , long totalSize ) throws IOException {
1102+ // Overloaded copyDir so that the Save As progress bar gets updated when the
1103+ // files are in folders as well (like in the data folder)
1104+ if (sourceDir .equals (targetDir )) {
1105+ final String urDum = "source and target directories are identical" ;
1106+ throw new IllegalArgumentException (urDum );
1107+ }
1108+ targetDir .mkdirs ();
1109+ String files [] = sourceDir .list ();
1110+ for (String filename : files ) {
1111+ // Ignore dot files (.DS_Store), dot folders (.svn) while copying
1112+ if (filename .charAt (0 ) == '.' ) {
1113+ continue ;
1114+ }
1115+
1116+ File source = new File (sourceDir , filename );
1117+ File target = new File (targetDir , filename );
1118+ if (source .isDirectory ()) {
1119+ progress = copyDir (source , target , progress , totalSize );
1120+ //progressBar.setValue((int) Math.min(Math.ceil(progress * 100.0 / totalSize), 100));
1121+ setProgress ((int ) (100L * progress / totalSize ));
1122+ target .setLastModified (source .lastModified ());
1123+ } else {
1124+ copyFile (source , target , progress , totalSize );
1125+ progress += source .length ();
1126+ //progressBar.setValue((int) Math.min(Math.ceil(progress * 100.0 / totalSize), 100));
1127+ setProgress ((int ) (100L * progress / totalSize ));
1128+ }
1129+ }
1130+ return progress ;
1131+ }
1132+
1133+
1134+ @ Override
1135+ public void done () {
1136+ frame .dispose ();
1137+ }
1138+ }.execute ();
1139+ }
1140+ });
1141+ }
1142+
9741143
9751144 /**
9761145 * Update internal state for new sketch name or folder location.
0 commit comments