Exporter.vala 12.4 KB
Newer Older
1
/* Copyright 2016 Software Freedom Conservancy Inc.
2 3
 *
 * This software is licensed under the GNU Lesser General Public License
4
 * (version 2.1 or later).  See the COPYING file in this distribution.
5 6
 */

7 8 9 10 11 12 13 14 15 16 17
public enum ExportFormatMode {
    UNMODIFIED,
    CURRENT,
    SPECIFIED, /* use an explicitly specified format like PNG or JPEG */
    LAST       /* use whatever format was used in the previous export operation */
}

public struct ExportFormatParameters {
    public ExportFormatMode mode;
    public PhotoFileFormat specified_format;
    public Jpeg.Quality quality;
Jonas Bushart's avatar
Jonas Bushart committed
18
    public bool export_metadata;
19 20 21 22 23 24
    
    private ExportFormatParameters(ExportFormatMode mode, PhotoFileFormat specified_format,
        Jpeg.Quality quality) {
        this.mode = mode;
        this.specified_format = specified_format;
        this.quality = quality;
Jonas Bushart's avatar
Jonas Bushart committed
25
        this.export_metadata = true;
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
    }
    
    public static ExportFormatParameters current() {
        return ExportFormatParameters(ExportFormatMode.CURRENT,
            PhotoFileFormat.get_system_default_format(), Jpeg.Quality.HIGH);
    }
       
    public static ExportFormatParameters unmodified() {
        return ExportFormatParameters(ExportFormatMode.UNMODIFIED,
            PhotoFileFormat.get_system_default_format(), Jpeg.Quality.HIGH);
    }
    
    public static ExportFormatParameters for_format(PhotoFileFormat format) {
        return ExportFormatParameters(ExportFormatMode.SPECIFIED, format, Jpeg.Quality.HIGH);
    }
    
    public static ExportFormatParameters last() {
        return ExportFormatParameters(ExportFormatMode.LAST,
            PhotoFileFormat.get_system_default_format(), Jpeg.Quality.HIGH);
    }
    
    public static ExportFormatParameters for_JPEG(Jpeg.Quality quality) {
        return ExportFormatParameters(ExportFormatMode.SPECIFIED, PhotoFileFormat.JFIF,
            quality);
    }
}

53
public class Exporter : Object {
54 55 56 57 58 59 60
    public enum Overwrite {
        YES,
        NO,
        CANCEL,
        REPLACE_ALL
    }
    
61
    public delegate void CompletionCallback(Exporter exporter, bool is_cancelled);
62
    
63
    public delegate Overwrite OverwriteCallback(Exporter exporter, File file);
64
    
65
    public delegate bool ExportFailedCallback(Exporter exporter, File file, int remaining, 
66 67 68
        Error err);
    
    private class ExportJob : BackgroundJob {
69
        public MediaSource media;
70
        public File dest;
71 72 73
        public Scaling? scaling;
        public Jpeg.Quality? quality;
        public PhotoFileFormat? format;
74
        public Error? err = null;
75
        public bool direct_copy_unmodified = false;
Jonas Bushart's avatar
Jonas Bushart committed
76
        public bool export_metadata = true;
77
        
78
        public ExportJob(Exporter owner, MediaSource media, File dest, Scaling? scaling, 
79
            Jpeg.Quality? quality, PhotoFileFormat? format, Cancellable cancellable,
Jonas Bushart's avatar
Jonas Bushart committed
80
            bool direct_copy_unmodified = false, bool export_metadata = true) {
81
            base (owner, owner.on_exported, cancellable, owner.on_export_cancelled);
82
            
83 84 85
            assert(media is Photo || media is Video);
            
            this.media = media;
86 87 88 89
            this.dest = dest;
            this.scaling = scaling;
            this.quality = quality;
            this.format = format;
90
            this.direct_copy_unmodified = direct_copy_unmodified;
Jonas Bushart's avatar
Jonas Bushart committed
91
            this.export_metadata = export_metadata;
92
        }
93

94 95
        public override void execute() {
            try {
96
                if (media is Photo) {
Jonas Bushart's avatar
Jonas Bushart committed
97
                    ((Photo) media).export(dest, scaling, quality, format, direct_copy_unmodified, export_metadata);
98
                } else if (media is Video) {
99
                    ((Video) media).export(dest);
100
                }
101 102 103 104 105 106
            } catch (Error err) {
                this.err = err;
            }
        }
    }
    
107 108 109
    private Gee.Collection<MediaSource> to_export = new Gee.ArrayList<MediaSource>();
    private File[] exported_files;
    private File? dir;
110 111
    private Scaling scaling;
    private int completed_count = 0;
112
    private Workers workers = new Workers(Workers.threads_per_cpu(1, 4), false);
113 114 115 116
    private unowned CompletionCallback? completion_callback = null;
    private unowned ExportFailedCallback? error_callback = null;
    private unowned OverwriteCallback? overwrite_callback = null;
    private unowned ProgressMonitor? monitor = null;
117
    private Cancellable cancellable;
118
    private bool replace_all = false;
119
    private bool aborted = false;
120 121
    private ExportFormatParameters export_params;

122
    public Exporter(Gee.Collection<MediaSource> to_export, File? dir, Scaling scaling,
123
        ExportFormatParameters export_params, bool auto_replace_all = false) {
124
        this.to_export.add_all(to_export);
125 126
        this.dir = dir;
        this.scaling = scaling;
127
        this.export_params = export_params;
128
        this.replace_all = auto_replace_all;
129
    }
130
       
131
    public Exporter.for_temp_file(Gee.Collection<MediaSource> to_export, Scaling scaling,
132
        ExportFormatParameters export_params) {
133 134 135
        this.to_export.add_all(to_export);
        this.dir = null;
        this.scaling = scaling;
136
        this.export_params = export_params;
137
    }
138

139 140 141 142 143 144 145
    // This should be called only once; the object does not reset its internal state when completed.
    public void export(CompletionCallback completion_callback, ExportFailedCallback error_callback,
        OverwriteCallback overwrite_callback, Cancellable? cancellable, ProgressMonitor? monitor) {
        this.completion_callback = completion_callback;
        this.error_callback = error_callback;
        this.overwrite_callback = overwrite_callback;
        this.monitor = monitor;
146
        this.cancellable = cancellable ?? new Cancellable();
147 148
        
        if (!process_queue())
149
            export_completed(true);
150 151 152 153 154 155 156
    }
    
    private void on_exported(BackgroundJob j) {
        ExportJob job = (ExportJob) j;
        
        completed_count++;
        
157 158
        // because the monitor spins the event loop, and so it's possible this function will be
        // re-entered, decide now if this is the last job
159
        bool completed = completed_count == to_export.size;
160 161
        
        if (!aborted && job.err != null) {
162
            if (!error_callback(this, job.dest, to_export.size - completed_count, job.err)) {
163
                aborted = true;
164
                
165 166
                if (!completed)
                    return;
167 168 169
            }
        }
        
170
        if (!aborted && monitor != null) {
171
            if (!monitor(completed_count, to_export.size, false)) {
172
                aborted = true;
173
                
174 175
                if (!completed)
                    return;
176 177
            } else {
                exported_files += job.dest;
178 179 180
            }
        }
        
181
        if (completed)
182
            export_completed(false);
183 184
    }
    
185
    private void on_export_cancelled(BackgroundJob j) {
186
        if (++completed_count == to_export.size)
187
            export_completed(true);
188 189
    }
    
190 191 192 193
    public File[] get_exported_files() {
        return exported_files;
    }
    
194 195
    private bool process_queue() {
        int submitted = 0;
196 197
        foreach (MediaSource source in to_export) {
            File? use_source_file = null;
198 199 200 201 202 203 204 205 206 207
            PhotoFileFormat real_export_format = PhotoFileFormat.get_system_default_format();
            string? basename = null;
            if (source is Photo) {
                Photo photo = (Photo) source;
                real_export_format = photo.get_export_format_for_parameters(export_params);
                basename = photo.get_export_basename_for_parameters(export_params);
            } else if (source is Video) {
                basename = ((Video) source).get_basename();
            }
            assert(basename != null);
208
            
209 210 211 212 213 214 215
            if (use_source_file != null) {
                exported_files += use_source_file;
                
                completed_count++;
                if (monitor != null) {
                    if (!monitor(completed_count, to_export.size)) {
                        cancellable.cancel();
216 217
                        
                        return false;
218 219 220 221 222 223 224 225 226 227 228 229
                    }
                }
                
                continue;
            }
            
            File? export_dir = dir;
            File? dest = null;
            
            if (export_dir == null) {
                try {
                    bool collision;
230
                    dest = generate_unique_file(AppDirs.get_temp_dir(), basename, out collision);
231 232 233
                } catch (Error err) {
                    AppWindow.error_message(_("Unable to generate a temporary file for %s: %s").printf(
                        source.get_file().get_basename(), err.message));
234
                    
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
                    break;
                }
            } else {
                dest = dir.get_child(basename);
                
                if (!replace_all && dest.query_exists(null)) {
                    switch (overwrite_callback(this, dest)) {
                        case Overwrite.YES:
                            // continue
                        break;
                        
                        case Overwrite.REPLACE_ALL:
                            replace_all = true;
                        break;
                        
                        case Overwrite.CANCEL:
                            cancellable.cancel();
                            
                            return false;
254
                        
255 256 257 258 259 260 261 262 263 264 265 266 267
                        case Overwrite.NO:
                        default:
                            completed_count++;
                            if (monitor != null) {
                                if (!monitor(completed_count, to_export.size)) {
                                    cancellable.cancel();
                                    
                                    return false;
                                }
                            }
                            
                            continue;
                    }
268 269
                }
            }
270 271

            workers.enqueue(new ExportJob(this, source, dest, scaling, export_params.quality,
Jonas Bushart's avatar
Jonas Bushart committed
272
                real_export_format, cancellable, export_params.mode == ExportFormatMode.UNMODIFIED, export_params.export_metadata));
273 274 275 276 277 278
            submitted++;
        }
        
        return submitted > 0;
    }
    
279 280
    private void export_completed(bool is_cancelled) {
        completion_callback(this, is_cancelled);
281 282 283
    }
}

284 285
public class ExporterUI {
    private Exporter exporter;
286 287
    private Cancellable cancellable = new Cancellable();
    private ProgressDialog? progress_dialog = null;
288
    private unowned Exporter.CompletionCallback? completion_callback = null;
289
    
290
    public ExporterUI(Exporter exporter) {
291 292 293
        this.exporter = exporter;
    }
    
294
    public void export(Exporter.CompletionCallback completion_callback) {
295 296 297 298 299 300 301 302 303
        this.completion_callback = completion_callback;
        
        AppWindow.get_instance().set_busy_cursor();
        
        progress_dialog = new ProgressDialog(AppWindow.get_instance(), _("Exporting"), cancellable);
        exporter.export(on_export_completed, on_export_failed, on_export_overwrite, cancellable,
            progress_dialog.monitor);
    }
    
304
    private void on_export_completed(Exporter exporter, bool is_cancelled) {
305 306 307 308 309 310 311
        if (progress_dialog != null) {
            progress_dialog.close();
            progress_dialog = null;
        }
        
        AppWindow.get_instance().set_normal_cursor();
        
312
        completion_callback(exporter, is_cancelled);
313 314
    }
    
315
    private Exporter.Overwrite on_export_overwrite(Exporter exporter, File file) {
316
        progress_dialog.set_modal(false);
317
        string question = _("File %s already exists. Replace?").printf(file.get_basename());
318
        Gtk.ResponseType response = AppWindow.negate_affirm_all_cancel_question(question, 
319
            _("_Skip"), _("_Replace"), _("Replace _All"), _("Export"));
320
        
321 322
        progress_dialog.set_modal(true);

323 324
        switch (response) {
            case Gtk.ResponseType.APPLY:
325
                return Exporter.Overwrite.REPLACE_ALL;
326 327
            
            case Gtk.ResponseType.YES:
328
                return Exporter.Overwrite.YES;
329 330
            
            case Gtk.ResponseType.CANCEL:
331
                return Exporter.Overwrite.CANCEL;
332 333 334
            
            case Gtk.ResponseType.NO:
            default:
335
                return Exporter.Overwrite.NO;
336 337 338
        }
    }
    
339
    private bool on_export_failed(Exporter exporter, File file, int remaining, Error err) {
340 341 342 343
        return export_error_dialog(file, remaining > 0) != Gtk.ResponseType.CANCEL;
    }
}