summaryrefslogtreecommitdiff
path: root/src/main.vala
blob: 32e3d83dbcccc0419b2e3dde8fb22850ed9bd7d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

enum ShotwellCommand {
    // user-defined commands must be positive ints
    MOUNTED_CAMERA = 1
}

private Timer startup_timer = null;
private bool was_already_running = false;

void library_exec(string[] mounts) {
    was_already_running = Application.get_is_remote();
    
    if (was_already_running) {
        // Send attached cameras out to the primary instance.
        // The primary instance will get a 'command-line' signal with mounts[]
        // as an argument, and an 'activate', which will present the window.
        //
        // This will also take care of killing us when it sees that another
        // instance was already registered.
        Application.present_primary_instance();
        Application.send_to_primary_instance(mounts);
        return;
    }
    
    // preconfigure units
    Db.preconfigure(AppDirs.get_data_subdir("data").get_child("photo.db"));
    
    // initialize units
    try {
        Library.app_init();
    } catch (Error err) {
        AppWindow.panic(err.message);
        
        return;
    }
    
    // validate the databases prior to using them
    message("Verifying database…");
    string errormsg = null;
    string app_version;
    int schema_version;
    Db.VerifyResult result = Db.verify_database(out app_version, out schema_version);
    switch (result) {
        case Db.VerifyResult.OK:
            // do nothing; no problems
        break;
        
        case Db.VerifyResult.FUTURE_VERSION:
            errormsg = _("Your photo library is not compatible with this version of Shotwell. It appears it was created by Shotwell %s (schema %d). This version is %s (schema %d). Please use the latest version of Shotwell.").printf(
                app_version, schema_version, Resources.APP_VERSION, DatabaseTable.SCHEMA_VERSION);
        break;
        
        case Db.VerifyResult.UPGRADE_ERROR:
            errormsg = _("Shotwell was unable to upgrade your photo library from version %s (schema %d) to %s (schema %d). For more information please check the Shotwell Wiki at %s").printf(
                app_version, schema_version, Resources.APP_VERSION, DatabaseTable.SCHEMA_VERSION,
                Resources.HOME_URL);
        break;
        
        case Db.VerifyResult.NO_UPGRADE_AVAILABLE:
            errormsg = _("Your photo library is not compatible with this version of Shotwell. It appears it was created by Shotwell %s (schema %d). This version is %s (schema %d). Please clear your library by deleting %s and re-import your photos.").printf(
                app_version, schema_version, Resources.APP_VERSION, DatabaseTable.SCHEMA_VERSION,
                AppDirs.get_data_dir().get_path());
        break;
        
        default:
            errormsg = _("Unknown error attempting to verify Shotwell’s database: %s").printf(
                result.to_string());
        break;
    }

    // Need to set this before anything else, but _after_ setting the profile
    var use_dark = Config.Facade.get_instance().get_gtk_theme_variant();
    Gtk.Settings.get_default().gtk_application_prefer_dark_theme = use_dark;
    
    if (errormsg != null) {
        Gtk.MessageDialog dialog = new Gtk.MessageDialog(null, Gtk.DialogFlags.MODAL, 
            Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s", errormsg);
        dialog.title = Resources.APP_TITLE;
        dialog.run();
        dialog.destroy();
        
        DatabaseTable.terminate();
        
        return;
    }
    
    Upgrades.init();
    
    ProgressDialog progress_dialog = null;
    AggregateProgressMonitor aggregate_monitor = null;
    ProgressMonitor monitor = null;
    
    if (!CommandlineOptions.no_startup_progress) {
        // only throw up a startup progress dialog if over a reasonable amount of objects ... multiplying
        // photos by two because there's two heavy-duty operations on them: creating the LibraryPhoto
        // objects and then populating the initial page with them.
        uint64 grand_total = PhotoTable.get_instance().get_row_count()
            + EventTable.get_instance().get_row_count()
            + TagTable.get_instance().get_row_count()
            + VideoTable.get_instance().get_row_count()
            + FaceTable.get_instance().get_row_count()
            + FaceLocationTable.get_instance().get_row_count()
            + Upgrades.get_instance().get_step_count();
        if (grand_total > 5000) {
            progress_dialog = new ProgressDialog(null, _("Loading Shotwell"));
            progress_dialog.update_display_every(100);
            progress_dialog.set_minimum_on_screen_time_msec(250);
            try {
                progress_dialog.icon = new Gdk.Pixbuf.from_resource("/org/gnome/Shotwell/icons/hicolor/scalable/org.gnome.Shotwell.svg");
            } catch (Error err) {
                debug("Warning - could not load application icon for loading window: %s", err.message);
            }
            
            aggregate_monitor = new AggregateProgressMonitor(grand_total, progress_dialog.monitor);
            monitor = aggregate_monitor.monitor;
        }
    }
    
    ThumbnailCache.init();
    Tombstone.init();

    LibraryFiles.select_copy_function();
    
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("LibraryPhoto.init");
    LibraryPhoto.init(monitor);
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("Video.init");
    Video.init(monitor);
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("Upgrades.execute");
    Upgrades.get_instance().execute();
    
    LibraryMonitorPool.init();
    MediaCollectionRegistry.init();
    MediaCollectionRegistry registry = MediaCollectionRegistry.get_instance();
    registry.register_collection(LibraryPhoto.global);
    registry.register_collection(Video.global);
    
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("Event.init");
    Event.init(monitor);
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("Tag.init");
    Tag.init(monitor);
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("FaceLocation.init");
    FaceLocation.init(monitor);
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("Face.init");
    Face.init(monitor);
    
    MetadataWriter.init();
    DesktopIntegration.init();
    
    Application.get_instance().init_done();
    
    // create main library application window
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("LibraryWindow");
    LibraryWindow library_window = new LibraryWindow(monitor);
    
    if (aggregate_monitor != null)
        aggregate_monitor.next_step("done");
    
    // destroy and tear down everything ... no need for them to stick around the lifetime of the
    // application
    
    monitor = null;
    aggregate_monitor = null;
    if (progress_dialog != null)
        progress_dialog.destroy();
    progress_dialog = null;

    // report mount points
    foreach (string mount in mounts)
        library_window.mounted_camera_shell_notification(mount, true);

    library_window.show_all();

    WelcomeServiceEntry[] selected_import_entries = new WelcomeServiceEntry[0];
    if (Config.Facade.get_instance().get_show_welcome_dialog() &&
        LibraryPhoto.global.get_count() == 0) {
        WelcomeDialog welcome = new WelcomeDialog(library_window);
        Config.Facade.get_instance().set_show_welcome_dialog(welcome.execute(out selected_import_entries,
            out do_system_pictures_import));
    } else {
        Config.Facade.get_instance().set_show_welcome_dialog(false);
    }
    
    if (selected_import_entries.length > 0) {
        do_external_import = true;
        foreach (WelcomeServiceEntry entry in selected_import_entries)
            entry.execute();
    } 
    if (do_system_pictures_import) {
        /*  Do the system import even if other plugins have run as some plugins may not
            as some plugins may not import pictures from the system folder.
         */
        run_system_pictures_import();
    }
    
    debug("%lf seconds to Gtk.main()", startup_timer.elapsed());
    
    Application.get_instance().start();
    
    DesktopIntegration.terminate();
    MetadataWriter.terminate();
    Tag.terminate();
    Event.terminate();
    LibraryPhoto.terminate();
    MediaCollectionRegistry.terminate();
    LibraryMonitorPool.terminate();
    Tombstone.terminate();
    ThumbnailCache.terminate();
    Video.terminate();
    Face.terminate();
    FaceLocation.terminate();

    Library.app_terminate();
}

private bool do_system_pictures_import = false;
private bool do_external_import = false;

public void run_system_pictures_import(ImportManifest? external_exclusion_manifest = null) {
    if (!do_system_pictures_import)
        return;

    Gee.ArrayList<FileImportJob> jobs = new Gee.ArrayList<FileImportJob>();
    jobs.add(new FileImportJob(AppDirs.get_import_dir(), false, true));
    
    LibraryWindow library_window = (LibraryWindow) AppWindow.get_instance();
    
    BatchImport batch_import = new BatchImport(jobs, "startup_import",
        report_system_pictures_import, null, null, null, null, external_exclusion_manifest);
    library_window.enqueue_batch_import(batch_import, true);

    library_window.switch_to_import_queue_page();
}

private void report_system_pictures_import(ImportManifest manifest, BatchImportRoll import_roll) {
    /* Don't report the manifest to the user if exteral import was done and the entire manifest
       is empty. An empty manifest in this case results from files that were already imported
       in the external import phase being skipped. Note that we are testing against manifest.all,
       not manifest.success; manifest.all is zero when no files were enqueued for import in the
       first place and the only way this happens is if all files were skipped -- even failed
       files are counted in manifest.all */
    if (do_external_import && (manifest.all.size == 0))
        return;

    ImportUI.report_manifest(manifest, true);
}

void dump_tags (GExiv2.Metadata metadata, string[] tags) throws Error {
    foreach (string tag in tags) {
        try {
            print("%-64s%s\n",
                tag,
                metadata.try_get_tag_interpreted_string (tag));
        } catch (Error err) {
            print("Failed to get tag %s: %s\n", tag, err.message);
        }
    }
}

void dump_metadata (string filename) {
    try {
        var metadata = new GExiv2.Metadata();
        var file = File.new_for_commandline_arg(filename);
        metadata.from_stream (file.read());

        dump_tags(metadata, metadata.get_exif_tags());
        dump_tags(metadata, metadata.get_iptc_tags());
        dump_tags(metadata, metadata.get_xmp_tags());
    } catch (Error err) {
        stderr.printf("Unable to dump metadata for %s: %s\n", filename, err.message);
    }
}

void editing_exec(string filename, bool fullscreen) {
    File initial_file = File.new_for_commandline_arg(filename);
    
    // preconfigure units
    Direct.preconfigure(initial_file);
    Db.preconfigure(null);

    // Need to set this before anything else, but _after_ setting the profile
    var use_dark = Config.Facade.get_instance().get_gtk_theme_variant();
    Gtk.Settings.get_default().gtk_application_prefer_dark_theme = use_dark;

    // initialize units for direct-edit mode
    try {
        Direct.app_init();
    } catch (Error err) {
        AppWindow.panic(err.message);
        
        return;
    }
    
    // init modules direct-editing relies on
    DesktopIntegration.init();
    
    // TODO: At some point in the future, to support mixed-media in direct-edit mode, we will
    //       refactor DirectPhotoSourceCollection to be a MediaSourceCollection. At that point,
    //       we'll need to register DirectPhoto.global with the MediaCollectionRegistry
    
    DirectWindow direct_window = new DirectWindow(initial_file);
    direct_window.show_all();
    direct_window.maximize();
    
    debug("%lf seconds to Gtk.main()", startup_timer.elapsed());

    if (fullscreen) {
        var action = direct_window.get_common_action("CommonFullscreen");
        if (action != null) {
            action.activate(null);
        }
    }
    
    Application.get_instance().start();

    DesktopIntegration.terminate();
    
    // terminate units for direct-edit mode
    Direct.app_terminate();
}

namespace CommandlineOptions {

bool no_startup_progress = false;
string? data_dir = null;
bool show_version = false;
bool no_runtime_monitoring = false;
bool fullscreen = false;
bool show_metadata = false;
string? profile = null;
bool create_profile = false;
bool list_profiles = false;
bool browse_profiles = false;

const OptionEntry[] entries = {
    { "datadir", 'd', 0, OptionArg.FILENAME, ref data_dir, N_("Path to Shotwell’s private data"), N_("DIRECTORY") },
    { "no-runtime-monitoring", 0, 0, OptionArg.NONE, ref no_runtime_monitoring, N_("Do not monitor library directory at runtime for changes"), null },
    { "no-startup-progress", 0, 0, OptionArg.NONE, ref no_startup_progress, N_("Don’t display startup progress meter"), null },
    { "version", 'V', 0, OptionArg.NONE, ref show_version, N_("Show the application’s version") },
    { "fullscreen", 'f', 0, OptionArg.NONE, ref fullscreen, N_("Start the application in fullscreen mode"), null },
    { "show-metadata", 'p', 0, OptionArg.NONE, ref show_metadata, N_("Print the metadata of the image file"), null },
    { "profile", 'i', 0, OptionArg.STRING, ref profile, N_("Name for a custom profile"), N_("PROFILE") },
    { "profile-browser", 'b', 0, OptionArg.NONE, ref browse_profiles, N_("Start with a browser of available profiles"), null },
    { "create", 'c', 0, OptionArg.NONE, ref create_profile, N_("If PROFILE given with --profile does not exist, create it"), null },
    { "list-profiles", 'l', 0, OptionArg.NONE, ref list_profiles, N_("Show available profiles"), null },
    { null, 0, 0, 0, null, null, null }
};
}

void main(string[] args) {
    // Call AppDirs init *before* calling Gtk.init_with_args, as it will strip the
    // exec file from the array
    AppDirs.init(args[0]);

    // This has to be done before the AppWindow is created in order to ensure the XMP
    // parser is initialized in a thread-safe fashion; please see 
    // https://bugzilla.gnome.org/show_bug.cgi?id=717931 for details.
    GExiv2.initialize();
    GExiv2.log_use_glib_logging();

    // Set GExiv2 log level to DEBUG, filtering will be done through Shotwell
    // logging mechanisms
    GExiv2.log_set_level(GExiv2.LogLevel.DEBUG);

    // If set to non-empty, initialize GdkPixbuf with an additional loader path
    if (Resources.PIXBUF_LOADER_PATH != "") {
        debug("Trying to set module path to %s", Resources.PIXBUF_LOADER_PATH);
        try {
            Gdk.Pixbuf.init_modules(Resources.PIXBUF_LOADER_PATH);
        } catch (Error err) {
            message("Failed to set additional pixbuf loader path: %s", err.message);
        }
    }

    // following the GIO programming guidelines at http://developer.gnome.org/gio/2.26/ch03.html,
    // set the GSETTINGS_SCHEMA_DIR environment variable to allow us to load GSettings schemas from 
    // the build directory. this allows us to access local GSettings schemas without having to
    // muck with the user's XDG_... directories, which is seriously frowned upon
    if (AppDirs.get_install_dir() == null) {
        GLib.Environment.set_variable("GSETTINGS_SCHEMA_DIR", AppDirs.get_lib_dir().get_path() +
            "/data/gsettings", true);
    }

    // init GTK (valac has already called g_threads_init())
    try {
        Gtk.init_with_args(ref args, _("[FILE]"), CommandlineOptions.entries,
            Resources.APP_GETTEXT_PACKAGE);

    } catch (Error e) {
        print(e.message + "\n");
        print(_("Run “%s --help” to see a full list of available command line options.\n"), args[0]);
        AppDirs.terminate();
        return;
    }

    if (CommandlineOptions.browse_profiles) {
        var window = new Gtk.Dialog();
        window.set_title (_("Choose Shotwell's profile"));
        var browser = new Shotwell.ProfileBrowser();
        browser.profile_activated.connect((profile) => {
            if (profile.id != Shotwell.Profile.SYSTEM) {
                CommandlineOptions.profile = profile.name;
                CommandlineOptions.data_dir = profile.data_dir;
            } else {
                CommandlineOptions.profile = null;
            }
            window.response(Gtk.ResponseType.OK);
        });
        window.get_content_area().add(browser);
        window.set_size_request(430, 560);
        var response = window.run();
        window.destroy();
        // Anything else than selecting an entry in the list will stop shotwell from starting
        if (response != Gtk.ResponseType.OK) {
            return;
        }
    }

    // Setup profile manager
    if (CommandlineOptions.profile != null) {
        var manager = Shotwell.ProfileManager.get_instance();
        if (!manager.has_profile (CommandlineOptions.profile)) {
             if (!CommandlineOptions.create_profile) {
                print(_("Profile %s does not exist. Did you mean to pass --create as well?"),
                      CommandlineOptions.profile);
                AppDirs.terminate();
                return;
             }
        }
        manager.set_profile(CommandlineOptions.profile);
        CommandlineOptions.data_dir = manager.derive_data_dir(CommandlineOptions.data_dir);
    } else {
        message("Starting session with system profile");
    }

    if (CommandlineOptions.show_version) {
        if (Resources.GIT_VERSION != "")
            print("%s %s (%s)\n", Resources.APP_TITLE, Resources.APP_VERSION, Resources.GIT_VERSION);
        else
            print("%s %s\n", Resources.APP_TITLE, Resources.APP_VERSION);

        AppDirs.terminate();
        
        return;
    }

    if (CommandlineOptions.list_profiles) {
        var manager  = Shotwell.ProfileManager.get_instance();
        manager.print_profiles();

        AppDirs.terminate();

        return;
    }

    // init debug prior to anything else (except Gtk, which it relies on, and AppDirs, which needs
    // to be set ASAP) ... since we need to know what mode we're in, examine the command-line
    // first
    
    // walk command-line arguments for camera mounts or filename for direct editing ... only one
    // filename supported for now, so take the first one and drop the rest ... note that URIs for
    // filenames are currently not permitted, to differentiate between mount points
    string[] mounts = new string[0];
    string filename = null;

    foreach (var arg in args[1:args.length]) {
        if (LibraryWindow.is_mount_uri_supported(arg)) {
            mounts += arg;
        } else if (is_string_empty(filename) && !arg.contains("://")) {
            filename = arg;
        }
    }

    if (CommandlineOptions.show_metadata) {
        dump_metadata (filename);

        AppDirs.terminate();

        return;
    }
    
    Debug.init(is_string_empty(filename) ? Debug.LIBRARY_PREFIX : Debug.VIEWER_PREFIX);

    if (Resources.GIT_VERSION != "")
        message("Shotwell %s %s (%s)",
            is_string_empty(filename) ? Resources.APP_LIBRARY_ROLE : Resources.APP_DIRECT_ROLE,
            Resources.APP_VERSION, Resources.GIT_VERSION);
    else
        message("Shotwell %s %s",
            is_string_empty(filename) ? Resources.APP_LIBRARY_ROLE : Resources.APP_DIRECT_ROLE,
            Resources.APP_VERSION);
    debug ("Shotwell is running in timezone %s", new
           DateTime.now_local().get_timezone_abbreviation ());
        
    // Have a filename here?  If so, configure ourselves for direct
    // mode, otherwise, default to library mode.
    Application.init(!is_string_empty(filename));
    
    // set custom data directory if it's been supplied
    if (CommandlineOptions.data_dir != null)
        AppDirs.set_data_dir(CommandlineOptions.data_dir);
    else
        AppDirs.try_migrate_data();
    
    // Verify the private data directory before continuing
    AppDirs.verify_data_dir();
    AppDirs.verify_cache_dir();
    
    // init internationalization with the default system locale
    InternationalSupport.init(Resources.APP_GETTEXT_PACKAGE, args);
    
    startup_timer = new Timer();
    startup_timer.start();
    
    // set up GLib environment
    GLib.Environment.set_application_name(Resources.APP_TITLE);
    
    // in both the case of running as the library or an editor, Resources is always
    // initialized
    Resources.init();
    
    // since it's possible for a mount name to be passed that's not supported (and hence an empty
    // mount list), or for nothing to be on the command-line at all, only go to direct editing if a
    // filename is spec'd
    if (is_string_empty(filename))
        library_exec(mounts);
    else
        editing_exec(filename, CommandlineOptions.fullscreen);
    
    // terminate mode-inspecific modules
    Resources.terminate();
    Application.terminate();
    Debug.terminate();
    AppDirs.terminate();

    // Back up db on successful run so we have something to roll back to if
    // it gets corrupted in the next session.  Don't do this if another shotwell
    // is open or if we're in direct mode.
    if (is_string_empty(filename) && !was_already_running) {
        string orig_path = AppDirs.get_data_subdir("data").get_child("photo.db").get_path();
        string backup_path = orig_path + ".bak";
        try {
            File src = File.new_for_commandline_arg(orig_path);
            File dest = File.new_for_commandline_arg(backup_path);
            src.copy(dest,
                     FileCopyFlags.OVERWRITE |
                     FileCopyFlags.ALL_METADATA);
        } catch(Error error) {
            warning("Failed to create backup file of database: %s",
                    error.message);
        }
        Posix.sync();
    }
}