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 7public class MediaSourceItem : CheckerboardItem { 8 private string? natural_collation_key = null; 9 10 // preserve the same constructor arguments and semantics as CheckerboardItem so that we're 11 // a drop-in replacement 12 public MediaSourceItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title, 13 string? comment, bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) { 14 base(source, initial_pixbuf_dim, title, comment, marked_up, alignment); 15 } 16 17 public new void set_title(string text, bool marked_up = false, 18 Pango.Alignment alignment = Pango.Alignment.LEFT) { 19 base.set_title(text, marked_up, alignment); 20 this.natural_collation_key = null; 21 } 22 23 public string get_natural_collation_key() { 24 if (this.natural_collation_key == null) { 25 this.natural_collation_key = NaturalCollate.collate_key(this.get_title()); 26 } 27 return this.natural_collation_key; 28 } 29} 30 31public abstract class MediaPage : CheckerboardPage { 32 public const int SORT_ORDER_ASCENDING = 0; 33 public const int SORT_ORDER_DESCENDING = 1; 34 35 // steppings should divide evenly into (Thumbnail.MAX_SCALE - Thumbnail.MIN_SCALE) 36 public const int MANUAL_STEPPING = 16; 37 public const int SLIDER_STEPPING = 4; 38 39 public enum SortBy { 40 MIN = 1, 41 TITLE = 1, 42 EXPOSURE_DATE = 2, 43 RATING = 3, 44 FILENAME = 4, 45 MAX = 4 46 } 47 48 protected class ZoomSliderAssembly : Gtk.ToolItem { 49 private Gtk.Scale slider; 50 private Gtk.Adjustment adjustment; 51 52 public signal void zoom_changed(); 53 54 public ZoomSliderAssembly() { 55 Gtk.Box zoom_group = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0); 56 57 Gtk.Image zoom_out = new Gtk.Image.from_icon_name("image-zoom-out-symbolic", Gtk.IconSize.SMALL_TOOLBAR); 58 Gtk.EventBox zoom_out_box = new Gtk.EventBox(); 59 zoom_out_box.set_above_child(true); 60 zoom_out_box.set_visible_window(false); 61 zoom_out_box.add(zoom_out); 62 zoom_out_box.button_press_event.connect(on_zoom_out_pressed); 63 64 zoom_group.pack_start(zoom_out_box, false, false, 0); 65 66 // virgin ZoomSliderAssemblies are created such that they have whatever value is 67 // persisted in the configuration system for the photo thumbnail scale 68 int persisted_scale = Config.Facade.get_instance().get_photo_thumbnail_scale(); 69 adjustment = new Gtk.Adjustment(ZoomSliderAssembly.scale_to_slider(persisted_scale), 0, 70 ZoomSliderAssembly.scale_to_slider(Thumbnail.MAX_SCALE), 1, 10, 0); 71 72 slider = new Gtk.Scale(Gtk.Orientation.HORIZONTAL, adjustment); 73 slider.value_changed.connect(on_slider_changed); 74 slider.set_draw_value(false); 75 slider.set_size_request(200, -1); 76 slider.set_tooltip_text(_("Adjust the size of the thumbnails")); 77 78 zoom_group.pack_start(slider, false, false, 0); 79 80 Gtk.Image zoom_in = new Gtk.Image.from_icon_name("image-zoom-in-symbolic", Gtk.IconSize.SMALL_TOOLBAR); 81 Gtk.EventBox zoom_in_box = new Gtk.EventBox(); 82 zoom_in_box.set_above_child(true); 83 zoom_in_box.set_visible_window(false); 84 zoom_in_box.add(zoom_in); 85 zoom_in_box.button_press_event.connect(on_zoom_in_pressed); 86 87 zoom_group.pack_start(zoom_in_box, false, false, 0); 88 89 add(zoom_group); 90 } 91 92 public static double scale_to_slider(int value) { 93 assert(value >= Thumbnail.MIN_SCALE); 94 assert(value <= Thumbnail.MAX_SCALE); 95 96 return (double) ((value - Thumbnail.MIN_SCALE) / SLIDER_STEPPING); 97 } 98 99 public static int slider_to_scale(double value) { 100 int res = ((int) (value * SLIDER_STEPPING)) + Thumbnail.MIN_SCALE; 101 102 assert(res >= Thumbnail.MIN_SCALE); 103 assert(res <= Thumbnail.MAX_SCALE); 104 105 return res; 106 } 107 108 private bool on_zoom_out_pressed(Gdk.EventButton event) { 109 snap_to_min(); 110 return true; 111 } 112 113 private bool on_zoom_in_pressed(Gdk.EventButton event) { 114 snap_to_max(); 115 return true; 116 } 117 118 private void on_slider_changed() { 119 zoom_changed(); 120 } 121 122 public void snap_to_min() { 123 slider.set_value(scale_to_slider(Thumbnail.MIN_SCALE)); 124 } 125 126 public void snap_to_max() { 127 slider.set_value(scale_to_slider(Thumbnail.MAX_SCALE)); 128 } 129 130 public void increase_step() { 131 int new_scale = compute_zoom_scale_increase(get_scale()); 132 133 if (get_scale() == new_scale) 134 return; 135 136 slider.set_value(scale_to_slider(new_scale)); 137 } 138 139 public void decrease_step() { 140 int new_scale = compute_zoom_scale_decrease(get_scale()); 141 142 if (get_scale() == new_scale) 143 return; 144 145 slider.set_value(scale_to_slider(new_scale)); 146 } 147 148 public int get_scale() { 149 return slider_to_scale(slider.get_value()); 150 } 151 152 public void set_scale(int scale) { 153 if (get_scale() == scale) 154 return; 155 156 slider.set_value(scale_to_slider(scale)); 157 } 158 } 159 160 private ZoomSliderAssembly? connected_slider = null; 161 private DragAndDropHandler dnd_handler = null; 162 private MediaViewTracker tracker; 163 164 protected MediaPage(string page_name) { 165 base (page_name); 166 167 tracker = new MediaViewTracker(get_view()); 168 169 get_view().items_altered.connect(on_media_altered); 170 171 get_view().freeze_notifications(); 172 get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, 173 Config.Facade.get_instance().get_display_photo_titles()); 174 get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, 175 Config.Facade.get_instance().get_display_photo_comments()); 176 get_view().set_property(Thumbnail.PROP_SHOW_TAGS, 177 Config.Facade.get_instance().get_display_photo_tags()); 178 get_view().set_property(Thumbnail.PROP_SIZE, get_thumb_size()); 179 get_view().set_property(Thumbnail.PROP_SHOW_RATINGS, 180 Config.Facade.get_instance().get_display_photo_ratings()); 181 get_view().thaw_notifications(); 182 183 // enable drag-and-drop export of media 184 dnd_handler = new DragAndDropHandler(this); 185 } 186 187 private static int compute_zoom_scale_increase(int current_scale) { 188 int new_scale = current_scale + MANUAL_STEPPING; 189 return new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE); 190 } 191 192 private static int compute_zoom_scale_decrease(int current_scale) { 193 int new_scale = current_scale - MANUAL_STEPPING; 194 return new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE); 195 } 196 197 protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) { 198 base.init_collect_ui_filenames(ui_filenames); 199 200 ui_filenames.add("media.ui"); 201 } 202 203 private const GLib.ActionEntry[] entries = { 204 { "Export", on_export }, 205 { "SendTo", on_send_to }, 206 { "SendToContextMenu", on_send_to }, 207 { "RemoveFromLibrary", on_remove_from_library }, 208 { "MoveToTrash", on_move_to_trash }, 209 { "NewEvent", on_new_event }, 210 { "AddTags", on_add_tags }, 211 { "ModifyTags", on_modify_tags }, 212 { "IncreaseSize", on_increase_size }, 213 { "DecreaseSize", on_decrease_size }, 214 { "Flag", on_flag_unflag }, 215 { "IncreaseRating", on_increase_rating }, 216 { "DecreaseRating", on_decrease_rating }, 217 { "RateRejected", on_rate_rejected }, 218 { "RateUnrated", on_rate_unrated }, 219 { "RateOne", on_rate_one }, 220 { "RateTwo", on_rate_two }, 221 { "RateThree", on_rate_three }, 222 { "RateFour", on_rate_four }, 223 { "RateFive", on_rate_five }, 224 { "EditTitle", on_edit_title }, 225 { "EditComment", on_edit_comment }, 226 { "PlayVideo", on_play_video }, 227 228 // Toggle actions 229 { "ViewTitle", on_action_toggle, null, "false", on_display_titles }, 230 { "ViewComment", on_action_toggle, null, "false", on_display_comments }, 231 { "ViewRatings", on_action_toggle, null, "false", on_display_ratings }, 232 { "ViewTags", on_action_toggle, null, "false", on_display_tags }, 233 234 // Radio actions 235 { "SortBy", on_action_radio, "s", "'1'", on_sort_changed }, 236 { "Sort", on_action_radio, "s", "'ascending'", on_sort_changed }, 237 }; 238 239 protected override void add_actions (GLib.ActionMap map) { 240 base.add_actions (map); 241 242 bool sort_order; 243 int sort_by; 244 get_config_photos_sort(out sort_order, out sort_by); 245 246 map.add_action_entries (entries, this); 247 get_action ("ViewTitle").change_state (Config.Facade.get_instance ().get_display_photo_titles ()); 248 get_action ("ViewComment").change_state (Config.Facade.get_instance ().get_display_photo_comments ()); 249 get_action ("ViewRatings").change_state (Config.Facade.get_instance ().get_display_photo_ratings ()); 250 get_action ("ViewTags").change_state (Config.Facade.get_instance ().get_display_photo_tags ()); 251 get_action ("SortBy").change_state ("%d".printf (sort_by)); 252 get_action ("Sort").change_state (sort_order ? "ascending" : "descending"); 253 254 var d = Config.Facade.get_instance().get_default_raw_developer(); 255 var action = new GLib.SimpleAction.stateful("RawDeveloper", 256 GLib.VariantType.STRING, d == RawDeveloper.SHOTWELL ? "Shotwell" : "Camera"); 257 action.change_state.connect(on_raw_developer_changed); 258 action.set_enabled(true); 259 map.add_action(action); 260 } 261 262 protected override void remove_actions(GLib.ActionMap map) { 263 base.remove_actions(map); 264 foreach (var entry in entries) { 265 map.remove_action(entry.name); 266 } 267 } 268 269 protected override void update_actions(int selected_count, int count) { 270 set_action_sensitive("Export", selected_count > 0); 271 set_action_sensitive("EditTitle", selected_count > 0); 272 set_action_sensitive("EditComment", selected_count > 0); 273 set_action_sensitive("IncreaseSize", get_thumb_size() < Thumbnail.MAX_SCALE); 274 set_action_sensitive("DecreaseSize", get_thumb_size() > Thumbnail.MIN_SCALE); 275 set_action_sensitive("RemoveFromLibrary", selected_count > 0); 276 set_action_sensitive("MoveToTrash", selected_count > 0); 277 278 if (DesktopIntegration.is_send_to_installed()) 279 set_action_sensitive("SendTo", selected_count > 0); 280 else 281 set_action_sensitive("SendTo", false); 282 283 set_action_sensitive("Rate", selected_count > 0); 284 update_rating_sensitivities(); 285 286 update_development_menu_item_sensitivity(); 287 288 set_action_sensitive("PlayVideo", selected_count == 1 289 && get_view().get_selected_source_at(0) is Video); 290 291 update_flag_action(selected_count); 292 293 base.update_actions(selected_count, count); 294 } 295 296 private void on_media_altered(Gee.Map<DataObject, Alteration> altered) { 297 foreach (DataObject object in altered.keys) { 298 if (altered.get(object).has_detail("metadata", "flagged")) { 299 update_flag_action(get_view().get_selected_count()); 300 301 break; 302 } 303 } 304 } 305 306 private void update_rating_sensitivities() { 307 set_action_sensitive("RateRejected", can_rate_selected(Rating.REJECTED)); 308 set_action_sensitive("RateUnrated", can_rate_selected(Rating.UNRATED)); 309 set_action_sensitive("RateOne", can_rate_selected(Rating.ONE)); 310 set_action_sensitive("RateTwo", can_rate_selected(Rating.TWO)); 311 set_action_sensitive("RateThree", can_rate_selected(Rating.THREE)); 312 set_action_sensitive("RateFour", can_rate_selected(Rating.FOUR)); 313 set_action_sensitive("RateFive", can_rate_selected(Rating.FIVE)); 314 set_action_sensitive("IncreaseRating", can_increase_selected_rating()); 315 set_action_sensitive("DecreaseRating", can_decrease_selected_rating()); 316 } 317 318 private void update_development_menu_item_sensitivity() { 319 if (get_view().get_selected().size == 0) { 320 set_action_sensitive("RawDeveloper", false); 321 return; 322 } 323 324 // Collect some stats about what's selected. 325 bool is_raw = false; // True if any RAW photos are selected 326 foreach (DataView view in get_view().get_selected()) { 327 Photo? photo = ((Thumbnail) view).get_media_source() as Photo; 328 if (photo != null && photo.get_master_file_format() == PhotoFileFormat.RAW) { 329 is_raw = true; 330 331 break; 332 } 333 } 334 335 // Enable/disable menu. 336 set_action_sensitive("RawDeveloper", is_raw); 337 } 338 339 private void update_flag_action(int selected_count) { 340 set_action_sensitive("Flag", selected_count > 0); 341 } 342 343 public override Core.ViewTracker? get_view_tracker() { 344 return tracker; 345 } 346 347 public void set_display_ratings(bool display) { 348 get_view().freeze_notifications(); 349 get_view().set_property(Thumbnail.PROP_SHOW_RATINGS, display); 350 get_view().thaw_notifications(); 351 352 this.set_action_active ("ViewRatings", display); 353 } 354 355 private bool can_rate_selected(Rating rating) { 356 foreach (DataView view in get_view().get_selected()) { 357 if(((Thumbnail) view).get_media_source().get_rating() != rating) 358 return true; 359 } 360 361 return false; 362 } 363 364 private bool can_increase_selected_rating() { 365 foreach (DataView view in get_view().get_selected()) { 366 if(((Thumbnail) view).get_media_source().get_rating().can_increase()) 367 return true; 368 } 369 370 return false; 371 } 372 373 private bool can_decrease_selected_rating() { 374 foreach (DataView view in get_view().get_selected()) { 375 if(((Thumbnail) view).get_media_source().get_rating().can_decrease()) 376 return true; 377 } 378 379 return false; 380 } 381 382 public ZoomSliderAssembly create_zoom_slider_assembly() { 383 return new ZoomSliderAssembly(); 384 } 385 386 protected override bool on_mousewheel_up(Gdk.EventScroll event) { 387 if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) { 388 increase_zoom_level(); 389 return true; 390 } else { 391 return base.on_mousewheel_up(event); 392 } 393 } 394 395 protected override bool on_mousewheel_down(Gdk.EventScroll event) { 396 if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) { 397 decrease_zoom_level(); 398 return true; 399 } else { 400 return base.on_mousewheel_down(event); 401 } 402 } 403 404 private void on_send_to() { 405 DesktopIntegration.send_to((Gee.Collection<MediaSource>) get_view().get_selected_sources()); 406 } 407 408 protected void on_play_video() { 409 if (get_view().get_selected_count() != 1) 410 return; 411 412 Video? video = get_view().get_selected_at(0).get_source() as Video; 413 if (video == null) 414 return; 415 416 try { 417 AppInfo.launch_default_for_uri(video.get_file().get_uri(), null); 418 } catch (Error e) { 419 AppWindow.error_message(_("Shotwell was unable to play the selected video:\n%s").printf( 420 e.message)); 421 } 422 } 423 424 protected override bool on_app_key_pressed(Gdk.EventKey event) { 425 bool handled = true; 426 switch (Gdk.keyval_name(event.keyval)) { 427 case "equal": 428 case "plus": 429 case "KP_Add": 430 activate_action("IncreaseSize"); 431 break; 432 433 case "minus": 434 case "underscore": 435 case "KP_Subtract": 436 activate_action("DecreaseSize"); 437 break; 438 439 case "period": 440 activate_action("IncreaseRating"); 441 break; 442 443 case "comma": 444 activate_action("DecreaseRating"); 445 break; 446 447 case "KP_1": 448 activate_action("RateOne"); 449 break; 450 451 case "KP_2": 452 activate_action("RateTwo"); 453 break; 454 455 case "KP_3": 456 activate_action("RateThree"); 457 break; 458 459 case "KP_4": 460 activate_action("RateFour"); 461 break; 462 463 case "KP_5": 464 activate_action("RateFive"); 465 break; 466 467 case "KP_0": 468 activate_action("RateUnrated"); 469 break; 470 471 case "KP_9": 472 activate_action("RateRejected"); 473 break; 474 475 case "slash": 476 activate_action("Flag"); 477 break; 478 479 default: 480 handled = false; 481 break; 482 } 483 484 return handled ? true : base.on_app_key_pressed(event); 485 } 486 487 public override void switched_to() { 488 base.switched_to(); 489 490 // set display options to match Configuration toggles (which can change while switched away) 491 get_view().freeze_notifications(); 492 set_display_titles(Config.Facade.get_instance().get_display_photo_titles()); 493 set_display_comments(Config.Facade.get_instance().get_display_photo_comments()); 494 set_display_ratings(Config.Facade.get_instance().get_display_photo_ratings()); 495 set_display_tags(Config.Facade.get_instance().get_display_photo_tags()); 496 get_view().thaw_notifications(); 497 498 // Update cursor position to match the selection that potentially moved while the user 499 // navigated in SinglePhotoPage 500 if (get_view().get_selected_count() > 0) { 501 CheckerboardItem? selected = (CheckerboardItem?) get_view().get_selected_at(0); 502 if (selected != null) 503 cursor_to_item(selected); 504 } 505 506 sync_sort(); 507 } 508 509 public override void switching_from() { 510 disconnect_slider(); 511 512 base.switching_from(); 513 } 514 515 protected void connect_slider(ZoomSliderAssembly slider) { 516 connected_slider = slider; 517 connected_slider.zoom_changed.connect(on_zoom_changed); 518 load_persistent_thumbnail_scale(); 519 } 520 521 private void save_persistent_thumbnail_scale() { 522 if (connected_slider == null) 523 return; 524 525 Config.Facade.get_instance().set_photo_thumbnail_scale(connected_slider.get_scale()); 526 } 527 528 private void load_persistent_thumbnail_scale() { 529 if (connected_slider == null) 530 return; 531 532 int persistent_scale = Config.Facade.get_instance().get_photo_thumbnail_scale(); 533 534 connected_slider.set_scale(persistent_scale); 535 set_thumb_size(persistent_scale); 536 } 537 538 protected void disconnect_slider() { 539 if (connected_slider == null) 540 return; 541 542 connected_slider.zoom_changed.disconnect(on_zoom_changed); 543 connected_slider = null; 544 } 545 546 protected virtual void on_zoom_changed() { 547 if (connected_slider != null) 548 set_thumb_size(connected_slider.get_scale()); 549 550 save_persistent_thumbnail_scale(); 551 } 552 553 protected abstract void on_export(); 554 555 protected virtual void on_increase_size() { 556 increase_zoom_level(); 557 } 558 559 protected virtual void on_decrease_size() { 560 decrease_zoom_level(); 561 } 562 563 private void on_add_tags() { 564 if (get_view().get_selected_count() == 0) 565 return; 566 567 AddTagsDialog dialog = new AddTagsDialog(); 568 string[]? names = dialog.execute(); 569 570 if (names != null) { 571 get_command_manager().execute(new AddTagsCommand( 572 HierarchicalTagIndex.get_global_index().get_paths_for_names_array(names), 573 (Gee.Collection<MediaSource>) get_view().get_selected_sources())); 574 } 575 } 576 577 private void on_modify_tags() { 578 if (get_view().get_selected_count() != 1) 579 return; 580 581 MediaSource media = (MediaSource) get_view().get_selected_at(0).get_source(); 582 583 ModifyTagsDialog dialog = new ModifyTagsDialog(media); 584 Gee.ArrayList<Tag>? new_tags = dialog.execute(); 585 586 if (new_tags == null) 587 return; 588 589 get_command_manager().execute(new ModifyTagsCommand(media, new_tags)); 590 } 591 592 private void set_display_tags(bool display) { 593 get_view().freeze_notifications(); 594 get_view().set_property(Thumbnail.PROP_SHOW_TAGS, display); 595 get_view().thaw_notifications(); 596 597 this.set_action_active ("ViewTags", display); 598 } 599 600 private void on_new_event() { 601 if (get_view().get_selected_count() > 0) 602 get_command_manager().execute(new NewEventCommand(get_view().get_selected())); 603 } 604 605 private void on_flag_unflag() { 606 if (get_view().get_selected_count() == 0) 607 return; 608 609 Gee.Collection<MediaSource> sources = 610 (Gee.Collection<MediaSource>) get_view().get_selected_sources_of_type(typeof(MediaSource)); 611 612 // If all are flagged, then unflag, otherwise flag 613 bool flag = false; 614 foreach (MediaSource source in sources) { 615 Flaggable? flaggable = source as Flaggable; 616 if (flaggable != null && !flaggable.is_flagged()) { 617 flag = true; 618 619 break; 620 } 621 } 622 623 get_command_manager().execute(new FlagUnflagCommand(sources, flag)); 624 } 625 626 protected virtual void on_increase_rating() { 627 if (get_view().get_selected_count() == 0) 628 return; 629 630 SetRatingCommand command = new SetRatingCommand.inc_dec(get_view().get_selected(), true); 631 get_command_manager().execute(command); 632 633 update_rating_sensitivities(); 634 } 635 636 protected virtual void on_decrease_rating() { 637 if (get_view().get_selected_count() == 0) 638 return; 639 640 SetRatingCommand command = new SetRatingCommand.inc_dec(get_view().get_selected(), false); 641 get_command_manager().execute(command); 642 643 update_rating_sensitivities(); 644 } 645 646 protected virtual void on_set_rating(Rating rating) { 647 if (get_view().get_selected_count() == 0) 648 return; 649 650 SetRatingCommand command = new SetRatingCommand(get_view().get_selected(), rating); 651 get_command_manager().execute(command); 652 653 update_rating_sensitivities(); 654 } 655 656 protected virtual void on_rate_rejected() { 657 on_set_rating(Rating.REJECTED); 658 } 659 660 protected virtual void on_rate_unrated() { 661 on_set_rating(Rating.UNRATED); 662 } 663 664 protected virtual void on_rate_one() { 665 on_set_rating(Rating.ONE); 666 } 667 668 protected virtual void on_rate_two() { 669 on_set_rating(Rating.TWO); 670 } 671 672 protected virtual void on_rate_three() { 673 on_set_rating(Rating.THREE); 674 } 675 676 protected virtual void on_rate_four() { 677 on_set_rating(Rating.FOUR); 678 } 679 680 protected virtual void on_rate_five() { 681 on_set_rating(Rating.FIVE); 682 } 683 684 private void on_remove_from_library() { 685 remove_photos_from_library((Gee.Collection<LibraryPhoto>) get_view().get_selected_sources()); 686 } 687 688 protected virtual void on_move_to_trash() { 689 CheckerboardItem? restore_point = null; 690 691 if (cursor != null) { 692 restore_point = get_view().get_next(cursor) as CheckerboardItem; 693 } 694 695 var sources = get_view().get_selected_sources(); 696 697 if ((restore_point != null) && (get_view().contains(restore_point))) { 698 set_cursor(restore_point); 699 } 700 701 if (get_view().get_selected_count() > 0) { 702 get_command_manager().execute(new TrashUntrashPhotosCommand( 703 (Gee.Collection<MediaSource>) sources, true)); 704 } 705 706 } 707 708 protected virtual void on_edit_title() { 709 if (get_view().get_selected_count() == 0) 710 return; 711 712 Gee.List<MediaSource> media_sources = (Gee.List<MediaSource>) get_view().get_selected_sources(); 713 714 EditTitleDialog edit_title_dialog = new EditTitleDialog(media_sources[0].get_title()); 715 string? new_title = edit_title_dialog.execute(); 716 if (new_title != null) 717 get_command_manager().execute(new EditMultipleTitlesCommand(media_sources, new_title)); 718 } 719 720 protected virtual void on_edit_comment() { 721 if (get_view().get_selected_count() == 0) 722 return; 723 724 Gee.List<MediaSource> media_sources = (Gee.List<MediaSource>) get_view().get_selected_sources(); 725 726 EditCommentDialog edit_comment_dialog = new EditCommentDialog(media_sources[0].get_comment()); 727 string? new_comment = edit_comment_dialog.execute(); 728 if (new_comment != null) 729 get_command_manager().execute(new EditMultipleCommentsCommand(media_sources, new_comment)); 730 } 731 732 protected virtual void on_display_titles(GLib.SimpleAction action, Variant? value) { 733 bool display = value.get_boolean (); 734 735 set_display_titles(display); 736 737 Config.Facade.get_instance().set_display_photo_titles(display); 738 action.set_state (value); 739 } 740 741 protected virtual void on_display_comments(GLib.SimpleAction action, Variant? value) { 742 bool display = value.get_boolean (); 743 744 set_display_comments(display); 745 746 Config.Facade.get_instance().set_display_photo_comments(display); 747 action.set_state (value); 748 } 749 750 protected virtual void on_display_ratings(GLib.SimpleAction action, Variant? value) { 751 bool display = value.get_boolean (); 752 753 set_display_ratings(display); 754 755 Config.Facade.get_instance().set_display_photo_ratings(display); 756 action.set_state (value); 757 } 758 759 protected virtual void on_display_tags(GLib.SimpleAction action, Variant? value) { 760 bool display = value.get_boolean (); 761 762 set_display_tags(display); 763 764 Config.Facade.get_instance().set_display_photo_tags(display); 765 action.set_state (value); 766 } 767 768 protected abstract void get_config_photos_sort(out bool sort_order, out int sort_by); 769 770 protected abstract void set_config_photos_sort(bool sort_order, int sort_by); 771 772 public virtual void on_sort_changed(GLib.SimpleAction action, Variant? value) { 773 action.set_state (value); 774 775 int sort_by = get_menu_sort_by(); 776 bool sort_order = get_menu_sort_order(); 777 778 set_view_comparator(sort_by, sort_order); 779 set_config_photos_sort(sort_order, sort_by); 780 } 781 782 private void on_raw_developer_changed(GLib.SimpleAction action, 783 Variant? value) { 784 RawDeveloper developer = RawDeveloper.SHOTWELL; 785 786 switch (value.get_string ()) { 787 case "Shotwell": 788 developer = RawDeveloper.SHOTWELL; 789 break; 790 case "Camera": 791 developer = RawDeveloper.CAMERA; 792 break; 793 default: 794 break; 795 } 796 797 developer_changed(developer); 798 799 action.set_state (value); 800 } 801 802 protected virtual void developer_changed(RawDeveloper rd) { 803 if (get_view().get_selected_count() == 0) 804 return; 805 806 // Check if any photo has edits 807 808 // Display warning only when edits could be destroyed 809 bool need_warn = false; 810 811 // Make a list of all photos that need their developer changed. 812 Gee.ArrayList<DataView> to_set = new Gee.ArrayList<DataView>(); 813 foreach (DataView view in get_view().get_selected()) { 814 Photo? p = view.get_source() as Photo; 815 if (p != null && (!rd.is_equivalent(p.get_raw_developer()))) { 816 to_set.add(view); 817 818 if (p.has_transformations()) { 819 need_warn = true; 820 } 821 } 822 } 823 824 if (!need_warn || Dialogs.confirm_warn_developer_changed(to_set.size)) { 825 SetRawDeveloperCommand command = new SetRawDeveloperCommand(to_set, rd); 826 get_command_manager().execute(command); 827 828 update_development_menu_item_sensitivity(); 829 } 830 } 831 832 protected override void set_display_titles(bool display) { 833 base.set_display_titles(display); 834 835 this.set_action_active ("ViewTitle", display); 836 } 837 838 protected override void set_display_comments(bool display) { 839 base.set_display_comments(display); 840 841 this.set_action_active ("ViewComment", display); 842 } 843 844 private GLib.Action sort_by_title_action() { 845 var action = get_action ("SortBy"); 846 assert(action != null); 847 return action; 848 } 849 850 private GLib.Action sort_ascending_action() { 851 var action = get_action ("Sort"); 852 assert(action != null); 853 return action; 854 } 855 856 protected int get_menu_sort_by() { 857 // any member of the group knows the current value 858 return int.parse (sort_by_title_action().get_state().get_string ()); 859 } 860 861 protected void set_menu_sort_by(int val) { 862 var sort = "%d".printf (val); 863 sort_by_title_action().change_state (sort); 864 } 865 866 protected bool get_menu_sort_order() { 867 // any member of the group knows the current value 868 return sort_ascending_action().get_state ().get_string () == "ascending"; 869 } 870 871 protected void set_menu_sort_order(bool ascending) { 872 sort_ascending_action().change_state (ascending ? "ascending" : "descending"); 873 } 874 875 void set_view_comparator(int sort_by, bool ascending) { 876 Comparator comparator; 877 ComparatorPredicate predicate; 878 879 switch (sort_by) { 880 case SortBy.TITLE: 881 if (ascending) 882 comparator = Thumbnail.title_ascending_comparator; 883 else comparator = Thumbnail.title_descending_comparator; 884 predicate = Thumbnail.title_comparator_predicate; 885 break; 886 887 case SortBy.EXPOSURE_DATE: 888 if (ascending) 889 comparator = Thumbnail.exposure_time_ascending_comparator; 890 else comparator = Thumbnail.exposure_time_desending_comparator; 891 predicate = Thumbnail.exposure_time_comparator_predicate; 892 break; 893 894 case SortBy.RATING: 895 if (ascending) 896 comparator = Thumbnail.rating_ascending_comparator; 897 else comparator = Thumbnail.rating_descending_comparator; 898 predicate = Thumbnail.rating_comparator_predicate; 899 break; 900 901 case SortBy.FILENAME: 902 if (ascending) 903 comparator = Thumbnail.filename_ascending_comparator; 904 else comparator = Thumbnail.filename_descending_comparator; 905 predicate = Thumbnail.filename_comparator_predicate; 906 break; 907 908 default: 909 debug("Unknown sort criteria: %s", get_menu_sort_by().to_string()); 910 comparator = Thumbnail.title_descending_comparator; 911 predicate = Thumbnail.title_comparator_predicate; 912 break; 913 } 914 915 get_view().set_comparator(comparator, predicate); 916 } 917 918 protected void sync_sort() { 919 // It used to be that the config and UI could both agree on what 920 // sort order and criteria were selected, but the sorting wouldn't 921 // match them, due to the current view's comparator not actually 922 // being set to match, and since there was a check to see if the 923 // config and UI matched that would frequently succeed in this case, 924 // the sorting was often wrong until the user went in and changed 925 // it. Because there is no tidy way to query the current view's 926 // comparator, we now set it any time we even think the sorting 927 // might have changed to force them to always stay in sync. 928 // 929 // Although this means we pay for a re-sort every time, in practice, 930 // this isn't terribly expensive - it _might_ take as long as .5 sec. 931 // with a media page containing over 15000 items on a modern CPU. 932 933 bool sort_ascending; 934 int sort_by; 935 get_config_photos_sort(out sort_ascending, out sort_by); 936 937 set_menu_sort_by(sort_by); 938 set_menu_sort_order(sort_ascending); 939 940 set_view_comparator(sort_by, sort_ascending); 941 } 942 943 public override void destroy() { 944 disconnect_slider(); 945 946 base.destroy(); 947 } 948 949 public void increase_zoom_level() { 950 if (connected_slider != null) { 951 connected_slider.increase_step(); 952 } else { 953 int new_scale = compute_zoom_scale_increase(get_thumb_size()); 954 save_persistent_thumbnail_scale(); 955 set_thumb_size(new_scale); 956 } 957 } 958 959 public void decrease_zoom_level() { 960 if (connected_slider != null) { 961 connected_slider.decrease_step(); 962 } else { 963 int new_scale = compute_zoom_scale_decrease(get_thumb_size()); 964 save_persistent_thumbnail_scale(); 965 set_thumb_size(new_scale); 966 } 967 } 968 969 public virtual DataView create_thumbnail(DataSource source) { 970 return new Thumbnail((MediaSource) source, get_thumb_size()); 971 } 972 973 // this is a view-level operation on this page only; it does not affect the persistent global 974 // thumbnail scale 975 public void set_thumb_size(int new_scale) { 976 if (get_thumb_size() == new_scale || !is_in_view()) 977 return; 978 979 new_scale = new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE); 980 get_checkerboard_layout().set_scale(new_scale); 981 982 // when doing mass operations on LayoutItems, freeze individual notifications 983 get_view().freeze_notifications(); 984 get_view().set_property(Thumbnail.PROP_SIZE, new_scale); 985 get_view().thaw_notifications(); 986 987 set_action_sensitive("IncreaseSize", new_scale < Thumbnail.MAX_SCALE); 988 set_action_sensitive("DecreaseSize", new_scale > Thumbnail.MIN_SCALE); 989 } 990 991 public int get_thumb_size() { 992 if (get_checkerboard_layout().get_scale() <= 0) 993 get_checkerboard_layout().set_scale(Config.Facade.get_instance().get_photo_thumbnail_scale()); 994 995 return get_checkerboard_layout().get_scale(); 996 } 997} 998 999