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

7 8 9 10 11
// PageCommand stores the current page when a Command is created.  Subclasses can call return_to_page()
// if it's appropriate to return to that page when executing an undo() or redo().
public abstract class PageCommand : Command {
    private Page? page;
    private bool auto_return = true;
12 13
    private Photo library_photo = null;
    private CollectionPage collection_page = null;
14
    
15
    protected PageCommand(string name, string explanation) {
16 17 18
        base (name, explanation);
        
        page = AppWindow.get_instance().get_current_page();
19 20
        
        if (page != null) {
21
            page.destroy.connect(on_page_destroyed);
22 23 24 25 26 27 28 29 30 31 32
            
            // If the command occurred on a LibaryPhotoPage, the PageCommand must record additional
            // objects to be restore it to its old state: a specific photo to focus on, a page to return 
            // to, and a view collection to operate over. Note that these objects can be cleared if 
            // the page goes into the background. The required objects are stored below.
            LibraryPhotoPage photo_page = page as LibraryPhotoPage;
            if (photo_page != null) {
                library_photo = photo_page.get_photo();
                collection_page = photo_page.get_controller_page();
                
                if (library_photo != null && collection_page != null) {
33 34
                    library_photo.destroyed.connect(on_photo_destroyed);
                    collection_page.destroy.connect(on_controller_destroyed);
35 36 37 38 39 40
                } else {
                    library_photo = null;
                    collection_page = null;
                }
            }
        }
41 42 43 44
    }
    
    ~PageCommand() {
        if (page != null)
45
            page.destroy.disconnect(on_page_destroyed);
46 47
        
        if (library_photo != null)
48
            library_photo.destroyed.disconnect(on_photo_destroyed);
49 50

        if (collection_page != null)
51
            collection_page.destroy.disconnect(on_controller_destroyed);
52 53 54 55 56 57 58 59 60 61 62 63 64 65
    }
    
    public void set_auto_return_to_page(bool auto_return) {
        this.auto_return = auto_return;
    }
    
    public override void prepare() {
        if (auto_return)
            return_to_page();
        
        base.prepare();
    }
    
    public void return_to_page() {
66 67 68
        LibraryPhotoPage photo_page = page as LibraryPhotoPage;  

        if (photo_page != null) { 
69 70 71 72
            if (library_photo != null && collection_page != null) {
                bool photo_in_collection = false;
                int count = collection_page.get_view().get_count();
                for (int i = 0; i < count; i++) {
Jim Nelson's avatar
Jim Nelson committed
73
                    if ( ((Thumbnail) collection_page.get_view().get_at(i)).get_media_source() == library_photo) {
74 75 76 77 78 79 80 81
                        photo_in_collection = true;
                        break;
                    }
                }
                
                if (photo_in_collection)
                    LibraryWindow.get_app().switch_to_photo_page(collection_page, library_photo);
            }
82
        } else if (page != null)
83 84 85
            AppWindow.get_instance().set_current_page(page);
    }
    
86
    private void on_page_destroyed() {
87
        page.destroy.disconnect(on_page_destroyed);
88
        page = null;
89
    }
90 91
    
    private void on_photo_destroyed() {
92
        library_photo.destroyed.disconnect(on_photo_destroyed);
93 94 95 96
        library_photo = null;
    }

    private void on_controller_destroyed() {
97
        collection_page.destroy.disconnect(on_controller_destroyed);
98 99 100
        collection_page = null;
    }

101 102 103
}

public abstract class SingleDataSourceCommand : PageCommand {
104 105
    protected DataSource source;
    
106
    protected SingleDataSourceCommand(DataSource source, string name, string explanation) {
107
        base(name, explanation);
108 109 110
        
        this.source = source;
        
111
        source.destroyed.connect(on_source_destroyed);
112 113 114
    }
    
    ~SingleDataSourceCommand() {
115
        source.destroyed.disconnect(on_source_destroyed);
116 117
    }
    
118 119 120 121
    public DataSource get_source() {
        return source;
    }
    
122 123 124
    private void on_source_destroyed() {
        // too much risk in simply removing this from the CommandManager; if this is considered too
        // broad a brushstroke, can return to this later
125
        get_command_manager().reset();
126 127 128
    }
}

Jim Nelson's avatar
Jim Nelson committed
129 130
public abstract class SimpleProxyableCommand : PageCommand {
    private SourceProxy proxy;
131
    private Gee.HashSet<SourceProxy> proxies = new Gee.HashSet<SourceProxy>();
Jim Nelson's avatar
Jim Nelson committed
132
    
133
    protected SimpleProxyableCommand(Proxyable proxyable, string name, string explanation) {
Jim Nelson's avatar
Jim Nelson committed
134 135 136
        base (name, explanation);
        
        proxy = proxyable.get_proxy();
137
        proxy.broken.connect(on_proxy_broken);
Jim Nelson's avatar
Jim Nelson committed
138 139 140
    }
    
    ~SimpleProxyableCommand() {
141
        proxy.broken.disconnect(on_proxy_broken);
142
        clear_added_proxies();
Jim Nelson's avatar
Jim Nelson committed
143 144 145 146 147 148 149 150 151 152 153 154 155 156
    }
    
    public override void execute() {
        execute_on_source(proxy.get_source());
    }
    
    protected abstract void execute_on_source(DataSource source);
    
    public override void undo() {
        undo_on_source(proxy.get_source());
    }
    
    protected abstract void undo_on_source(DataSource source);
    
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    // If the Command deals with other Proxyables during processing, it can add them here and the
    // SimpleProxyableCommand will deal with created a SourceProxy and if it signals it's broken.
    // Note that these cannot be removed programatically, but only cleared en masse; it's expected
    // this is fine for the nature of a Command.
    protected void add_proxyables(Gee.Collection<Proxyable> proxyables) {
        foreach (Proxyable proxyable in proxyables) {
            SourceProxy added_proxy = proxyable.get_proxy();
            added_proxy.broken.connect(on_proxy_broken);
            proxies.add(added_proxy);
        }
    }
    
    // See add_proxyables() for a note on use.
    protected void clear_added_proxies() {
        foreach (SourceProxy added_proxy in proxies)
            added_proxy.broken.disconnect(on_proxy_broken);
        
        proxies.clear();
    }
    
Jim Nelson's avatar
Jim Nelson committed
177
    private void on_proxy_broken() {
178
        debug("on_proxy_broken");
Jim Nelson's avatar
Jim Nelson committed
179 180 181 182
        get_command_manager().reset();
    }
}

183 184 185
public abstract class SinglePhotoTransformationCommand : SingleDataSourceCommand {
    private PhotoTransformationState state;
    
186
    protected SinglePhotoTransformationCommand(Photo photo, string name, string explanation) {
187
        base(photo, name, explanation);
188 189
        
        state = photo.save_transformation_state();
190
        state.broken.connect(on_state_broken);
191 192 193
    }
    
    ~SinglePhotoTransformationCommand() {
194
        state.broken.disconnect(on_state_broken);
195 196 197
    }
    
    public override void undo() {
Jim Nelson's avatar
Jim Nelson committed
198
        ((Photo) source).load_transformation_state(state);
199
    }
200 201 202 203
    
    private void on_state_broken() {
        get_command_manager().reset();
    }
204 205
}

206 207 208 209
public abstract class GenericPhotoTransformationCommand : SingleDataSourceCommand {
    private PhotoTransformationState original_state = null;
    private PhotoTransformationState transformed_state = null;
    
210
    protected GenericPhotoTransformationCommand(Photo photo, string name, string explanation) {
211 212 213
        base(photo, name, explanation);
    }
    
214
    ~GenericPhotoTransformationCommand() {
215
        if (original_state != null)
216
            original_state.broken.disconnect(on_state_broken);
217 218
        
        if (transformed_state != null)
219
            transformed_state.broken.disconnect(on_state_broken);
220 221
    }
    
222
    public override void execute() {
Jim Nelson's avatar
Jim Nelson committed
223
        Photo photo = (Photo) source;
224 225
        
        original_state = photo.save_transformation_state();
226
        original_state.broken.connect(on_state_broken);
227 228 229 230
        
        execute_on_photo(photo);
        
        transformed_state = photo.save_transformation_state();
231
        transformed_state.broken.connect(on_state_broken);
232 233
    }
    
Jim Nelson's avatar
Jim Nelson committed
234
    public abstract void execute_on_photo(Photo photo);
235 236 237
    
    public override void undo() {
        // use the original state of the photo
Jim Nelson's avatar
Jim Nelson committed
238
        ((Photo) source).load_transformation_state(original_state);
239 240 241 242
    }
    
    public override void redo() {
        // use the state of the photo after transformation
Jim Nelson's avatar
Jim Nelson committed
243
        ((Photo) source).load_transformation_state(transformed_state);
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
    }
    
    protected virtual bool can_compress(Command command) {
        return false;
    }
    
    public override bool compress(Command command) {
        if (!can_compress(command))
            return false;
        
        GenericPhotoTransformationCommand generic = command as GenericPhotoTransformationCommand;
        if (generic == null)
            return false;
        
        if (generic.source != source)
            return false;
        
        // execute this new (and successive) command
        generic.execute();
        
        // save it's new transformation state as ours
        transformed_state = generic.transformed_state;
        
        return true;
    }
269 270 271 272
    
    private void on_state_broken() {
        get_command_manager().reset();
    }
273 274
}

275
public abstract class MultipleDataSourceCommand : PageCommand {
276 277 278 279 280 281 282
    protected const int MIN_OPS_FOR_PROGRESS_WINDOW = 5;
    
    protected Gee.ArrayList<DataSource> source_list = new Gee.ArrayList<DataSource>();
    
    private string progress_text;
    private string undo_progress_text;
    private Gee.ArrayList<DataSource> acted_upon = new Gee.ArrayList<DataSource>();
283
    private Gee.HashSet<SourceCollection> hooked_collections = new Gee.HashSet<SourceCollection>();
284
    
285
    protected MultipleDataSourceCommand(Gee.Iterable<DataView> iter, string progress_text,
286 287
        string undo_progress_text, string name, string explanation) {
        base(name, explanation);
288 289 290 291 292 293
        
        this.progress_text = progress_text;
        this.undo_progress_text = undo_progress_text;
        
        foreach (DataView view in iter) {
            DataSource source = view.get_source();
294 295 296 297
            SourceCollection? collection = (SourceCollection) source.get_membership();
    
            if (collection != null) {
                hooked_collections.add(collection);
298 299 300 301
            }
            source_list.add(source);
        }
        
302 303 304
        foreach (SourceCollection current_collection in hooked_collections) {
            current_collection.item_destroyed.connect(on_source_destroyed);
        }
305 306 307
    }
    
    ~MultipleDataSourceCommand() {
308 309 310
        foreach (SourceCollection current_collection in hooked_collections) {
            current_collection.item_destroyed.disconnect(on_source_destroyed);
        }
311 312
    }
    
313 314 315 316 317 318 319 320
    public Gee.Iterable<DataSource> get_sources() {
        return source_list;
    }
    
    public int get_source_count() {
        return source_list.size;
    }
    
321 322 323 324
    private void on_source_destroyed(DataSource source) {
        // as with SingleDataSourceCommand, too risky to selectively remove commands from the stack,
        // although this could be reconsidered in the future
        if (source_list.contains(source))
325
            get_command_manager().reset();
326 327 328 329
    }
    
    public override void execute() {
        acted_upon.clear();
330 331
        
        start_transaction();
332
        execute_all(true, true, source_list, acted_upon);
333
        commit_transaction();
334 335 336 337 338 339
    }
    
    public abstract void execute_on_source(DataSource source);
    
    public override void undo() {
        if (acted_upon.size > 0) {
340
            start_transaction();
341
            execute_all(false, false, acted_upon, null);
342 343
            commit_transaction();
            
344 345 346 347 348 349
            acted_upon.clear();
        }
    }
    
    public abstract void undo_on_source(DataSource source);
    
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
    private void start_transaction() {
        foreach (SourceCollection sources in hooked_collections) {
            MediaSourceCollection? media_collection = sources as MediaSourceCollection;
            if (media_collection != null)
                media_collection.transaction_controller.begin();
        }
    }
    
    private void commit_transaction() {
        foreach (SourceCollection sources in hooked_collections) {
            MediaSourceCollection? media_collection = sources as MediaSourceCollection;
            if (media_collection != null)
                media_collection.transaction_controller.commit();
        }
    }
    
366 367 368 369 370 371
    private void execute_all(bool exec, bool can_cancel, Gee.ArrayList<DataSource> todo, 
        Gee.ArrayList<DataSource>? completed) {
        AppWindow.get_instance().set_busy_cursor();
        
        int count = 0;
        int total = todo.size;
372 373 374
        int two_percent = (int) ((double) total / 50.0);
        if (two_percent <= 0)
            two_percent = 1;
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
        
        string text = exec ? progress_text : undo_progress_text;
        
        Cancellable cancellable = null;
        ProgressDialog progress = null;
        if (total >= MIN_OPS_FOR_PROGRESS_WINDOW) {
            cancellable = can_cancel ? new Cancellable() : null;
            progress = new ProgressDialog(AppWindow.get_instance(), text, cancellable);
        }
        
        foreach (DataSource source in todo) {
            if (exec)
                execute_on_source(source);
            else
                undo_on_source(source);
            
            if (completed != null)
                completed.add(source);

            if (progress != null) {
395 396 397 398
                if ((++count % two_percent) == 0) {
                    progress.set_fraction(count, total);
                    spin_event_loop();
                }
399 400 401 402 403 404 405 406 407 408 409 410 411
                
                if (cancellable != null && cancellable.is_cancelled())
                    break;
            }
        }
        
        if (progress != null)
            progress.close();
        
        AppWindow.get_instance().set_normal_cursor();
    }
}

412
// TODO: Upgrade MultipleDataSourceAtOnceCommand to use TransactionControllers.
Jim Nelson's avatar
Jim Nelson committed
413 414 415 416
public abstract class MultipleDataSourceAtOnceCommand : PageCommand {
    private Gee.HashSet<DataSource> sources = new Gee.HashSet<DataSource>();
    private Gee.HashSet<SourceCollection> hooked_collections = new Gee.HashSet<SourceCollection>();
    
417
    protected MultipleDataSourceAtOnceCommand(Gee.Collection<DataSource> sources, string name,
Jim Nelson's avatar
Jim Nelson committed
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
        string explanation) {
        base (name, explanation);
        
        this.sources.add_all(sources);
        
        foreach (DataSource source in this.sources) {
            SourceCollection? membership = source.get_membership() as SourceCollection;
            if (membership != null)
                hooked_collections.add(membership);
        }
        
        foreach (SourceCollection source_collection in hooked_collections)
            source_collection.items_destroyed.connect(on_sources_destroyed);
    }
    
    ~MultipleDataSourceAtOnceCommand() {
        foreach (SourceCollection source_collection in hooked_collections)
            source_collection.items_destroyed.disconnect(on_sources_destroyed);
    }
    
    public override void execute() {
        AppWindow.get_instance().set_busy_cursor();
        
        DatabaseTable.begin_transaction();
        MediaCollectionRegistry.get_instance().freeze_all();
        
        execute_on_all(sources);
        
        MediaCollectionRegistry.get_instance().thaw_all();
        try {
            DatabaseTable.commit_transaction();
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        } finally {
            AppWindow.get_instance().set_normal_cursor();
        }
    }
    
    protected abstract void execute_on_all(Gee.Collection<DataSource> sources);
    
    public override void undo() {
        AppWindow.get_instance().set_busy_cursor();
        
        DatabaseTable.begin_transaction();
        MediaCollectionRegistry.get_instance().freeze_all();
        
        undo_on_all(sources);
        
        MediaCollectionRegistry.get_instance().thaw_all();
        try {
            DatabaseTable.commit_transaction();
        } catch (DatabaseError err) {
            AppWindow.database_error(err);
        } finally {
            AppWindow.get_instance().set_normal_cursor();
        }
    }
    
    protected abstract void undo_on_all(Gee.Collection<DataSource> sources);
    
    private void on_sources_destroyed(Gee.Collection<DataSource> destroyed) {
        foreach (DataSource source in destroyed) {
            if (sources.contains(source)) {
                get_command_manager().reset();
                
                break;
            }
        }
    }
}

489
public abstract class MultiplePhotoTransformationCommand : MultipleDataSourceCommand {
Jim Nelson's avatar
Jim Nelson committed
490 491
    private Gee.HashMap<Photo, PhotoTransformationState> map = new Gee.HashMap<
        Photo, PhotoTransformationState>();
492
    
493
    protected MultiplePhotoTransformationCommand(Gee.Iterable<DataView> iter, string progress_text,
494 495
        string undo_progress_text, string name, string explanation) {
        base(iter, progress_text, undo_progress_text, name, explanation);
496 497
        
        foreach (DataSource source in source_list) {
Jim Nelson's avatar
Jim Nelson committed
498
            Photo photo = (Photo) source;
499
            PhotoTransformationState state = photo.save_transformation_state();
500
            state.broken.connect(on_state_broken);
501 502
            
            map.set(photo, state);
503 504 505
        }
    }
    
506 507
    ~MultiplePhotoTransformationCommand() {
        foreach (PhotoTransformationState state in map.values)
508
            state.broken.disconnect(on_state_broken);
509 510
    }
    
511
    public override void undo_on_source(DataSource source) {
Jim Nelson's avatar
Jim Nelson committed
512
        Photo photo = (Photo) source;
513 514 515 516 517 518
        
        PhotoTransformationState state = map.get(photo);
        assert(state != null);
        
        photo.load_transformation_state(state);
    }
519 520 521 522
    
    private void on_state_broken() {
        get_command_manager().reset();
    }
523 524 525 526 527
}

public class RotateSingleCommand : SingleDataSourceCommand {
    private Rotation rotation;
    
Jim Nelson's avatar
Jim Nelson committed
528
    public RotateSingleCommand(Photo photo, Rotation rotation, string name, string explanation) {
529
        base(photo, name, explanation);
530 531 532 533 534
        
        this.rotation = rotation;
    }
    
    public override void execute() {
Jim Nelson's avatar
Jim Nelson committed
535
        ((Photo) source).rotate(rotation);
536 537 538
    }
    
    public override void undo() {
Jim Nelson's avatar
Jim Nelson committed
539
        ((Photo) source).rotate(rotation.opposite());
540 541 542 543 544 545 546
    }
}

public class RotateMultipleCommand : MultipleDataSourceCommand {
    private Rotation rotation;
    
    public RotateMultipleCommand(Gee.Iterable<DataView> iter, Rotation rotation, string name, 
547 548
        string explanation, string progress_text, string undo_progress_text) {
        base(iter, progress_text, undo_progress_text, name, explanation);
549 550 551 552 553
        
        this.rotation = rotation;
    }
    
    public override void execute_on_source(DataSource source) {
Jim Nelson's avatar
Jim Nelson committed
554
        ((Photo) source).rotate(rotation);
555 556 557
    }
    
    public override void undo_on_source(DataSource source) {
Jim Nelson's avatar
Jim Nelson committed
558
        ((Photo) source).rotate(rotation.opposite());
559 560 561
    }
}

562 563 564
public class EditTitleCommand : SingleDataSourceCommand {
    private string new_title;
    private string? old_title;
565
    
566
    public EditTitleCommand(MediaSource source, string new_title) {
567 568 569
        var title = GLib.dpgettext2 (null, "Button Label",
                Resources.EDIT_TITLE_LABEL);
        base(source, title, "");
570
        
571
        this.new_title = new_title;
572
        old_title = source.get_title();
573 574 575
    }
    
    public override void execute() {
576
        ((MediaSource) source).set_title(new_title);
577 578 579
    }
    
    public override void undo() {
580
        ((MediaSource) source).set_title(old_title);
581 582 583
    }
}

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
public class EditCommentCommand : SingleDataSourceCommand {
    private string new_comment;
    private string? old_comment;
    
    public EditCommentCommand(MediaSource source, string new_comment) {
        base(source, Resources.EDIT_COMMENT_LABEL, "");
        
        this.new_comment = new_comment;
        old_comment = source.get_comment();
    }
    
    public override void execute() {
        ((MediaSource) source).set_comment(new_comment);
    }
    
    public override void undo() {
        ((MediaSource) source).set_comment(old_comment);
    }
}

604 605 606 607 608
public class EditMultipleTitlesCommand : MultipleDataSourceAtOnceCommand {
    public string new_title;
    public Gee.HashMap<MediaSource, string?> old_titles = new Gee.HashMap<MediaSource, string?>();
    
    public EditMultipleTitlesCommand(Gee.Collection<MediaSource> media_sources, string new_title) {
609 610 611
        var title = GLib.dpgettext2 (null, "Button Label",
                Resources.EDIT_TITLE_LABEL);
        base (media_sources, title, "");
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
        
        this.new_title = new_title;
        foreach (MediaSource media in media_sources)
            old_titles.set(media, media.get_title());
    }
    
    public override void execute_on_all(Gee.Collection<DataSource> sources) {
        foreach (DataSource source in sources)
            ((MediaSource) source).set_title(new_title);
    }
    
    public override void undo_on_all(Gee.Collection<DataSource> sources) {
        foreach (DataSource source in sources)
            ((MediaSource) source).set_title(old_titles.get((MediaSource) source));
    }
}

629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
public class EditMultipleCommentsCommand : MultipleDataSourceAtOnceCommand {
    public string new_comment;
    public Gee.HashMap<MediaSource, string?> old_comments = new Gee.HashMap<MediaSource, string?>();
    
    public EditMultipleCommentsCommand(Gee.Collection<MediaSource> media_sources, string new_comment) {
        base (media_sources, Resources.EDIT_COMMENT_LABEL, "");
        
        this.new_comment = new_comment;
        foreach (MediaSource media in media_sources)
            old_comments.set(media, media.get_comment());
    }
    
    public override void execute_on_all(Gee.Collection<DataSource> sources) {
        foreach (DataSource source in sources)
            ((MediaSource) source).set_comment(new_comment);
    }
    
    public override void undo_on_all(Gee.Collection<DataSource> sources) {
        foreach (DataSource source in sources)
            ((MediaSource) source).set_comment(old_comments.get((MediaSource) source));
    }
}

Eric Gregory's avatar
Eric Gregory committed
652
public class RenameEventCommand : SimpleProxyableCommand {
653 654 655 656
    private string new_name;
    private string? old_name;
    
    public RenameEventCommand(Event event, string new_name) {
657
        base(event, Resources.RENAME_EVENT_LABEL, "");
658 659 660 661 662
        
        this.new_name = new_name;
        old_name = event.get_raw_name();
    }
    
Eric Gregory's avatar
Eric Gregory committed
663
    public override void execute_on_source(DataSource source) {
664 665 666
        ((Event) source).rename(new_name);
    }
    
Eric Gregory's avatar
Eric Gregory committed
667
    public override void undo_on_source(DataSource source) {
668 669 670 671
        ((Event) source).rename(old_name);
    }
}

672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
public class EditEventCommentCommand : SimpleProxyableCommand {
    private string new_comment;
    private string? old_comment;
    
    public EditEventCommentCommand(Event event, string new_comment) {
        base(event, Resources.EDIT_COMMENT_LABEL, "");
        
        this.new_comment = new_comment;
        old_comment = event.get_comment();
    }
    
    public override void execute_on_source(DataSource source) {
        ((Event) source).set_comment(new_comment);
    }
    
    public override void undo_on_source(DataSource source) {
        ((Event) source).set_comment(old_comment);
    }
}

692
public class SetKeyPhotoCommand : SingleDataSourceCommand {
693 694
    private MediaSource new_primary_source;
    private MediaSource old_primary_source;
695
    
696
    public SetKeyPhotoCommand(Event event, MediaSource new_primary_source) {
697
        base(event, Resources.MAKE_KEY_PHOTO_LABEL, "");
698
        
699 700
        this.new_primary_source = new_primary_source;
        old_primary_source = event.get_primary_source();
701 702 703
    }
    
    public override void execute() {
704
        ((Event) source).set_primary_source(new_primary_source);
705 706 707
    }
    
    public override void undo() {
708
        ((Event) source).set_primary_source(old_primary_source);
709 710 711
    }
}

712
public class RevertSingleCommand : GenericPhotoTransformationCommand {
Jim Nelson's avatar
Jim Nelson committed
713
    public RevertSingleCommand(Photo photo) {
714
        base(photo, Resources.REVERT_LABEL, "");
715 716
    }
    
Jim Nelson's avatar
Jim Nelson committed
717
    public override void execute_on_photo(Photo photo) {
718 719 720 721 722 723 724 725 726 727 728 729 730 731
        photo.remove_all_transformations();
    }
    
    public override bool compress(Command command) {
        RevertSingleCommand revert_single_command = command as RevertSingleCommand;
        if (revert_single_command == null)
            return false;
        
        if (revert_single_command.source != source)
            return false;
        
        // no need to execute anything; multiple successive reverts on the same photo are as good
        // as one
        return true;
732 733 734 735 736
    }
}

public class RevertMultipleCommand : MultiplePhotoTransformationCommand {
    public RevertMultipleCommand(Gee.Iterable<DataView> iter) {
737
        base(iter, _("Reverting"), _("Undoing Revert"), Resources.REVERT_LABEL,
738
            "");
739 740 741
    }
    
    public override void execute_on_source(DataSource source) {
Jim Nelson's avatar
Jim Nelson committed
742
        ((Photo) source).remove_all_transformations();
743 744 745
    }
}

746
public class EnhanceSingleCommand : GenericPhotoTransformationCommand {
Jim Nelson's avatar
Jim Nelson committed
747
    public EnhanceSingleCommand(Photo photo) {
748 749 750
        base(photo, Resources.ENHANCE_LABEL, Resources.ENHANCE_TOOLTIP);
    }
    
Jim Nelson's avatar
Jim Nelson committed
751
    public override void execute_on_photo(Photo photo) {
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778
        AppWindow.get_instance().set_busy_cursor();
#if MEASURE_ENHANCE
        Timer overall_timer = new Timer();
#endif
        
        photo.enhance();
        
#if MEASURE_ENHANCE
        overall_timer.stop();
        debug("Auto-Enhance overall time: %f sec", overall_timer.elapsed());
#endif
        AppWindow.get_instance().set_normal_cursor();
    }
    
    public override bool compress(Command command) {
        EnhanceSingleCommand enhance_single_command = command as EnhanceSingleCommand;
        if (enhance_single_command == null)
            return false;
        
        if (enhance_single_command.source != source)
            return false;
        
        // multiple successive enhances on the same photo are as good as a single
        return true;
    }
}

779 780
public class EnhanceMultipleCommand : MultiplePhotoTransformationCommand {
    public EnhanceMultipleCommand(Gee.Iterable<DataView> iter) {
781
        base(iter, _("Enhancing"), _("Undoing Enhance"), Resources.ENHANCE_LABEL,
782 783 784 785
            Resources.ENHANCE_TOOLTIP);
    }
    
    public override void execute_on_source(DataSource source) {
Jim Nelson's avatar
Jim Nelson committed
786
        ((Photo) source).enhance();
787 788 789
    }
}

790 791
public class StraightenCommand : GenericPhotoTransformationCommand {
    private double theta;
792
    private Box crop;   // straightening can change the crop rectangle
793
    
794
    public StraightenCommand(Photo photo, double theta, Box crop, string name, string explanation) {
795 796 797
        base(photo, name, explanation);
        
        this.theta = theta;
798
        this.crop = crop;
799 800 801
    }
    
    public override void execute_on_photo(Photo photo) {
802 803 804 805 806
        // thaw collection so both alterations are signalled at the same time
        DataCollection? collection = photo.get_membership();
        if (collection != null)
            collection.freeze_notifications();
        
807
        photo.set_straighten(theta);
808
        photo.set_crop(crop);
809 810 811
        
        if (collection != null)
            collection.thaw_notifications();
812 813 814
    }
}

815 816 817
public class CropCommand : GenericPhotoTransformationCommand {
    private Box crop;
    
Jim Nelson's avatar
Jim Nelson committed
818
    public CropCommand(Photo photo, Box crop, string name, string explanation) {
819 820 821 822 823
        base(photo, name, explanation);
        
        this.crop = crop;
    }
    
Jim Nelson's avatar
Jim Nelson committed
824
    public override void execute_on_photo(Photo photo) {
825 826 827 828
        photo.set_crop(crop);
    }
}

829
public class AdjustColorsSingleCommand : GenericPhotoTransformationCommand {
830 831
    private PixelTransformationBundle transformations;
    
832
    public AdjustColorsSingleCommand(Photo photo, PixelTransformationBundle transformations,
833 834 835 836 837 838
        string name, string explanation) {
        base(photo, name, explanation);
        
        this.transformations = transformations;
    }
    
Jim Nelson's avatar
Jim Nelson committed
839
    public override void execute_on_photo(Photo photo) {
840 841 842 843 844 845 846 847
        AppWindow.get_instance().set_busy_cursor();
        
        photo.set_color_adjustments(transformations);
        
        AppWindow.get_instance().set_normal_cursor();
    }
    
    public override bool can_compress(Command command) {
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864
        return command is AdjustColorsSingleCommand;
    }
}

public class AdjustColorsMultipleCommand : MultiplePhotoTransformationCommand {
    private PixelTransformationBundle transformations;
    
    public AdjustColorsMultipleCommand(Gee.Iterable<DataView> iter,
        PixelTransformationBundle transformations, string name, string explanation) {
        base(iter, _("Applying Color Transformations"), _("Undoing Color Transformations"),
            name, explanation);
        
        this.transformations = transformations;
    }
    
    public override void execute_on_source(DataSource source) {
        ((Photo) source).set_color_adjustments(transformations);
865 866 867 868
    }
}

public class RedeyeCommand : GenericPhotoTransformationCommand {
869
    private EditingTools.RedeyeInstance redeye_instance;
870
    
871
    public RedeyeCommand(Photo photo, EditingTools.RedeyeInstance redeye_instance, string name,
872 873 874 875 876 877
        string explanation) {
        base(photo, name, explanation);
        
        this.redeye_instance = redeye_instance;
    }
    
Jim Nelson's avatar
Jim Nelson committed
878
    public override void execute_on_photo(Photo photo) {
879 880 881 882
        photo.add_redeye_instance(redeye_instance);
    }
}

883 884
public abstract class MovePhotosCommand : Command {
    // Piggyback on a private command so that processing to determine new_event can occur before
885
    // construction, if needed
886 887
    protected class RealMovePhotosCommand : MultipleDataSourceCommand {
        private SourceProxy new_event_proxy = null;
888 889
        private Gee.HashMap<MediaSource, SourceProxy?> old_events = new Gee.HashMap<
            MediaSource, SourceProxy?>();
890
        
891
        public RealMovePhotosCommand(Event? new_event, Gee.Iterable<DataView> source_views,
892
            string progress_text, string undo_progress_text, string name, string explanation) {
893
            base(source_views, progress_text, undo_progress_text, name, explanation);
894
            
895
            // get proxies for each media source's event
896
            foreach (DataSource source in source_list) {
897 898
                MediaSource current_media = (MediaSource) source;
                Event? old_event = current_media.get_event();
899 900 901 902
                SourceProxy? old_event_proxy = (old_event != null) ? old_event.get_proxy() : null;
                
                // if any of the proxies break, the show's off
                if (old_event_proxy != null)
903
                    old_event_proxy.broken.connect(on_proxy_broken);
904
                
905
                old_events.set(current_media, old_event_proxy);
906
            }
907
            
908 909
            // stash the proxy of the new event
            new_event_proxy = new_event.get_proxy();
910
            new_event_proxy.broken.connect(on_proxy_broken);
911 912 913
        }
        
        ~RealMovePhotosCommand() {
914
            new_event_proxy.broken.disconnect(on_proxy_broken);
915
            
916
            foreach (SourceProxy? proxy in old_events.values) {
917
                if (proxy != null)
918
                    proxy.broken.disconnect(on_proxy_broken);
919
            }
920 921
        }
        
922
        public override void execute() {
Gert's avatar
Gert committed
923 924 925
            // create the new event
            base.execute();

926
            // Are we at an event page already?
927 928 929 930 931 932 933 934 935
            if ((LibraryWindow.get_app().get_current_page() is EventPage)) {
                Event evt = ((EventPage) LibraryWindow.get_app().get_current_page()).get_event();
                
                // Will moving these empty this event?
                if (evt.get_media_count() == source_list.size) {
                    // Yes - jump away from this event, since it will have zero
                    // entries and is going to be removed.
                    LibraryWindow.get_app().switch_to_event((Event) new_event_proxy.get_source());
                }
936 937
            } else {
                // We're in a library or tag page.
938
                
Gert's avatar
Gert committed
939 940
                // Are we moving these to a newly-created event (i.e. has same size)?
                if (((Event) new_event_proxy.get_source()).get_media_count() == source_list.size) {
941 942 943
                    // Yes - jump to the new event.
                    LibraryWindow.get_app().switch_to_event((Event) new_event_proxy.get_source());
                }
944
            }
945
            // Otherwise - don't jump; users found the jumping disconcerting.
946
        }
947
        
948
        public override void execute_on_source(DataSource source) {
949
            ((MediaSource) source).set_event((Event?) new_event_proxy.get_source());
950
        }
951
        
952
        public override void undo_on_source(DataSource source) {
953 954
            MediaSource current_media = (MediaSource) source;
            SourceProxy? event_proxy = old_events.get(current_media);
955
            
956
            current_media.set_event(event_proxy != null ? (Event?) event_proxy.get_source() : null);
957 958 959 960
        }
        
        private void on_proxy_broken() {
            get_command_manager().reset();
961 962
        }
    }
963 964

    protected RealMovePhotosCommand real_command;
965
    
966
    protected MovePhotosCommand(string name, string explanation) {
967
        base(name, explanation);
968 969
    }
    
970 971 972
    public override void prepare() {
        assert(real_command != null);
        real_command.prepare();
973 974
    }
    
975 976 977
    public override void execute() {
        assert(real_command != null);
        real_command.execute();
978 979
    }
    
980 981 982
    public override void undo() {
        assert(real_command != null);
        real_command.undo();
983 984 985
    }
}

986 987
public class NewEventCommand : MovePhotosCommand {
    public NewEventCommand(Gee.Iterable<DataView> iter) {
988
        base(Resources.NEW_EVENT_LABEL, "");
989

990 991
        // get the primary or "key" source for the new event (which is simply the first one)
        MediaSource key_source = null;
992
        foreach (DataView view in iter) {
993
            MediaSource current_source = (MediaSource) view.get_source();
994
            
995 996
            if (key_source == null) {
                key_source = current_source;
997
                break;
998 999 1000
            }
        }
        
1001
        // key photo is required for an event
1002
        assert(key_source != null);
1003

1004
        Event new_event = Event.create_empty_event(key_source);
1005

1006 1007
        real_command = new RealMovePhotosCommand(new_event, iter, _("Creating New Event"),
            _("Removing Event"), Resources.NEW_EVENT_LABEL,
1008
            "");
1009
    }
1010 1011
}

1012
public class SetEventCommand : MovePhotosCommand {
1013 1014 1015
    public SetEventCommand(Gee.Iterable<DataView> iter, Event new_event) {
        base(Resources.SET_PHOTO_EVENT_LABEL, Resources.SET_PHOTO_EVENT_TOOLTIP);

1016 1017
        real_command = new RealMovePhotosCommand(new_event, iter, _("Moving Photos to New Event"),
            _("Setting Photos to Previous Event"), Resources.SET_PHOTO_EVENT_LABEL, 
1018
            "");
1019 1020 1021 1022
    }
}

public class MergeEventsCommand : MovePhotosCommand {
1023
    public MergeEventsCommand(Gee.Iterable<DataView> iter) {
1024
        base (Resources.MERGE_LABEL, "");
1025
        
1026 1027 1028 1029
        // Because it requires fewer operations to merge small events onto large ones,
        // rather than the other way round, we try to choose the event with the most
        // sources as the 'master', preferring named events over unnamed ones so that
        // names can persist.
1030
        Event master_event = null;
1031 1032
        int named_evt_src_count = 0;
        int unnamed_evt_src_count = 0;
1033
        Gee.ArrayList<ThumbnailView> media_thumbs = new Gee.ArrayList<ThumbnailView>();
1034 1035 1036 1037
        
        foreach (DataView view in iter) {
            Event event = (Event) view.get_source();
            
1038 1039 1040 1041
            // First event we've examined?
            if (master_event == null) {
                // Yes. Make it the master for now and remember it as
                // having the most sources (out of what we've seen so far).
1042
                master_event = event;
1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
                unnamed_evt_src_count = master_event.get_media_count();
                if (event.has_name())
                    named_evt_src_count = master_event.get_media_count();
            } else {
                // No. Check whether this event has a name and whether
                // it has more sources than any other we've seen...
                if (event.has_name()) {
                    if (event.get_media_count() > named_evt_src_count) {
                        named_evt_src_count = event.get_media_count();
                        master_event = event;
                    }
                } else if (named_evt_src_count == 0) {
                    // Per the original app design, named events -always- trump
                    // unnamed ones, so only choose an unnamed one if we haven't
                    // seen any named ones yet.
                    if (event.get_media_count() > unnamed_evt_src_count) {
                        unnamed_evt_src_count = event.get_media_count();
                        master_event = event;
                    }
                }
            }
1064
            
1065
            // store all media sources in this operation; they will be moved to the master event
1066
            // (keep proxies of their original event for undo)
1067 1068
            foreach (MediaSource media_source in event.get_media())
                media_thumbs.add(new ThumbnailView(media_source));
1069 1070 1071
        }
        
        assert(master_event != null);
1072
        assert(media_thumbs.size > 0);
1073
        
1074
        real_command = new RealMovePhotosCommand(master_event, media_thumbs, _("Merging"), 
1075
            _("Unmerging"), Resources.MERGE_LABEL, "");
1076 1077 1078
    }
}

1079 1080 1081 1082 1083
public class DuplicateMultiplePhotosCommand : MultipleDataSourceCommand {
    private Gee.HashMap<LibraryPhoto, LibraryPhoto> dupes = new Gee.HashMap<LibraryPhoto, LibraryPhoto>();
    private int failed = 0;
    
    public DuplicateMultiplePhotosCommand(Gee.Iterable<DataView> iter) {
1084
        base (iter, _("Duplicating photos"), _("Removing duplicated photos"), 
1085
            Resources.DUPLICATE_PHOTO_LABEL, Resources.DUPLICATE_PHOTO_TOOLTIP);
1086
        
1087
        LibraryPhoto.global.item_destroyed.connect(on_photo_destroyed);
1088 1089 1090
    }
    
    ~DuplicateMultiplePhotosCommand() {
1091
        LibraryPhoto.global.item_destroyed.disconnect(on_photo_destroyed);
1092 1093 1094 1095 1096 1097
    }
    
    private void on_photo_destroyed(DataSource source) {
        // if one of the duplicates is destroyed, can no longer undo it (which destroys it again)
        if (dupes.values.contains((LibraryPhoto) source))
            get_command_manager().reset();
1098 1099 1100 1101 1102 1103 1104 1105
    }
    
    public override void execute() {
        dupes.clear();
        failed = 0;
        
        base.execute();
        
1106 1107 1108 1109 1110
        if (failed > 0) {
            string error_string = (ngettext("Unable to duplicate one photo due to a file error",
                "Unable to duplicate %d photos due to file errors", failed)).printf(failed);
            AppWindow.error_message(error_string);
        }
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
    }
    
    public override void execute_on_source(DataSource source) {
        LibraryPhoto photo = (LibraryPhoto) source;
        
        try {
            LibraryPhoto dupe = photo.duplicate();
            dupes.set(photo, dupe);
        } catch (Error err) {
            critical("Unable to duplicate file %s: %s", photo.get_file().get_path(), err.message);
            failed++;
        }
    }
    
    public override void undo() {
1126
        // disconnect from monitoring the duplicates' destruction, as undo() does exactly that
1127
        LibraryPhoto.global.item_destroyed.disconnect(on_photo_destroyed);
1128
        
1129 1130 1131 1132 1133
        base.undo();
        
        // be sure to drop everything that was destroyed
        dupes.clear();
        failed = 0;
1134 1135
        
        // re-monitor for duplicates' destruction
1136
        LibraryPhoto.global.item_destroyed.connect(on_photo_destroyed);
1137 1138 1139 1140 1141
    }
    
    public override void undo_on_source(DataSource source) {
        LibraryPhoto photo = (LibraryPhoto) source;
        
1142 1143
        Marker marker = LibraryPhoto.global.mark(dupes.get(photo));
        LibraryPhoto.global.destroy_marked(marker, true);
1144 1145
    }
}
1146

1147 1148 1149 1150 1151 1152 1153
public class SetRatingSingleCommand : SingleDataSourceCommand {
    private Rating last_rating;
    private Rating new_rating;
    private bool set_direct;
    private bool incrementing;

    public SetRatingSingleCommand(DataSource source, Rating rating) {
1154
        base (source, Resources.rating_label(rating), "");
1155 1156 1157 1158 1159 1160 1161 1162
        set_direct = true;
        new_rating = rating;

        last_rating = ((LibraryPhoto)source).get_rating();
    }

    public SetRatingSingleCommand.inc_dec(DataSource source, bool is_incrementing) {
        base (source, is_incrementing ? Resources.INCREASE_RATING_LABEL : 
1163
            Resources.DECREASE_RATING_LABEL, "");
1164 1165 1166
        set_direct = false;
        incrementing = is_incrementing;

1167
        last_rating = ((MediaSource) source).get_rating();
1168 1169 1170 1171
    }

    public override void execute() {
        if (set_direct)
1172
            ((MediaSource) source).set_rating(new_rating);
1173 1174
        else {
            if (incrementing) 
1175
                ((MediaSource) source).increase_rating();
1176
            else
1177
                ((MediaSource) source).decrease_rating();
1178 1179 1180 1181
        }
    }
    
    public override void undo() {
1182
        ((MediaSource) source).set_rating(last_rating);
1183 1184 1185 1186 1187 1188 1189 1190
    }
}

public class SetRatingCommand : MultipleDataSourceCommand {
    private Gee.HashMap<DataSource, Rating> last_rating_map;
    private Rating new_rating;
    private bool set_direct;
    private bool incrementing;
1191
    private int action_count = 0;
1192 1193 1194

    public SetRatingCommand(Gee.Iterable<DataView> iter, Rating rating) {
        base (iter, Resources.rating_progress(rating), _("Restoring previous rating"),
1195
            Resources.rating_label(rating), "");
1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206
        set_direct = true;
        new_rating = rating;

        save_source_states(iter);
    } 
    
    public SetRatingCommand.inc_dec(Gee.Iterable<DataView> iter, bool is_incrementing) {
        base (iter, 
            is_incrementing ? _("Increasing ratings") : _("Decreasing ratings"),
            is_incrementing ? _("Decreasing ratings") : _("Increasing ratings"), 
            is_incrementing ? Resources.INCREASE_RATING_LABEL : Resources.DECREASE_RATING_LABEL, 
1207
            "");
1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218
        set_direct = false;
        incrementing = is_incrementing;
        
        save_source_states(iter);
    }
    
    private void save_source_states(Gee.Iterable<DataView> iter) {
        last_rating_map = new Gee.HashMap<DataSource, Rating>();

        foreach (DataView view in iter) {
            DataSource source = view.get_source();
1219
            last_rating_map[source] = ((MediaSource) source).get_rating();
1220 1221 1222
        }
    }
    
1223
    public override void execute() {
1224
        action_count = 0;
1225 1226 1227 1228
        base.execute();
    }
    
    public override void undo() {
1229
        action_count = 0;
1230 1231 1232
        base.undo();
    }
    
1233 1234
    public override void execute_on_source(DataSource source) {
        if (set_direct)
1235
            ((MediaSource) source).set_rating(new_rating);
1236 1237
        else {
            if (incrementing)
1238
                ((MediaSource) source).increase_rating();
1239
            else
1240
                ((MediaSource) source).decrease_rating();
1241 1242 1243 1244
        }
    }
    
    public override void undo_on_source(DataSource source) {
1245
        ((MediaSource) source).set_rating(last_rating_map[source]);
1246 1247 1248
    }
}

1249 1250
public class SetRawDeveloperCommand : MultipleDataSourceCommand {
    private Gee.HashMap<Photo, RawDeveloper> last_developer_map;
1251
    private Gee.HashMap<Photo, PhotoTransformationState> last_transformation_map;
1252 1253 1254 1255
    private RawDeveloper new_developer;

    public SetRawDeveloperCommand(Gee.Iterable<DataView> iter, RawDeveloper developer) {
        base (iter, _("Setting RAW developer"), _("Restoring previous RAW developer"),
1256
            _("Set Developer"), "");
1257 1258 1259 1260 1261 1262
        new_developer = developer;
        save_source_states(iter);
    }
    
    private void save_source_states(Gee.Iterable<DataView> iter) {
        last_developer_map = new Gee.HashMap<Photo, RawDeveloper>();
1263
        last_transformation_map = new Gee.HashMap<Photo, PhotoTransformationState>();
1264 1265 1266
        
        foreach (DataView view in iter) {
            Photo? photo = view.get_source() as Photo;
1267
            if (is_raw_photo(photo)) {
1268
                last_developer_map[photo] = photo.get_raw_developer();
1269 1270
                last_transformation_map[photo] = photo.save_transformation_state();
            }
1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293
        }
    }
    
    public override void execute() {
        base.execute();
    }
    
    public override void undo() {
        base.undo();
    }
    
    public override void execute_on_source(DataSource source) {
        Photo? photo = source as Photo;
        if (is_raw_photo(photo)) {
            if (new_developer == RawDeveloper.CAMERA && !photo.is_raw_developer_available(RawDeveloper.CAMERA))
                photo.set_raw_developer(RawDeveloper.EMBEDDED);
            else
                photo.set_raw_developer(new_developer);
        }
    }
    
    public override void undo_on_source(DataSource source) {
        Photo? photo = source as Photo;
1294
        if (is_raw_photo(photo)) {
1295
            photo.set_raw_developer(last_developer_map[photo]);
1296 1297
            photo.load_transformation_state(last_transformation_map[photo]);
        }
1298 1299 1300 1301 1302 1303 1304
    }
    
    private bool is_raw_photo(Photo? photo) {
        return photo != null && photo.get_master_file_format() == PhotoFileFormat.RAW;
    }
}

1305
public class AdjustDateTimePhotoCommand : SingleDataSourceCommand {
1306
    private Dateable dateable;
1307
    private Event? prev_event;
1308 1309 1310
    private int64 time_shift;
    private bool modify_original;

1311
    public AdjustDateTimePhotoCommand(Dateable dateable, int64 time_shift, bool modify_original) {
1312
        base(dateable, Resources.ADJUST_DATE_TIME_LABEL, "");
1313

1314
        this.dateable = dateable;