[shotwell] Split-up Page.vala
- From: Jens Georg <jensgeorg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [shotwell] Split-up Page.vala
- Date: Sat, 23 Feb 2019 19:03:55 +0000 (UTC)
commit 49dd545e91d958817fd502ee3a45b1c0de8642b0
Author: Jens Georg <mail jensge org>
Date: Sat Feb 23 19:47:51 2019 +0100
Split-up Page.vala
po/POTFILES.in | 4 +
po/POTFILES.skip | 4 +
src/CheckerboardPage.vala | 763 ++++++++++++++++++++++
src/DragAndDropHandler.vala | 187 ++++++
src/Page.vala | 1472 -------------------------------------------
src/PageMessagePane.vala | 19 +
src/SinglePhotoPage.vala | 529 ++++++++++++++++
src/meson.build | 4 +
8 files changed, 1510 insertions(+), 1472 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index edc08eac..f0ea7c05 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -83,6 +83,7 @@ src/BatchImport.vala
src/camera/CameraBranch.vala
src/camera/CameraTable.vala
src/camera/ImportPage.vala
+src/CheckerboardPage.vala
src/CollectionPage.vala
src/Commands.vala
src/data_imports/DataImportsPluginHost.vala
@@ -100,6 +101,7 @@ src/dialogs/WelcomeDialog.vala
src/Dimensions.vala
src/direct/DirectPhotoPage.vala
src/DirectoryMonitor.vala
+src/DragAndDropHandler.vala
src/editing_tools/EditingTools.vala
src/editing_tools/StraightenTool.vala
src/events/EventDirectoryItem.vala
@@ -123,6 +125,7 @@ src/main.vala
src/MediaMonitor.vala
src/MediaPage.vala
src/Page.vala
+src/PageMessagePane.vala
src/PhotoPage.vala
src/photos/BmpSupport.vala
src/photos/GifSupport.vala
@@ -145,6 +148,7 @@ src/searches/SavedSearchDialog.vala
src/searches/SearchBoolean.vala
src/searches/SearchesBranch.vala
src/SearchFilter.vala
+src/SinglePhotoPage.vala
src/SlideshowPage.vala
src/slideshow/Slideshow.vala
src/slideshow/TransitionEffects.vala
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 00c4e430..373808a0 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -36,6 +36,7 @@ src/BatchImport.c
src/camera/CameraBranch.c
src/camera/CameraTable.c
src/camera/ImportPage.c
+src/CheckerboardPage.c
src/CollectionPage.c
src/Commands.c
src/data_imports/DataImports.c
@@ -53,6 +54,7 @@ src/Dialogs.c
src/Dimensions.c
src/direct/DirectPhotoPage.c
src/DirectoryMonitor.c
+src/DragAndDropHandler.c
src/editing_tools/EditingTools.c
src/editing_tools/StraightenTool.c
src/Event.c
@@ -76,6 +78,7 @@ src/main.c
src/MediaMonitor.c
src/MediaPage.c
src/Page.c
+src/PageMessagePane.c
src/Photo.c
src/PhotoPage.c
src/photos/BmpSupport.c
@@ -98,6 +101,7 @@ src/searches/SavedSearchDialog.c
src/searches/SearchBoolean.c
src/searches/SearchesBranch.c
src/SearchFilter.c
+src/SinglePhotoPage.c
src/SlideshowPage.c
src/slideshow/Slideshow.c
src/slideshow/TransitionEffects.c
diff --git a/src/CheckerboardPage.vala b/src/CheckerboardPage.vala
new file mode 100644
index 00000000..b8ef3332
--- /dev/null
+++ b/src/CheckerboardPage.vala
@@ -0,0 +1,763 @@
+/* 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.
+ */
+
+public abstract class CheckerboardPage : Page {
+ private const int AUTOSCROLL_PIXELS = 50;
+ private const int AUTOSCROLL_TICKS_MSEC = 50;
+
+ private CheckerboardLayout layout;
+ private Gtk.Stack stack;
+ private PageMessagePane message_pane;
+ private string item_context_menu_path = null;
+ private string page_context_menu_path = null;
+ private Gtk.Viewport viewport = new Gtk.Viewport(null, null);
+ protected CheckerboardItem anchor = null;
+ protected CheckerboardItem cursor = null;
+ private CheckerboardItem current_hovered_item = null;
+ private bool autoscroll_scheduled = false;
+ private CheckerboardItem activated_item = null;
+ private Gee.ArrayList<CheckerboardItem> previously_selected = null;
+ private MapWidget map_widget = null;
+
+ public enum Activator {
+ KEYBOARD,
+ MOUSE
+ }
+
+ public struct KeyboardModifiers {
+ public KeyboardModifiers(Page page) {
+ ctrl_pressed = page.get_ctrl_pressed();
+ alt_pressed = page.get_alt_pressed();
+ shift_pressed = page.get_shift_pressed();
+ super_pressed = page.get_super_pressed();
+ }
+
+ public bool ctrl_pressed;
+ public bool alt_pressed;
+ public bool shift_pressed;
+ public bool super_pressed;
+ }
+
+ public CheckerboardPage(string page_name) {
+ base (page_name);
+
+ stack = new Gtk.Stack();
+ message_pane = new PageMessagePane();
+
+ layout = new CheckerboardLayout(get_view());
+ layout.set_name(page_name);
+ stack.add_named (layout, "layout");
+ stack.add_named (message_pane, "message");
+ stack.set_visible_child(layout);
+
+ set_event_source(layout);
+
+ set_border_width(0);
+ set_shadow_type(Gtk.ShadowType.NONE);
+
+ viewport.set_border_width(0);
+ viewport.set_shadow_type(Gtk.ShadowType.NONE);
+
+ viewport.add(stack);
+
+ // want to set_adjustments before adding to ScrolledWindow to let our signal handlers
+ // run first ... otherwise, the thumbnails draw late
+ layout.set_adjustments(get_hadjustment(), get_vadjustment());
+
+ add(viewport);
+
+ // need to monitor items going hidden when dealing with anchor/cursor/highlighted items
+ get_view().items_hidden.connect(on_items_hidden);
+ get_view().contents_altered.connect(on_contents_altered);
+ get_view().items_state_changed.connect(on_items_state_changed);
+ get_view().items_visibility_changed.connect(on_items_visibility_changed);
+
+ // scrollbar policy
+ set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+
+ map_widget = MapWidget.get_instance();
+ }
+
+ public void init_item_context_menu(string path) {
+ item_context_menu_path = path;
+ }
+
+ public void init_page_context_menu(string path) {
+ page_context_menu_path = path;
+ }
+
+ public Gtk.Menu? get_context_menu() {
+ // show page context menu if nothing is selected
+ return (get_view().get_selected_count() != 0) ? get_item_context_menu() :
+ get_page_context_menu();
+ }
+
+ private Gtk.Menu item_context_menu;
+ public virtual Gtk.Menu? get_item_context_menu() {
+ if (item_context_menu == null) {
+ var model = this.builder.get_object (item_context_menu_path)
+ as GLib.MenuModel;
+ item_context_menu = new Gtk.Menu.from_model (model);
+ item_context_menu.attach_to_widget (this, null);
+ }
+
+ return item_context_menu;
+ }
+
+ private Gtk.Menu page_context_menu;
+ public override Gtk.Menu? get_page_context_menu() {
+ if (page_context_menu_path == null)
+ return null;
+
+ if (page_context_menu == null) {
+ var model = this.builder.get_object (page_context_menu_path)
+ as GLib.MenuModel;
+ page_context_menu = new Gtk.Menu.from_model (model);
+ page_context_menu.attach_to_widget (this, null);
+ }
+
+ return page_context_menu;
+ }
+
+ protected override bool on_context_keypress() {
+ return popup_context_menu(get_context_menu());
+ }
+
+ protected virtual string get_view_empty_icon() {
+ return "image-x-generic-symbolic";
+ }
+
+ protected virtual string get_view_empty_message() {
+ return _("No photos/videos");
+ }
+
+ protected virtual string get_filter_no_match_message() {
+ return _("No photos/videos found which match the current filter");
+ }
+
+ protected virtual void on_item_activated(CheckerboardItem item, Activator activator,
+ KeyboardModifiers modifiers) {
+ }
+
+ public CheckerboardLayout get_checkerboard_layout() {
+ return layout;
+ }
+
+ // Gets the search view filter for this page.
+ public abstract SearchViewFilter get_search_view_filter();
+
+ public virtual Core.ViewTracker? get_view_tracker() {
+ return null;
+ }
+
+ public override void switching_from() {
+ layout.set_in_view(false);
+ get_search_view_filter().refresh.disconnect(on_view_filter_refresh);
+
+ // unselect everything so selection won't persist after page loses focus
+ get_view().unselect_all();
+
+ base.switching_from();
+ }
+
+ public void scroll_to_item(CheckerboardItem item) {
+ Gtk.Adjustment vadj = get_vadjustment();
+ if (!(get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
+ && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) ==
AdjustmentRelation.IN_RANGE))) {
+
+ // scroll to see the new item
+ int top = 0;
+ if (item.allocation.y < vadj.get_value()) {
+ top = item.allocation.y;
+ top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
+ } else {
+ top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
+ top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
+ }
+
+ vadj.set_value(top);
+
+ }
+ }
+
+ public override void switched_to() {
+ layout.set_in_view(true);
+ get_search_view_filter().refresh.connect(on_view_filter_refresh);
+ on_view_filter_refresh();
+
+ if (get_view().get_selected_count() > 0) {
+ CheckerboardItem? item = (CheckerboardItem?) get_view().get_selected_at(0);
+
+ // if item is in any way out of view, scroll to it
+ scroll_to_item(item);
+ }
+
+ base.switched_to();
+ }
+
+ private void on_view_filter_refresh() {
+ update_view_filter_message();
+ }
+
+ private void on_contents_altered(Gee.Iterable<DataObject>? added,
+ Gee.Iterable<DataObject>? removed) {
+ update_view_filter_message();
+ }
+
+ private void on_items_state_changed(Gee.Iterable<DataView> changed) {
+ update_view_filter_message();
+ }
+
+ private void on_items_visibility_changed(Gee.Collection<DataView> changed) {
+ update_view_filter_message();
+ }
+
+ private void update_view_filter_message() {
+ if (get_view().are_items_filtered_out() && get_view().get_count() == 0) {
+ set_page_message(get_filter_no_match_message());
+ } else if (get_view().get_count() == 0) {
+ set_page_message(get_view_empty_message());
+ } else {
+ unset_page_message();
+ }
+ }
+
+ public void set_page_message(string message) {
+ message_pane.label.label = message;
+ try {
+ message_pane.icon_image.icon_name = null;
+ message_pane.icon_image.gicon = Icon.new_for_string (get_view_empty_icon());
+ } catch (Error error) {
+ message_pane.icon_image.gicon = null;
+ message_pane.icon_image.icon_name = "image-x-generic-symbolic";
+ }
+ stack.set_visible_child_name ("message");
+ }
+
+ public void unset_page_message() {
+ stack.set_visible_child (layout);
+ }
+
+ public override void set_page_name(string name) {
+ base.set_page_name(name);
+
+ layout.set_name(name);
+ }
+
+ public CheckerboardItem? get_item_at_pixel(double x, double y) {
+ return layout.get_item_at_pixel(x, y);
+ }
+
+ private void on_items_hidden(Gee.Iterable<DataView> hidden) {
+ foreach (DataView view in hidden) {
+ CheckerboardItem item = (CheckerboardItem) view;
+
+ if (anchor == item)
+ anchor = null;
+
+ if (cursor == item)
+ cursor = null;
+
+ if (current_hovered_item == item)
+ current_hovered_item = null;
+ }
+ }
+
+ protected override bool key_press_event(Gdk.EventKey event) {
+ bool handled = true;
+
+ // mask out the modifiers we're interested in
+ uint state = event.state & Gdk.ModifierType.SHIFT_MASK;
+
+ switch (Gdk.keyval_name(event.keyval)) {
+ case "Up":
+ case "KP_Up":
+ move_cursor(CompassPoint.NORTH);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Down":
+ case "KP_Down":
+ move_cursor(CompassPoint.SOUTH);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Left":
+ case "KP_Left":
+ move_cursor(CompassPoint.WEST);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Right":
+ case "KP_Right":
+ move_cursor(CompassPoint.EAST);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Home":
+ case "KP_Home":
+ CheckerboardItem? first = (CheckerboardItem?) get_view().get_first();
+ if (first != null)
+ cursor_to_item(first);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "End":
+ case "KP_End":
+ CheckerboardItem? last = (CheckerboardItem?) get_view().get_last();
+ if (last != null)
+ cursor_to_item(last);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Return":
+ case "KP_Enter":
+ if (get_view().get_selected_count() == 1)
+ on_item_activated((CheckerboardItem) get_view().get_selected_at(0),
+ Activator.KEYBOARD, KeyboardModifiers(this));
+ else
+ handled = false;
+ break;
+
+ case "space":
+ Marker marker = get_view().mark(layout.get_cursor());
+ get_view().toggle_marked(marker);
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled)
+ return true;
+
+ return (base.key_press_event != null) ? base.key_press_event(event) : true;
+ }
+
+ protected override bool on_left_click(Gdk.EventButton event) {
+ // only interested in single-click and double-clicks for now
+ if ((event.type != Gdk.EventType.BUTTON_PRESS) && (event.type != Gdk.EventType.2BUTTON_PRESS))
+ return false;
+
+ // mask out the modifiers we're interested in
+ uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK);
+
+ // use clicks for multiple selection and activation only; single selects are handled by
+ // button release, to allow for multiple items to be selected then dragged ...
+ CheckerboardItem item = get_item_at_pixel(event.x, event.y);
+ if (item != null) {
+ // ... however, there is no dragging if the user clicks on an interactive part of the
+ // CheckerboardItem (e.g. a tag)
+ if (layout.handle_left_click(item, event.x, event.y, event.state))
+ return true;
+
+ switch (state) {
+ case Gdk.ModifierType.CONTROL_MASK:
+ // with only Ctrl pressed, multiple selections are possible ... chosen item
+ // is toggled
+ Marker marker = get_view().mark(item);
+ get_view().toggle_marked(marker);
+
+ if (item.is_selected()) {
+ anchor = item;
+ cursor = item;
+ }
+ break;
+
+ case Gdk.ModifierType.SHIFT_MASK:
+ get_view().unselect_all();
+
+ if (anchor == null)
+ anchor = item;
+
+ select_between_items(anchor, item);
+
+ cursor = item;
+ break;
+
+ case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
+ // Ticket #853 - Make Ctrl + Shift + Mouse Button 1 able to start a new run
+ // of contiguous selected items without unselecting previously-selected items
+ // a la Nautilus.
+ // Same as the case for SHIFT_MASK, but don't unselect anything first.
+ if (anchor == null)
+ anchor = item;
+
+ select_between_items(anchor, item);
+
+ cursor = item;
+ break;
+
+ default:
+ if (event.type == Gdk.EventType.2BUTTON_PRESS) {
+ activated_item = item;
+ } else {
+ // if the user has selected one or more items and is preparing for a drag,
+ // don't want to blindly unselect: if they've clicked on an unselected item
+ // unselect all and select that one; if they've clicked on a previously
+ // selected item, do nothing
+ if (!item.is_selected()) {
+ Marker all = get_view().start_marking();
+ all.mark_many(get_view().get_selected());
+
+ get_view().unselect_and_select_marked(all, get_view().mark(item));
+ }
+ }
+
+ anchor = item;
+ cursor = item;
+ break;
+ }
+ layout.set_cursor(item);
+ } else {
+ // user clicked on "dead" area; only unselect if control is not pressed
+ // do we want similar behavior for shift as well?
+ if (state != Gdk.ModifierType.CONTROL_MASK)
+ get_view().unselect_all();
+
+ // grab previously marked items
+ previously_selected = new Gee.ArrayList<CheckerboardItem>();
+ foreach (DataView view in get_view().get_selected())
+ previously_selected.add((CheckerboardItem) view);
+
+ layout.set_drag_select_origin((int) event.x, (int) event.y);
+
+ return true;
+ }
+
+ // need to determine if the signal should be passed to the DnD handlers
+ // Return true to block the DnD handler, false otherwise
+
+ return get_view().get_selected_count() == 0;
+ }
+
+ protected override bool on_left_released(Gdk.EventButton event) {
+ previously_selected = null;
+
+ // if drag-selecting, stop here and do nothing else
+ if (layout.is_drag_select_active()) {
+ layout.clear_drag_select();
+ anchor = cursor;
+
+ return true;
+ }
+
+ // only interested in non-modified button releases
+ if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0)
+ return false;
+
+ // if the item was activated in the double-click, report it now
+ if (activated_item != null) {
+ on_item_activated(activated_item, Activator.MOUSE, KeyboardModifiers(this));
+ activated_item = null;
+
+ return true;
+ }
+
+ CheckerboardItem item = get_item_at_pixel(event.x, event.y);
+ if (item == null) {
+ // released button on "dead" area
+ return true;
+ }
+
+ if (cursor != item) {
+ // user released mouse button after moving it off the initial item, or moved from dead
+ // space onto one. either way, unselect everything
+ get_view().unselect_all();
+ } else {
+ // the idea is, if a user single-clicks on an item with no modifiers, then all other items
+ // should be deselected, however, if they single-click in order to drag one or more items,
+ // they should remain selected, hence performing this here rather than on_left_click
+ // (item may not be selected if an unimplemented modifier key was used)
+ if (item.is_selected())
+ get_view().unselect_all_but(item);
+ }
+
+ return true;
+ }
+
+ protected override bool on_right_click(Gdk.EventButton event) {
+ // only interested in single-clicks for now
+ if (event.type != Gdk.EventType.BUTTON_PRESS)
+ return false;
+
+ // get what's right-clicked upon
+ CheckerboardItem item = get_item_at_pixel(event.x, event.y);
+ if (item != null) {
+ // mask out the modifiers we're interested in
+ switch (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) {
+ case Gdk.ModifierType.CONTROL_MASK:
+ // chosen item is toggled
+ Marker marker = get_view().mark(item);
+ get_view().toggle_marked(marker);
+ break;
+
+ case Gdk.ModifierType.SHIFT_MASK:
+ // TODO
+ break;
+
+ case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
+ // TODO
+ break;
+
+ default:
+ // if the item is already selected, proceed; if item is not selected, a bare right
+ // click unselects everything else but it
+ if (!item.is_selected()) {
+ Marker all = get_view().start_marking();
+ all.mark_many(get_view().get_selected());
+
+ get_view().unselect_and_select_marked(all, get_view().mark(item));
+ }
+ break;
+ }
+ } else {
+ // clicked in "dead" space, unselect everything
+ get_view().unselect_all();
+ }
+
+ Gtk.Menu context_menu = get_context_menu();
+ return popup_context_menu(context_menu, event);
+ }
+
+ protected virtual bool on_mouse_over(CheckerboardItem? item, int x, int y, Gdk.ModifierType mask) {
+ if (item != null)
+ layout.handle_mouse_motion(item, x, y, mask);
+
+ // if hovering over the last hovered item, or both are null (nothing highlighted and
+ // hovering over empty space), do nothing
+ if (item == current_hovered_item)
+ return true;
+
+ // either something new is highlighted or now hovering over empty space, so dim old item
+ if (current_hovered_item != null) {
+ current_hovered_item.handle_mouse_leave();
+ map_widget.unhighlight_position_marker(current_hovered_item);
+ current_hovered_item = null;
+ }
+
+ // if over empty space, done
+ if (item == null)
+ return true;
+
+ // brighten the new item
+ current_hovered_item = item;
+ current_hovered_item.handle_mouse_enter();
+ map_widget.highlight_position_marker(item);
+
+ return true;
+ }
+
+ protected override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
+ // report what item the mouse is hovering over
+ if (!on_mouse_over(get_item_at_pixel(x, y), x, y, mask))
+ return false;
+
+ // go no further if not drag-selecting
+ if (!layout.is_drag_select_active())
+ return false;
+
+ // set the new endpoint of the drag selection
+ layout.set_drag_select_endpoint(x, y);
+
+ updated_selection_band();
+
+ // if out of bounds, schedule a check to auto-scroll the viewport
+ if (!autoscroll_scheduled
+ && get_adjustment_relation(get_vadjustment(), y) != AdjustmentRelation.IN_RANGE) {
+ Timeout.add(AUTOSCROLL_TICKS_MSEC, selection_autoscroll);
+ autoscroll_scheduled = true;
+ }
+
+ // return true to stop a potential drag-and-drop operation
+ return true;
+ }
+
+ private void updated_selection_band() {
+ assert(layout.is_drag_select_active());
+
+ // get all items inside the selection
+ Gee.List<CheckerboardItem>? intersection = layout.items_in_selection_band();
+ if (intersection == null)
+ return;
+
+ Marker to_unselect = get_view().start_marking();
+ Marker to_select = get_view().start_marking();
+
+ // mark all selected items to be unselected
+ to_unselect.mark_many(get_view().get_selected());
+
+ // except for the items that were selected before the drag began
+ assert(previously_selected != null);
+ to_unselect.unmark_many(previously_selected);
+ to_select.mark_many(previously_selected);
+
+ // toggle selection on everything in the intersection and update the cursor
+ cursor = null;
+
+ foreach (CheckerboardItem item in intersection) {
+ if (to_select.toggle(item))
+ to_unselect.unmark(item);
+ else
+ to_unselect.mark(item);
+
+ if (cursor == null)
+ cursor = item;
+ }
+
+ get_view().select_marked(to_select);
+ get_view().unselect_marked(to_unselect);
+ }
+
+ private bool selection_autoscroll() {
+ if (!layout.is_drag_select_active()) {
+ autoscroll_scheduled = false;
+
+ return false;
+ }
+
+ // as the viewport never scrolls horizontally, only interested in vertical
+ Gtk.Adjustment vadj = get_vadjustment();
+
+ int x, y;
+ Gdk.ModifierType mask;
+ get_event_source_pointer(out x, out y, out mask);
+
+ int new_value = (int) vadj.get_value();
+ switch (get_adjustment_relation(vadj, y)) {
+ case AdjustmentRelation.BELOW:
+ // pointer above window, scroll up
+ new_value -= AUTOSCROLL_PIXELS;
+ layout.set_drag_select_endpoint(x, new_value);
+ break;
+
+ case AdjustmentRelation.ABOVE:
+ // pointer below window, scroll down, extend selection to bottom of page
+ new_value += AUTOSCROLL_PIXELS;
+ layout.set_drag_select_endpoint(x, new_value + (int) vadj.get_page_size());
+ break;
+
+ case AdjustmentRelation.IN_RANGE:
+ autoscroll_scheduled = false;
+
+ return false;
+
+ default:
+ warn_if_reached();
+ break;
+ }
+
+ // It appears that in GTK+ 2.18, the adjustment is not clamped the way it was in 2.16.
+ // This may have to do with how adjustments are different w/ scrollbars, that they're upper
+ // clamp is upper - page_size ... either way, enforce these limits here
+ vadj.set_value(new_value.clamp((int) vadj.get_lower(),
+ (int) vadj.get_upper() - (int) vadj.get_page_size()));
+
+ updated_selection_band();
+
+ return true;
+ }
+
+ public void cursor_to_item(CheckerboardItem item) {
+ assert(get_view().contains(item));
+
+ cursor = item;
+
+ if (!get_ctrl_pressed()) {
+ get_view().unselect_all();
+ Marker marker = get_view().mark(item);
+ get_view().select_marked(marker);
+ }
+ layout.set_cursor(item);
+ scroll_to_item(item);
+ }
+
+ public void move_cursor(CompassPoint point) {
+ // if no items, nothing to do
+ if (get_view().get_count() == 0)
+ return;
+
+ // if there is no better starting point, simply select the first and exit
+ // The right half of the or is related to Bug #732334, the cursor might be non-null and still not
contained in
+ // the view, if the user dragged a full screen Photo off screen
+ if (cursor == null && layout.get_cursor() == null || cursor != null && !get_view().contains(cursor))
{
+ CheckerboardItem item = layout.get_item_at_coordinate(0, 0);
+ cursor_to_item(item);
+ anchor = item;
+
+ return;
+ }
+
+ if (cursor == null) {
+ cursor = layout.get_cursor() as CheckerboardItem;
+ }
+
+ // move the cursor relative to the "first" item
+ CheckerboardItem? item = layout.get_item_relative_to(cursor, point);
+ if (item != null)
+ cursor_to_item(item);
+ }
+
+ public void set_cursor(CheckerboardItem item) {
+ Marker marker = get_view().mark(item);
+ get_view().select_marked(marker);
+
+ cursor = item;
+ anchor = item;
+ }
+
+ public void select_between_items(CheckerboardItem item_start, CheckerboardItem item_end) {
+ Marker marker = get_view().start_marking();
+
+ bool passed_start = false;
+ bool passed_end = false;
+
+ foreach (DataObject object in get_view().get_all()) {
+ CheckerboardItem item = (CheckerboardItem) object;
+
+ if (item_start == item)
+ passed_start = true;
+
+ if (item_end == item)
+ passed_end = true;
+
+ if (passed_start || passed_end)
+ marker.mark((DataView) object);
+
+ if (passed_start && passed_end)
+ break;
+ }
+
+ get_view().select_marked(marker);
+ }
+
+ public void select_anchor_to_cursor(uint state) {
+ if (cursor == null || anchor == null)
+ return;
+
+ if (state == Gdk.ModifierType.SHIFT_MASK) {
+ get_view().unselect_all();
+ select_between_items(anchor, cursor);
+ } else {
+ anchor = cursor;
+ }
+ }
+
+ protected virtual void set_display_titles(bool display) {
+ get_view().freeze_notifications();
+ get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, display);
+ get_view().thaw_notifications();
+ }
+
+ protected virtual void set_display_comments(bool display) {
+ get_view().freeze_notifications();
+ get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, display);
+ get_view().thaw_notifications();
+ }
+}
+
+
diff --git a/src/DragAndDropHandler.vala b/src/DragAndDropHandler.vala
new file mode 100644
index 00000000..4348de22
--- /dev/null
+++ b/src/DragAndDropHandler.vala
@@ -0,0 +1,187 @@
+/* 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.
+ */
+
+//
+// DragAndDropHandler attaches signals to a Page to properly handle drag-and-drop requests for the
+// Page as a DnD Source. (DnD Destination handling is handled by the appropriate AppWindow, i.e.
+// LibraryWindow and DirectWindow). Assumes the Page's ViewCollection holds MediaSources.
+//
+public class DragAndDropHandler {
+ private enum TargetType {
+ XDS,
+ MEDIA_LIST
+ }
+
+ private const Gtk.TargetEntry[] SOURCE_TARGET_ENTRIES = {
+ { "XdndDirectSave0", Gtk.TargetFlags.OTHER_APP, TargetType.XDS },
+ { "shotwell/media-id-atom", Gtk.TargetFlags.SAME_APP, TargetType.MEDIA_LIST }
+ };
+
+ private static Gdk.Atom? XDS_ATOM = null;
+ private static Gdk.Atom? TEXT_ATOM = null;
+ private static uint8[]? XDS_FAKE_TARGET = null;
+
+ private weak Page page;
+ private Gtk.Widget event_source;
+ private File? drag_destination = null;
+ private ExporterUI exporter = null;
+
+ public DragAndDropHandler(Page page) {
+ this.page = page;
+ this.event_source = page.get_event_source();
+ assert(event_source != null);
+ assert(event_source.get_has_window());
+
+ // Need to do this because static member variables are not properly handled
+ if (XDS_ATOM == null)
+ XDS_ATOM = Gdk.Atom.intern_static_string("XdndDirectSave0");
+
+ if (TEXT_ATOM == null)
+ TEXT_ATOM = Gdk.Atom.intern_static_string("text/plain");
+
+ if (XDS_FAKE_TARGET == null)
+ XDS_FAKE_TARGET = string_to_uchar_array("shotwell.txt");
+
+ // register what's available on this DnD Source
+ Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, SOURCE_TARGET_ENTRIES,
+ Gdk.DragAction.COPY);
+
+ // attach to the event source's DnD signals, not the Page's, which is a NO_WINDOW widget
+ // and does not emit them
+ event_source.drag_begin.connect(on_drag_begin);
+ event_source.drag_data_get.connect(on_drag_data_get);
+ event_source.drag_end.connect(on_drag_end);
+ event_source.drag_failed.connect(on_drag_failed);
+ }
+
+ ~DragAndDropHandler() {
+ if (event_source != null) {
+ event_source.drag_begin.disconnect(on_drag_begin);
+ event_source.drag_data_get.disconnect(on_drag_data_get);
+ event_source.drag_end.disconnect(on_drag_end);
+ event_source.drag_failed.disconnect(on_drag_failed);
+ }
+
+ page = null;
+ event_source = null;
+ }
+
+ private void on_drag_begin(Gdk.DragContext context) {
+ debug("on_drag_begin (%s)", page.get_page_name());
+
+ if (page == null || page.get_view().get_selected_count() == 0 || exporter != null)
+ return;
+
+ drag_destination = null;
+
+ // use the first media item as the icon
+ ThumbnailSource thumb = (ThumbnailSource) page.get_view().get_selected_at(0).get_source();
+
+ try {
+ Gdk.Pixbuf icon = thumb.get_thumbnail(AppWindow.DND_ICON_SCALE);
+ Gtk.drag_source_set_icon_pixbuf(event_source, icon);
+ } catch (Error err) {
+ warning("Unable to fetch icon for drag-and-drop from %s: %s", thumb.to_string(),
+ err.message);
+ }
+
+ // set the XDS property to indicate an XDS save is available
+#if VALA_0_20
+ Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE,
+ XDS_FAKE_TARGET, 1);
+#else
+ Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE,
+ XDS_FAKE_TARGET);
+#endif
+ }
+
+ private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
+ uint target_type, uint time) {
+ debug("on_drag_data_get (%s)", page.get_page_name());
+
+ if (page == null || page.get_view().get_selected_count() == 0)
+ return;
+
+ switch (target_type) {
+ case TargetType.XDS:
+ // Fetch the XDS property that has been set with the destination path
+ uchar[] data = new uchar[4096];
+ Gdk.Atom actual_type;
+ int actual_format = 0;
+ bool fetched = Gdk.property_get(context.get_source_window(), XDS_ATOM, TEXT_ATOM,
+ 0, data.length, 0, out actual_type, out actual_format, out data);
+
+ // the destination path is actually for our XDS_FAKE_TARGET, use its parent
+ // to determine where the file(s) should go
+ if (fetched && data != null && data.length > 0)
+ drag_destination = File.new_for_uri(uchar_array_to_string(data)).get_parent();
+
+ debug("on_drag_data_get (%s): %s", page.get_page_name(),
+ (drag_destination != null) ? drag_destination.get_path() : "(no path)");
+
+ // Set the property to "S" for Success or "E" for Error
+ selection_data.set(XDS_ATOM, 8,
+ string_to_uchar_array((drag_destination != null) ? "S" : "E"));
+ break;
+
+ case TargetType.MEDIA_LIST:
+ Gee.Collection<MediaSource> sources =
+ (Gee.Collection<MediaSource>) page.get_view().get_selected_sources();
+
+ // convert the selected media sources to Gdk.Atom-encoded sourceID strings for
+ // internal drag-and-drop
+ selection_data.set(Gdk.Atom.intern_static_string("SourceIDAtom"), (int) sizeof(Gdk.Atom),
+ serialize_media_sources(sources));
+ break;
+
+ default:
+ warning("on_drag_data_get (%s): unknown target type %u", page.get_page_name(),
+ target_type);
+ break;
+ }
+ }
+
+ private void on_drag_end() {
+ debug("on_drag_end (%s)", page.get_page_name());
+
+ if (page == null || page.get_view().get_selected_count() == 0 || drag_destination == null
+ || exporter != null) {
+ return;
+ }
+
+ debug("Exporting to %s", drag_destination.get_path());
+
+ // drag-and-drop export doesn't pop up an export dialog, so use what are likely the
+ // most common export settings (the current -- or "working" -- file format, with
+ // all transformations applied, at the image's original size).
+ if (drag_destination.get_path() != null) {
+ exporter = new ExporterUI(new Exporter(
+ (Gee.Collection<Photo>) page.get_view().get_selected_sources(),
+ drag_destination, Scaling.for_original(), ExportFormatParameters.current()));
+ exporter.export(on_export_completed);
+ } else {
+ AppWindow.error_message(_("Photos cannot be exported to this directory."));
+ }
+
+ drag_destination = null;
+ }
+
+ private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
+ debug("on_drag_failed (%s): %d", page.get_page_name(), (int) drag_result);
+
+ if (page == null)
+ return false;
+
+ drag_destination = null;
+
+ return false;
+ }
+
+ private void on_export_completed() {
+ exporter = null;
+ }
+
+}
diff --git a/src/Page.vala b/src/Page.vala
index 59cbfb8b..076f42ae 100644
--- a/src/Page.vala
+++ b/src/Page.vala
@@ -1213,1475 +1213,3 @@ public abstract class Page : Gtk.ScrolledWindow {
}
-[GtkTemplate (ui = "/org/gnome/Shotwell/ui/message_pane.ui")]
-private class PageMessagePane : Gtk.Box {
- [GtkChild]
- public Gtk.Label label;
-
- [GtkChild]
- public Gtk.Image icon_image;
-
- public PageMessagePane() {
- Object();
- }
-}
-
-public abstract class CheckerboardPage : Page {
- private const int AUTOSCROLL_PIXELS = 50;
- private const int AUTOSCROLL_TICKS_MSEC = 50;
-
- private CheckerboardLayout layout;
- private Gtk.Stack stack;
- private PageMessagePane message_pane;
- private string item_context_menu_path = null;
- private string page_context_menu_path = null;
- private Gtk.Viewport viewport = new Gtk.Viewport(null, null);
- protected CheckerboardItem anchor = null;
- protected CheckerboardItem cursor = null;
- private CheckerboardItem current_hovered_item = null;
- private bool autoscroll_scheduled = false;
- private CheckerboardItem activated_item = null;
- private Gee.ArrayList<CheckerboardItem> previously_selected = null;
- private MapWidget map_widget = null;
-
- public enum Activator {
- KEYBOARD,
- MOUSE
- }
-
- public struct KeyboardModifiers {
- public KeyboardModifiers(Page page) {
- ctrl_pressed = page.get_ctrl_pressed();
- alt_pressed = page.get_alt_pressed();
- shift_pressed = page.get_shift_pressed();
- super_pressed = page.get_super_pressed();
- }
-
- public bool ctrl_pressed;
- public bool alt_pressed;
- public bool shift_pressed;
- public bool super_pressed;
- }
-
- public CheckerboardPage(string page_name) {
- base (page_name);
-
- stack = new Gtk.Stack();
- message_pane = new PageMessagePane();
-
- layout = new CheckerboardLayout(get_view());
- layout.set_name(page_name);
- stack.add_named (layout, "layout");
- stack.add_named (message_pane, "message");
- stack.set_visible_child(layout);
-
- set_event_source(layout);
-
- set_border_width(0);
- set_shadow_type(Gtk.ShadowType.NONE);
-
- viewport.set_border_width(0);
- viewport.set_shadow_type(Gtk.ShadowType.NONE);
-
- viewport.add(stack);
-
- // want to set_adjustments before adding to ScrolledWindow to let our signal handlers
- // run first ... otherwise, the thumbnails draw late
- layout.set_adjustments(get_hadjustment(), get_vadjustment());
-
- add(viewport);
-
- // need to monitor items going hidden when dealing with anchor/cursor/highlighted items
- get_view().items_hidden.connect(on_items_hidden);
- get_view().contents_altered.connect(on_contents_altered);
- get_view().items_state_changed.connect(on_items_state_changed);
- get_view().items_visibility_changed.connect(on_items_visibility_changed);
-
- // scrollbar policy
- set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
-
- map_widget = MapWidget.get_instance();
- }
-
- public void init_item_context_menu(string path) {
- item_context_menu_path = path;
- }
-
- public void init_page_context_menu(string path) {
- page_context_menu_path = path;
- }
-
- public Gtk.Menu? get_context_menu() {
- // show page context menu if nothing is selected
- return (get_view().get_selected_count() != 0) ? get_item_context_menu() :
- get_page_context_menu();
- }
-
- private Gtk.Menu item_context_menu;
- public virtual Gtk.Menu? get_item_context_menu() {
- if (item_context_menu == null) {
- var model = this.builder.get_object (item_context_menu_path)
- as GLib.MenuModel;
- item_context_menu = new Gtk.Menu.from_model (model);
- item_context_menu.attach_to_widget (this, null);
- }
-
- return item_context_menu;
- }
-
- private Gtk.Menu page_context_menu;
- public override Gtk.Menu? get_page_context_menu() {
- if (page_context_menu_path == null)
- return null;
-
- if (page_context_menu == null) {
- var model = this.builder.get_object (page_context_menu_path)
- as GLib.MenuModel;
- page_context_menu = new Gtk.Menu.from_model (model);
- page_context_menu.attach_to_widget (this, null);
- }
-
- return page_context_menu;
- }
-
- protected override bool on_context_keypress() {
- return popup_context_menu(get_context_menu());
- }
-
- protected virtual string get_view_empty_icon() {
- return "image-x-generic-symbolic";
- }
-
- protected virtual string get_view_empty_message() {
- return _("No photos/videos");
- }
-
- protected virtual string get_filter_no_match_message() {
- return _("No photos/videos found which match the current filter");
- }
-
- protected virtual void on_item_activated(CheckerboardItem item, Activator activator,
- KeyboardModifiers modifiers) {
- }
-
- public CheckerboardLayout get_checkerboard_layout() {
- return layout;
- }
-
- // Gets the search view filter for this page.
- public abstract SearchViewFilter get_search_view_filter();
-
- public virtual Core.ViewTracker? get_view_tracker() {
- return null;
- }
-
- public override void switching_from() {
- layout.set_in_view(false);
- get_search_view_filter().refresh.disconnect(on_view_filter_refresh);
-
- // unselect everything so selection won't persist after page loses focus
- get_view().unselect_all();
-
- base.switching_from();
- }
-
- public void scroll_to_item(CheckerboardItem item) {
- Gtk.Adjustment vadj = get_vadjustment();
- if (!(get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
- && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) ==
AdjustmentRelation.IN_RANGE))) {
-
- // scroll to see the new item
- int top = 0;
- if (item.allocation.y < vadj.get_value()) {
- top = item.allocation.y;
- top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
- } else {
- top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
- top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
- }
-
- vadj.set_value(top);
-
- }
- }
-
- public override void switched_to() {
- layout.set_in_view(true);
- get_search_view_filter().refresh.connect(on_view_filter_refresh);
- on_view_filter_refresh();
-
- if (get_view().get_selected_count() > 0) {
- CheckerboardItem? item = (CheckerboardItem?) get_view().get_selected_at(0);
-
- // if item is in any way out of view, scroll to it
- scroll_to_item(item);
- }
-
- base.switched_to();
- }
-
- private void on_view_filter_refresh() {
- update_view_filter_message();
- }
-
- private void on_contents_altered(Gee.Iterable<DataObject>? added,
- Gee.Iterable<DataObject>? removed) {
- update_view_filter_message();
- }
-
- private void on_items_state_changed(Gee.Iterable<DataView> changed) {
- update_view_filter_message();
- }
-
- private void on_items_visibility_changed(Gee.Collection<DataView> changed) {
- update_view_filter_message();
- }
-
- private void update_view_filter_message() {
- if (get_view().are_items_filtered_out() && get_view().get_count() == 0) {
- set_page_message(get_filter_no_match_message());
- } else if (get_view().get_count() == 0) {
- set_page_message(get_view_empty_message());
- } else {
- unset_page_message();
- }
- }
-
- public void set_page_message(string message) {
- message_pane.label.label = message;
- try {
- message_pane.icon_image.icon_name = null;
- message_pane.icon_image.gicon = Icon.new_for_string (get_view_empty_icon());
- } catch (Error error) {
- message_pane.icon_image.gicon = null;
- message_pane.icon_image.icon_name = "image-x-generic-symbolic";
- }
- stack.set_visible_child_name ("message");
- }
-
- public void unset_page_message() {
- stack.set_visible_child (layout);
- }
-
- public override void set_page_name(string name) {
- base.set_page_name(name);
-
- layout.set_name(name);
- }
-
- public CheckerboardItem? get_item_at_pixel(double x, double y) {
- return layout.get_item_at_pixel(x, y);
- }
-
- private void on_items_hidden(Gee.Iterable<DataView> hidden) {
- foreach (DataView view in hidden) {
- CheckerboardItem item = (CheckerboardItem) view;
-
- if (anchor == item)
- anchor = null;
-
- if (cursor == item)
- cursor = null;
-
- if (current_hovered_item == item)
- current_hovered_item = null;
- }
- }
-
- protected override bool key_press_event(Gdk.EventKey event) {
- bool handled = true;
-
- // mask out the modifiers we're interested in
- uint state = event.state & Gdk.ModifierType.SHIFT_MASK;
-
- switch (Gdk.keyval_name(event.keyval)) {
- case "Up":
- case "KP_Up":
- move_cursor(CompassPoint.NORTH);
- select_anchor_to_cursor(state);
- break;
-
- case "Down":
- case "KP_Down":
- move_cursor(CompassPoint.SOUTH);
- select_anchor_to_cursor(state);
- break;
-
- case "Left":
- case "KP_Left":
- move_cursor(CompassPoint.WEST);
- select_anchor_to_cursor(state);
- break;
-
- case "Right":
- case "KP_Right":
- move_cursor(CompassPoint.EAST);
- select_anchor_to_cursor(state);
- break;
-
- case "Home":
- case "KP_Home":
- CheckerboardItem? first = (CheckerboardItem?) get_view().get_first();
- if (first != null)
- cursor_to_item(first);
- select_anchor_to_cursor(state);
- break;
-
- case "End":
- case "KP_End":
- CheckerboardItem? last = (CheckerboardItem?) get_view().get_last();
- if (last != null)
- cursor_to_item(last);
- select_anchor_to_cursor(state);
- break;
-
- case "Return":
- case "KP_Enter":
- if (get_view().get_selected_count() == 1)
- on_item_activated((CheckerboardItem) get_view().get_selected_at(0),
- Activator.KEYBOARD, KeyboardModifiers(this));
- else
- handled = false;
- break;
-
- case "space":
- Marker marker = get_view().mark(layout.get_cursor());
- get_view().toggle_marked(marker);
- break;
-
- default:
- handled = false;
- break;
- }
-
- if (handled)
- return true;
-
- return (base.key_press_event != null) ? base.key_press_event(event) : true;
- }
-
- protected override bool on_left_click(Gdk.EventButton event) {
- // only interested in single-click and double-clicks for now
- if ((event.type != Gdk.EventType.BUTTON_PRESS) && (event.type != Gdk.EventType.2BUTTON_PRESS))
- return false;
-
- // mask out the modifiers we're interested in
- uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK);
-
- // use clicks for multiple selection and activation only; single selects are handled by
- // button release, to allow for multiple items to be selected then dragged ...
- CheckerboardItem item = get_item_at_pixel(event.x, event.y);
- if (item != null) {
- // ... however, there is no dragging if the user clicks on an interactive part of the
- // CheckerboardItem (e.g. a tag)
- if (layout.handle_left_click(item, event.x, event.y, event.state))
- return true;
-
- switch (state) {
- case Gdk.ModifierType.CONTROL_MASK:
- // with only Ctrl pressed, multiple selections are possible ... chosen item
- // is toggled
- Marker marker = get_view().mark(item);
- get_view().toggle_marked(marker);
-
- if (item.is_selected()) {
- anchor = item;
- cursor = item;
- }
- break;
-
- case Gdk.ModifierType.SHIFT_MASK:
- get_view().unselect_all();
-
- if (anchor == null)
- anchor = item;
-
- select_between_items(anchor, item);
-
- cursor = item;
- break;
-
- case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
- // Ticket #853 - Make Ctrl + Shift + Mouse Button 1 able to start a new run
- // of contiguous selected items without unselecting previously-selected items
- // a la Nautilus.
- // Same as the case for SHIFT_MASK, but don't unselect anything first.
- if (anchor == null)
- anchor = item;
-
- select_between_items(anchor, item);
-
- cursor = item;
- break;
-
- default:
- if (event.type == Gdk.EventType.2BUTTON_PRESS) {
- activated_item = item;
- } else {
- // if the user has selected one or more items and is preparing for a drag,
- // don't want to blindly unselect: if they've clicked on an unselected item
- // unselect all and select that one; if they've clicked on a previously
- // selected item, do nothing
- if (!item.is_selected()) {
- Marker all = get_view().start_marking();
- all.mark_many(get_view().get_selected());
-
- get_view().unselect_and_select_marked(all, get_view().mark(item));
- }
- }
-
- anchor = item;
- cursor = item;
- break;
- }
- layout.set_cursor(item);
- } else {
- // user clicked on "dead" area; only unselect if control is not pressed
- // do we want similar behavior for shift as well?
- if (state != Gdk.ModifierType.CONTROL_MASK)
- get_view().unselect_all();
-
- // grab previously marked items
- previously_selected = new Gee.ArrayList<CheckerboardItem>();
- foreach (DataView view in get_view().get_selected())
- previously_selected.add((CheckerboardItem) view);
-
- layout.set_drag_select_origin((int) event.x, (int) event.y);
-
- return true;
- }
-
- // need to determine if the signal should be passed to the DnD handlers
- // Return true to block the DnD handler, false otherwise
-
- return get_view().get_selected_count() == 0;
- }
-
- protected override bool on_left_released(Gdk.EventButton event) {
- previously_selected = null;
-
- // if drag-selecting, stop here and do nothing else
- if (layout.is_drag_select_active()) {
- layout.clear_drag_select();
- anchor = cursor;
-
- return true;
- }
-
- // only interested in non-modified button releases
- if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0)
- return false;
-
- // if the item was activated in the double-click, report it now
- if (activated_item != null) {
- on_item_activated(activated_item, Activator.MOUSE, KeyboardModifiers(this));
- activated_item = null;
-
- return true;
- }
-
- CheckerboardItem item = get_item_at_pixel(event.x, event.y);
- if (item == null) {
- // released button on "dead" area
- return true;
- }
-
- if (cursor != item) {
- // user released mouse button after moving it off the initial item, or moved from dead
- // space onto one. either way, unselect everything
- get_view().unselect_all();
- } else {
- // the idea is, if a user single-clicks on an item with no modifiers, then all other items
- // should be deselected, however, if they single-click in order to drag one or more items,
- // they should remain selected, hence performing this here rather than on_left_click
- // (item may not be selected if an unimplemented modifier key was used)
- if (item.is_selected())
- get_view().unselect_all_but(item);
- }
-
- return true;
- }
-
- protected override bool on_right_click(Gdk.EventButton event) {
- // only interested in single-clicks for now
- if (event.type != Gdk.EventType.BUTTON_PRESS)
- return false;
-
- // get what's right-clicked upon
- CheckerboardItem item = get_item_at_pixel(event.x, event.y);
- if (item != null) {
- // mask out the modifiers we're interested in
- switch (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) {
- case Gdk.ModifierType.CONTROL_MASK:
- // chosen item is toggled
- Marker marker = get_view().mark(item);
- get_view().toggle_marked(marker);
- break;
-
- case Gdk.ModifierType.SHIFT_MASK:
- // TODO
- break;
-
- case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
- // TODO
- break;
-
- default:
- // if the item is already selected, proceed; if item is not selected, a bare right
- // click unselects everything else but it
- if (!item.is_selected()) {
- Marker all = get_view().start_marking();
- all.mark_many(get_view().get_selected());
-
- get_view().unselect_and_select_marked(all, get_view().mark(item));
- }
- break;
- }
- } else {
- // clicked in "dead" space, unselect everything
- get_view().unselect_all();
- }
-
- Gtk.Menu context_menu = get_context_menu();
- return popup_context_menu(context_menu, event);
- }
-
- protected virtual bool on_mouse_over(CheckerboardItem? item, int x, int y, Gdk.ModifierType mask) {
- if (item != null)
- layout.handle_mouse_motion(item, x, y, mask);
-
- // if hovering over the last hovered item, or both are null (nothing highlighted and
- // hovering over empty space), do nothing
- if (item == current_hovered_item)
- return true;
-
- // either something new is highlighted or now hovering over empty space, so dim old item
- if (current_hovered_item != null) {
- current_hovered_item.handle_mouse_leave();
- map_widget.unhighlight_position_marker(current_hovered_item);
- current_hovered_item = null;
- }
-
- // if over empty space, done
- if (item == null)
- return true;
-
- // brighten the new item
- current_hovered_item = item;
- current_hovered_item.handle_mouse_enter();
- map_widget.highlight_position_marker(item);
-
- return true;
- }
-
- protected override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
- // report what item the mouse is hovering over
- if (!on_mouse_over(get_item_at_pixel(x, y), x, y, mask))
- return false;
-
- // go no further if not drag-selecting
- if (!layout.is_drag_select_active())
- return false;
-
- // set the new endpoint of the drag selection
- layout.set_drag_select_endpoint(x, y);
-
- updated_selection_band();
-
- // if out of bounds, schedule a check to auto-scroll the viewport
- if (!autoscroll_scheduled
- && get_adjustment_relation(get_vadjustment(), y) != AdjustmentRelation.IN_RANGE) {
- Timeout.add(AUTOSCROLL_TICKS_MSEC, selection_autoscroll);
- autoscroll_scheduled = true;
- }
-
- // return true to stop a potential drag-and-drop operation
- return true;
- }
-
- private void updated_selection_band() {
- assert(layout.is_drag_select_active());
-
- // get all items inside the selection
- Gee.List<CheckerboardItem>? intersection = layout.items_in_selection_band();
- if (intersection == null)
- return;
-
- Marker to_unselect = get_view().start_marking();
- Marker to_select = get_view().start_marking();
-
- // mark all selected items to be unselected
- to_unselect.mark_many(get_view().get_selected());
-
- // except for the items that were selected before the drag began
- assert(previously_selected != null);
- to_unselect.unmark_many(previously_selected);
- to_select.mark_many(previously_selected);
-
- // toggle selection on everything in the intersection and update the cursor
- cursor = null;
-
- foreach (CheckerboardItem item in intersection) {
- if (to_select.toggle(item))
- to_unselect.unmark(item);
- else
- to_unselect.mark(item);
-
- if (cursor == null)
- cursor = item;
- }
-
- get_view().select_marked(to_select);
- get_view().unselect_marked(to_unselect);
- }
-
- private bool selection_autoscroll() {
- if (!layout.is_drag_select_active()) {
- autoscroll_scheduled = false;
-
- return false;
- }
-
- // as the viewport never scrolls horizontally, only interested in vertical
- Gtk.Adjustment vadj = get_vadjustment();
-
- int x, y;
- Gdk.ModifierType mask;
- get_event_source_pointer(out x, out y, out mask);
-
- int new_value = (int) vadj.get_value();
- switch (get_adjustment_relation(vadj, y)) {
- case AdjustmentRelation.BELOW:
- // pointer above window, scroll up
- new_value -= AUTOSCROLL_PIXELS;
- layout.set_drag_select_endpoint(x, new_value);
- break;
-
- case AdjustmentRelation.ABOVE:
- // pointer below window, scroll down, extend selection to bottom of page
- new_value += AUTOSCROLL_PIXELS;
- layout.set_drag_select_endpoint(x, new_value + (int) vadj.get_page_size());
- break;
-
- case AdjustmentRelation.IN_RANGE:
- autoscroll_scheduled = false;
-
- return false;
-
- default:
- warn_if_reached();
- break;
- }
-
- // It appears that in GTK+ 2.18, the adjustment is not clamped the way it was in 2.16.
- // This may have to do with how adjustments are different w/ scrollbars, that they're upper
- // clamp is upper - page_size ... either way, enforce these limits here
- vadj.set_value(new_value.clamp((int) vadj.get_lower(),
- (int) vadj.get_upper() - (int) vadj.get_page_size()));
-
- updated_selection_band();
-
- return true;
- }
-
- public void cursor_to_item(CheckerboardItem item) {
- assert(get_view().contains(item));
-
- cursor = item;
-
- if (!get_ctrl_pressed()) {
- get_view().unselect_all();
- Marker marker = get_view().mark(item);
- get_view().select_marked(marker);
- }
- layout.set_cursor(item);
- scroll_to_item(item);
- }
-
- public void move_cursor(CompassPoint point) {
- // if no items, nothing to do
- if (get_view().get_count() == 0)
- return;
-
- // if there is no better starting point, simply select the first and exit
- // The right half of the or is related to Bug #732334, the cursor might be non-null and still not
contained in
- // the view, if the user dragged a full screen Photo off screen
- if (cursor == null && layout.get_cursor() == null || cursor != null && !get_view().contains(cursor))
{
- CheckerboardItem item = layout.get_item_at_coordinate(0, 0);
- cursor_to_item(item);
- anchor = item;
-
- return;
- }
-
- if (cursor == null) {
- cursor = layout.get_cursor() as CheckerboardItem;
- }
-
- // move the cursor relative to the "first" item
- CheckerboardItem? item = layout.get_item_relative_to(cursor, point);
- if (item != null)
- cursor_to_item(item);
- }
-
- public void set_cursor(CheckerboardItem item) {
- Marker marker = get_view().mark(item);
- get_view().select_marked(marker);
-
- cursor = item;
- anchor = item;
- }
-
- public void select_between_items(CheckerboardItem item_start, CheckerboardItem item_end) {
- Marker marker = get_view().start_marking();
-
- bool passed_start = false;
- bool passed_end = false;
-
- foreach (DataObject object in get_view().get_all()) {
- CheckerboardItem item = (CheckerboardItem) object;
-
- if (item_start == item)
- passed_start = true;
-
- if (item_end == item)
- passed_end = true;
-
- if (passed_start || passed_end)
- marker.mark((DataView) object);
-
- if (passed_start && passed_end)
- break;
- }
-
- get_view().select_marked(marker);
- }
-
- public void select_anchor_to_cursor(uint state) {
- if (cursor == null || anchor == null)
- return;
-
- if (state == Gdk.ModifierType.SHIFT_MASK) {
- get_view().unselect_all();
- select_between_items(anchor, cursor);
- } else {
- anchor = cursor;
- }
- }
-
- protected virtual void set_display_titles(bool display) {
- get_view().freeze_notifications();
- get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, display);
- get_view().thaw_notifications();
- }
-
- protected virtual void set_display_comments(bool display) {
- get_view().freeze_notifications();
- get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, display);
- get_view().thaw_notifications();
- }
-}
-
-public abstract class SinglePhotoPage : Page {
- public const Gdk.InterpType FAST_INTERP = Gdk.InterpType.NEAREST;
- public const Gdk.InterpType QUALITY_INTERP = Gdk.InterpType.BILINEAR;
- public const int KEY_REPEAT_INTERVAL_MSEC = 200;
-
- public enum UpdateReason {
- NEW_PIXBUF,
- QUALITY_IMPROVEMENT,
- RESIZED_CANVAS
- }
-
- protected Gtk.DrawingArea canvas = new Gtk.DrawingArea();
- protected Gtk.Viewport viewport = new Gtk.Viewport(null, null);
-
- private bool scale_up_to_viewport;
- private TransitionClock transition_clock;
- private int transition_duration_msec = 0;
- private Cairo.Surface pixmap = null;
- private Cairo.Context pixmap_ctx = null;
- private Cairo.Context text_ctx = null;
- private Dimensions pixmap_dim = Dimensions();
- private Gdk.Pixbuf unscaled = null;
- private Dimensions max_dim = Dimensions();
- private Gdk.Pixbuf scaled = null;
- private Gdk.Pixbuf old_scaled = null; // previous scaled image
- private Gdk.Rectangle scaled_pos = Gdk.Rectangle();
- private ZoomState static_zoom_state;
- private bool zoom_high_quality = true;
- private ZoomState saved_zoom_state;
- private bool has_saved_zoom_state = false;
- private uint32 last_nav_key = 0;
-
- public SinglePhotoPage(string page_name, bool scale_up_to_viewport) {
- base(page_name);
-
- this.scale_up_to_viewport = scale_up_to_viewport;
-
- transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
-
- // With the current code automatically resizing the image to the viewport, scrollbars
- // should never be shown, but this may change if/when zooming is supported
- set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
-
- set_border_width(0);
- set_shadow_type(Gtk.ShadowType.NONE);
-
- viewport.set_shadow_type(Gtk.ShadowType.NONE);
- viewport.set_border_width(0);
- viewport.add(canvas);
-
- add(viewport);
-
- canvas.add_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.STRUCTURE_MASK
- | Gdk.EventMask.SUBSTRUCTURE_MASK);
-
- viewport.size_allocate.connect(on_viewport_resize);
- canvas.draw.connect(on_canvas_exposed);
-
- set_event_source(canvas);
- Config.Facade.get_instance().colors_changed.connect(on_colors_changed);
- }
-
- ~SinglePhotoPage() {
- Config.Facade.get_instance().colors_changed.disconnect(on_colors_changed);
- }
-
- public bool is_transition_in_progress() {
- return transition_clock.is_in_progress();
- }
-
- public void cancel_transition() {
- if (transition_clock.is_in_progress())
- transition_clock.cancel();
- }
-
- public void set_transition(string effect_id, int duration_msec) {
- cancel_transition();
-
- transition_clock = TransitionEffectsManager.get_instance().create_transition_clock(effect_id);
- if (transition_clock == null)
- transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
-
- transition_duration_msec = duration_msec;
- }
-
- // This method includes a call to pixmap_ctx.paint().
- private void render_zoomed_to_pixmap(ZoomState zoom_state) {
- assert(is_zoom_supported());
-
- Gdk.Rectangle view_rect = zoom_state.get_viewing_rectangle_wrt_content();
-
- Gdk.Pixbuf zoomed;
- if (get_zoom_buffer() != null) {
- zoomed = (zoom_high_quality) ? get_zoom_buffer().get_zoomed_image(zoom_state) :
- get_zoom_buffer().get_zoom_preview_image(zoom_state);
- } else {
- Gdk.Rectangle view_rect_proj = zoom_state.get_viewing_rectangle_projection(unscaled);
-
- Gdk.Pixbuf proj_subpixbuf = new Gdk.Pixbuf.subpixbuf(unscaled, view_rect_proj.x,
- view_rect_proj.y, view_rect_proj.width, view_rect_proj.height);
-
- zoomed = proj_subpixbuf.scale_simple(view_rect.width, view_rect.height,
- Gdk.InterpType.BILINEAR);
- }
-
- if (zoomed == null) {
- return;
- }
-
- int draw_x = (pixmap_dim.width - view_rect.width) / 2;
- draw_x = draw_x.clamp(0, int.MAX);
-
- int draw_y = (pixmap_dim.height - view_rect.height) / 2;
- draw_y = draw_y.clamp(0, int.MAX);
- paint_pixmap_with_background(pixmap_ctx, zoomed, draw_x, draw_y);
- }
-
- protected void on_interactive_zoom(ZoomState interactive_zoom_state) {
- assert(is_zoom_supported());
-
- set_source_color_from_string(pixmap_ctx, "#000");
- pixmap_ctx.paint();
-
- bool old_quality_setting = zoom_high_quality;
- zoom_high_quality = false;
- render_zoomed_to_pixmap(interactive_zoom_state);
- zoom_high_quality = old_quality_setting;
-
- canvas.queue_draw();
- }
-
- protected void on_interactive_pan(ZoomState interactive_zoom_state) {
- assert(is_zoom_supported());
-
- set_source_color_from_string(pixmap_ctx, "#000");
- pixmap_ctx.paint();
-
- bool old_quality_setting = zoom_high_quality;
- zoom_high_quality = true;
- render_zoomed_to_pixmap(interactive_zoom_state);
- zoom_high_quality = old_quality_setting;
-
- canvas.queue_draw();
- }
-
- protected virtual bool is_zoom_supported() {
- return false;
- }
-
- protected virtual void cancel_zoom() {
- if (pixmap != null) {
- set_source_color_from_string(pixmap_ctx, "#000");
- pixmap_ctx.paint();
- }
- }
-
- protected virtual void save_zoom_state() {
- saved_zoom_state = static_zoom_state;
- has_saved_zoom_state = true;
- }
-
- protected virtual void restore_zoom_state() {
- if (!has_saved_zoom_state)
- return;
-
- static_zoom_state = saved_zoom_state;
- repaint();
- has_saved_zoom_state = false;
- }
-
- protected virtual ZoomBuffer? get_zoom_buffer() {
- return null;
- }
-
- protected ZoomState get_saved_zoom_state() {
- return saved_zoom_state;
- }
-
- protected void set_zoom_state(ZoomState zoom_state) {
- assert(is_zoom_supported());
-
- static_zoom_state = zoom_state;
- }
-
- protected ZoomState get_zoom_state() {
- assert(is_zoom_supported());
-
- return static_zoom_state;
- }
-
- public override void switched_to() {
- base.switched_to();
-
- if (unscaled != null)
- repaint();
- }
-
- public override void set_container(Gtk.Window container) {
- base.set_container(container);
-
- // scrollbar policy in fullscreen mode needs to be auto/auto, else the pixbuf will shift
- // off the screen
- if (container is FullscreenWindow)
- set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
- }
-
- // max_dim represents the maximum size of the original pixbuf (i.e. pixbuf may be scaled and
- // the caller capable of producing larger ones depending on the viewport size). max_dim
- // is used when scale_up_to_viewport is set to true. Pass a Dimensions with no area if
- // max_dim should be ignored (i.e. scale_up_to_viewport is false).
- public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim, Direction? direction = null) {
- static_zoom_state = ZoomState(max_dim, pixmap_dim,
- static_zoom_state.get_interpolation_factor(),
- static_zoom_state.get_viewport_center());
-
- cancel_transition();
-
- this.unscaled = unscaled;
- this.max_dim = max_dim;
- this.old_scaled = scaled;
- scaled = null;
-
- // need to make sure this has happened
- canvas.realize();
-
- repaint(direction);
- }
-
- public void blank_display() {
- unscaled = null;
- max_dim = Dimensions();
- scaled = null;
- pixmap = null;
-
- // this has to have happened
- canvas.realize();
-
- // force a redraw
- invalidate_all();
- }
-
- public Cairo.Surface? get_surface() {
- return pixmap;
- }
-
- public Dimensions get_surface_dim() {
- return pixmap_dim;
- }
-
- public Cairo.Context get_cairo_context() {
- return pixmap_ctx;
- }
-
- public void paint_text(Pango.Layout pango_layout, int x, int y) {
- text_ctx.move_to(x, y);
- Pango.cairo_show_layout(text_ctx, pango_layout);
- }
-
- public Scaling get_canvas_scaling() {
- return (get_container() is FullscreenWindow) ? Scaling.for_screen(AppWindow.get_instance(),
scale_up_to_viewport)
- : Scaling.for_widget(viewport, scale_up_to_viewport);
- }
-
- public Gdk.Pixbuf? get_unscaled_pixbuf() {
- return unscaled;
- }
-
- public Gdk.Pixbuf? get_scaled_pixbuf() {
- return scaled;
- }
-
- // Returns a rectangle describing the pixbuf in relation to the canvas
- public Gdk.Rectangle get_scaled_pixbuf_position() {
- return scaled_pos;
- }
-
- public bool is_inside_pixbuf(int x, int y) {
- return coord_in_rectangle(x, y, scaled_pos);
- }
-
- public void invalidate(Gdk.Rectangle rect) {
- if (canvas.get_window() != null)
- canvas.get_window().invalidate_rect(rect, false);
- }
-
- public void invalidate_all() {
- if (canvas.get_window() != null)
- canvas.get_window().invalidate_rect(null, false);
- }
-
- private void on_viewport_resize() {
- // do fast repaints while resizing
- internal_repaint(true, null);
- }
-
- protected override void on_resize_finished(Gdk.Rectangle rect) {
- base.on_resize_finished(rect);
-
- // when the resize is completed, do a high-quality repaint
- repaint();
- }
-
- private bool on_canvas_exposed(Cairo.Context exposed_ctx) {
- // draw pixmap onto canvas unless it's not been instantiated, in which case draw black
- // (so either old image or contents of another page is not left on screen)
- if (pixmap != null)
- exposed_ctx.set_source_surface(pixmap, 0, 0);
- else
- set_source_color_from_string(exposed_ctx, "#000");
-
- exposed_ctx.rectangle(0, 0, get_allocated_width(), get_allocated_height());
- exposed_ctx.paint();
-
- return true;
- }
-
- protected virtual void new_surface(Cairo.Context ctx, Dimensions ctx_dim) {
- }
-
- protected virtual void updated_pixbuf(Gdk.Pixbuf pixbuf, UpdateReason reason, Dimensions old_dim) {
- }
-
- protected virtual void paint(Cairo.Context ctx, Dimensions ctx_dim) {
- if (is_zoom_supported() && (!static_zoom_state.is_default())) {
- set_source_color_from_string(ctx, "#000");
- ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
- ctx.fill();
-
- render_zoomed_to_pixmap(static_zoom_state);
- } else if (!transition_clock.paint(ctx, ctx_dim.width, ctx_dim.height)) {
- // transition is not running, so paint the full image on a black background
- set_source_color_from_string(ctx, "#000");
-
- ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
- ctx.fill();
-
- paint_pixmap_with_background(ctx, scaled, scaled_pos.x, scaled_pos.y);
- }
- }
-
- private void repaint_pixmap() {
- if (pixmap_ctx == null)
- return;
-
- paint(pixmap_ctx, pixmap_dim);
- invalidate_all();
- }
-
- public void repaint(Direction? direction = null) {
- internal_repaint(false, direction);
- }
-
- private void internal_repaint(bool fast, Direction? direction) {
- // if not in view, assume a full repaint needed in future but do nothing more
- if (!is_in_view()) {
- pixmap = null;
- scaled = null;
-
- return;
- }
-
- // no image or window, no painting
- if (unscaled == null || canvas.get_window() == null)
- return;
-
- Gtk.Allocation allocation;
- viewport.get_allocation(out allocation);
-
- int width = allocation.width;
- int height = allocation.height;
-
- if (width <= 0 || height <= 0)
- return;
-
- bool new_pixbuf = (scaled == null);
-
- // save if reporting an image being rescaled
- Dimensions old_scaled_dim = Dimensions.for_rectangle(scaled_pos);
- Gdk.Rectangle old_scaled_pos = scaled_pos;
-
- // attempt to reuse pixmap
- if (pixmap_dim.width != width || pixmap_dim.height != height)
- pixmap = null;
-
- // if necessary, create a pixmap as large as the entire viewport
- bool new_pixmap = false;
- if (pixmap == null) {
- init_pixmap(width, height);
- new_pixmap = true;
- }
-
- if (new_pixbuf || new_pixmap) {
- Dimensions unscaled_dim = Dimensions.for_pixbuf(unscaled);
-
- // determine scaled size of pixbuf ... if a max dimensions is set and not scaling up,
- // respect it
- Dimensions scaled_dim = Dimensions();
- if (!scale_up_to_viewport && max_dim.has_area() && max_dim.width < width && max_dim.height <
height)
- scaled_dim = max_dim;
- else
- scaled_dim = unscaled_dim.get_scaled_proportional(pixmap_dim);
-
- assert(width >= scaled_dim.width);
- assert(height >= scaled_dim.height);
-
- // center pixbuf on the canvas
- scaled_pos.x = (width - scaled_dim.width) / 2;
- scaled_pos.y = (height - scaled_dim.height) / 2;
- scaled_pos.width = scaled_dim.width;
- scaled_pos.height = scaled_dim.height;
- }
-
- Gdk.InterpType interp = (fast) ? FAST_INTERP : QUALITY_INTERP;
-
- // rescale if canvas rescaled or better quality is requested
- if (scaled == null) {
- scaled = resize_pixbuf(unscaled, Dimensions.for_rectangle(scaled_pos), interp);
-
- UpdateReason reason = UpdateReason.RESIZED_CANVAS;
- if (new_pixbuf)
- reason = UpdateReason.NEW_PIXBUF;
- else if (!new_pixmap && interp == QUALITY_INTERP)
- reason = UpdateReason.QUALITY_IMPROVEMENT;
-
- static_zoom_state = ZoomState(max_dim, pixmap_dim,
- static_zoom_state.get_interpolation_factor(),
- static_zoom_state.get_viewport_center());
-
- updated_pixbuf(scaled, reason, old_scaled_dim);
- }
-
- zoom_high_quality = !fast;
-
- if (direction != null && !transition_clock.is_in_progress()) {
- Spit.Transitions.Visuals visuals = new Spit.Transitions.Visuals(old_scaled,
- old_scaled_pos, scaled, scaled_pos, parse_color("#000"));
-
- transition_clock.start(visuals, direction.to_transition_direction(), transition_duration_msec,
- repaint_pixmap);
- }
-
- if (!transition_clock.is_in_progress())
- repaint_pixmap();
- }
-
- private void init_pixmap(int width, int height) {
- assert(unscaled != null);
- assert(canvas.get_window() != null);
-
- // Cairo backing surface (manual double-buffering)
- pixmap = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
- pixmap_dim = Dimensions(width, height);
-
- // Cairo context for drawing on the pixmap
- pixmap_ctx = new Cairo.Context(pixmap);
-
- // need a new pixbuf to fit this scale
- scaled = null;
-
- // Cairo context for drawing text on the pixmap
- text_ctx = new Cairo.Context(pixmap);
- set_source_color_from_string(text_ctx, "#fff");
-
-
- // no need to resize canvas, viewport does that automatically
-
- new_surface(pixmap_ctx, pixmap_dim);
- }
-
- protected override bool on_context_keypress() {
- return popup_context_menu(get_page_context_menu());
- }
-
- protected virtual void on_previous_photo() {
- }
-
- protected virtual void on_next_photo() {
- }
-
- public override bool key_press_event(Gdk.EventKey event) {
- // if the user holds the arrow keys down, we will receive a steady stream of key press
- // events for an operation that isn't designed for a rapid succession of output ...
- // we staunch the supply of new photos to under a quarter second (#533)
- bool nav_ok = (event.time - last_nav_key) > KEY_REPEAT_INTERVAL_MSEC;
-
- bool handled = true;
- switch (Gdk.keyval_name(event.keyval)) {
- case "Left":
- case "KP_Left":
- case "BackSpace":
- if (nav_ok) {
- on_previous_photo();
- last_nav_key = event.time;
- }
- break;
-
- case "Right":
- case "KP_Right":
- case "space":
- if (nav_ok) {
- on_next_photo();
- last_nav_key = event.time;
- }
- break;
-
- default:
- handled = false;
- break;
- }
-
- if (handled)
- return true;
-
- return (base.key_press_event != null) ? base.key_press_event(event) : true;
- }
-
- private void on_colors_changed() {
- invalidate_transparent_background();
- repaint();
- }
-}
-
-//
-// DragAndDropHandler attaches signals to a Page to properly handle drag-and-drop requests for the
-// Page as a DnD Source. (DnD Destination handling is handled by the appropriate AppWindow, i.e.
-// LibraryWindow and DirectWindow). Assumes the Page's ViewCollection holds MediaSources.
-//
-public class DragAndDropHandler {
- private enum TargetType {
- XDS,
- MEDIA_LIST
- }
-
- private const Gtk.TargetEntry[] SOURCE_TARGET_ENTRIES = {
- { "XdndDirectSave0", Gtk.TargetFlags.OTHER_APP, TargetType.XDS },
- { "shotwell/media-id-atom", Gtk.TargetFlags.SAME_APP, TargetType.MEDIA_LIST }
- };
-
- private static Gdk.Atom? XDS_ATOM = null;
- private static Gdk.Atom? TEXT_ATOM = null;
- private static uint8[]? XDS_FAKE_TARGET = null;
-
- private weak Page page;
- private Gtk.Widget event_source;
- private File? drag_destination = null;
- private ExporterUI exporter = null;
-
- public DragAndDropHandler(Page page) {
- this.page = page;
- this.event_source = page.get_event_source();
- assert(event_source != null);
- assert(event_source.get_has_window());
-
- // Need to do this because static member variables are not properly handled
- if (XDS_ATOM == null)
- XDS_ATOM = Gdk.Atom.intern_static_string("XdndDirectSave0");
-
- if (TEXT_ATOM == null)
- TEXT_ATOM = Gdk.Atom.intern_static_string("text/plain");
-
- if (XDS_FAKE_TARGET == null)
- XDS_FAKE_TARGET = string_to_uchar_array("shotwell.txt");
-
- // register what's available on this DnD Source
- Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, SOURCE_TARGET_ENTRIES,
- Gdk.DragAction.COPY);
-
- // attach to the event source's DnD signals, not the Page's, which is a NO_WINDOW widget
- // and does not emit them
- event_source.drag_begin.connect(on_drag_begin);
- event_source.drag_data_get.connect(on_drag_data_get);
- event_source.drag_end.connect(on_drag_end);
- event_source.drag_failed.connect(on_drag_failed);
- }
-
- ~DragAndDropHandler() {
- if (event_source != null) {
- event_source.drag_begin.disconnect(on_drag_begin);
- event_source.drag_data_get.disconnect(on_drag_data_get);
- event_source.drag_end.disconnect(on_drag_end);
- event_source.drag_failed.disconnect(on_drag_failed);
- }
-
- page = null;
- event_source = null;
- }
-
- private void on_drag_begin(Gdk.DragContext context) {
- debug("on_drag_begin (%s)", page.get_page_name());
-
- if (page == null || page.get_view().get_selected_count() == 0 || exporter != null)
- return;
-
- drag_destination = null;
-
- // use the first media item as the icon
- ThumbnailSource thumb = (ThumbnailSource) page.get_view().get_selected_at(0).get_source();
-
- try {
- Gdk.Pixbuf icon = thumb.get_thumbnail(AppWindow.DND_ICON_SCALE);
- Gtk.drag_source_set_icon_pixbuf(event_source, icon);
- } catch (Error err) {
- warning("Unable to fetch icon for drag-and-drop from %s: %s", thumb.to_string(),
- err.message);
- }
-
- // set the XDS property to indicate an XDS save is available
-#if VALA_0_20
- Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE,
- XDS_FAKE_TARGET, 1);
-#else
- Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE,
- XDS_FAKE_TARGET);
-#endif
- }
-
- private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
- uint target_type, uint time) {
- debug("on_drag_data_get (%s)", page.get_page_name());
-
- if (page == null || page.get_view().get_selected_count() == 0)
- return;
-
- switch (target_type) {
- case TargetType.XDS:
- // Fetch the XDS property that has been set with the destination path
- uchar[] data = new uchar[4096];
- Gdk.Atom actual_type;
- int actual_format = 0;
- bool fetched = Gdk.property_get(context.get_source_window(), XDS_ATOM, TEXT_ATOM,
- 0, data.length, 0, out actual_type, out actual_format, out data);
-
- // the destination path is actually for our XDS_FAKE_TARGET, use its parent
- // to determine where the file(s) should go
- if (fetched && data != null && data.length > 0)
- drag_destination = File.new_for_uri(uchar_array_to_string(data)).get_parent();
-
- debug("on_drag_data_get (%s): %s", page.get_page_name(),
- (drag_destination != null) ? drag_destination.get_path() : "(no path)");
-
- // Set the property to "S" for Success or "E" for Error
- selection_data.set(XDS_ATOM, 8,
- string_to_uchar_array((drag_destination != null) ? "S" : "E"));
- break;
-
- case TargetType.MEDIA_LIST:
- Gee.Collection<MediaSource> sources =
- (Gee.Collection<MediaSource>) page.get_view().get_selected_sources();
-
- // convert the selected media sources to Gdk.Atom-encoded sourceID strings for
- // internal drag-and-drop
- selection_data.set(Gdk.Atom.intern_static_string("SourceIDAtom"), (int) sizeof(Gdk.Atom),
- serialize_media_sources(sources));
- break;
-
- default:
- warning("on_drag_data_get (%s): unknown target type %u", page.get_page_name(),
- target_type);
- break;
- }
- }
-
- private void on_drag_end() {
- debug("on_drag_end (%s)", page.get_page_name());
-
- if (page == null || page.get_view().get_selected_count() == 0 || drag_destination == null
- || exporter != null) {
- return;
- }
-
- debug("Exporting to %s", drag_destination.get_path());
-
- // drag-and-drop export doesn't pop up an export dialog, so use what are likely the
- // most common export settings (the current -- or "working" -- file format, with
- // all transformations applied, at the image's original size).
- if (drag_destination.get_path() != null) {
- exporter = new ExporterUI(new Exporter(
- (Gee.Collection<Photo>) page.get_view().get_selected_sources(),
- drag_destination, Scaling.for_original(), ExportFormatParameters.current()));
- exporter.export(on_export_completed);
- } else {
- AppWindow.error_message(_("Photos cannot be exported to this directory."));
- }
-
- drag_destination = null;
- }
-
- private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
- debug("on_drag_failed (%s): %d", page.get_page_name(), (int) drag_result);
-
- if (page == null)
- return false;
-
- drag_destination = null;
-
- return false;
- }
-
- private void on_export_completed() {
- exporter = null;
- }
-
-}
diff --git a/src/PageMessagePane.vala b/src/PageMessagePane.vala
new file mode 100644
index 00000000..a5c5eee6
--- /dev/null
+++ b/src/PageMessagePane.vala
@@ -0,0 +1,19 @@
+/* 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.
+ */
+
+[GtkTemplate (ui = "/org/gnome/Shotwell/ui/message_pane.ui")]
+private class PageMessagePane : Gtk.Box {
+ [GtkChild]
+ public Gtk.Label label;
+
+ [GtkChild]
+ public Gtk.Image icon_image;
+
+ public PageMessagePane() {
+ Object();
+ }
+}
+
diff --git a/src/SinglePhotoPage.vala b/src/SinglePhotoPage.vala
new file mode 100644
index 00000000..0985eb1c
--- /dev/null
+++ b/src/SinglePhotoPage.vala
@@ -0,0 +1,529 @@
+/* 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.
+ */
+
+public abstract class SinglePhotoPage : Page {
+ public const Gdk.InterpType FAST_INTERP = Gdk.InterpType.NEAREST;
+ public const Gdk.InterpType QUALITY_INTERP = Gdk.InterpType.BILINEAR;
+ public const int KEY_REPEAT_INTERVAL_MSEC = 200;
+
+ public enum UpdateReason {
+ NEW_PIXBUF,
+ QUALITY_IMPROVEMENT,
+ RESIZED_CANVAS
+ }
+
+ protected Gtk.DrawingArea canvas = new Gtk.DrawingArea();
+ protected Gtk.Viewport viewport = new Gtk.Viewport(null, null);
+
+ private bool scale_up_to_viewport;
+ private TransitionClock transition_clock;
+ private int transition_duration_msec = 0;
+ private Cairo.Surface pixmap = null;
+ private Cairo.Context pixmap_ctx = null;
+ private Cairo.Context text_ctx = null;
+ private Dimensions pixmap_dim = Dimensions();
+ private Gdk.Pixbuf unscaled = null;
+ private Dimensions max_dim = Dimensions();
+ private Gdk.Pixbuf scaled = null;
+ private Gdk.Pixbuf old_scaled = null; // previous scaled image
+ private Gdk.Rectangle scaled_pos = Gdk.Rectangle();
+ private ZoomState static_zoom_state;
+ private bool zoom_high_quality = true;
+ private ZoomState saved_zoom_state;
+ private bool has_saved_zoom_state = false;
+ private uint32 last_nav_key = 0;
+
+ public SinglePhotoPage(string page_name, bool scale_up_to_viewport) {
+ base(page_name);
+
+ this.scale_up_to_viewport = scale_up_to_viewport;
+
+ transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
+
+ // With the current code automatically resizing the image to the viewport, scrollbars
+ // should never be shown, but this may change if/when zooming is supported
+ set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+
+ set_border_width(0);
+ set_shadow_type(Gtk.ShadowType.NONE);
+
+ viewport.set_shadow_type(Gtk.ShadowType.NONE);
+ viewport.set_border_width(0);
+ viewport.add(canvas);
+
+ add(viewport);
+
+ canvas.add_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.STRUCTURE_MASK
+ | Gdk.EventMask.SUBSTRUCTURE_MASK);
+
+ viewport.size_allocate.connect(on_viewport_resize);
+ canvas.draw.connect(on_canvas_exposed);
+
+ set_event_source(canvas);
+ Config.Facade.get_instance().colors_changed.connect(on_colors_changed);
+ }
+
+ ~SinglePhotoPage() {
+ Config.Facade.get_instance().colors_changed.disconnect(on_colors_changed);
+ }
+
+ public bool is_transition_in_progress() {
+ return transition_clock.is_in_progress();
+ }
+
+ public void cancel_transition() {
+ if (transition_clock.is_in_progress())
+ transition_clock.cancel();
+ }
+
+ public void set_transition(string effect_id, int duration_msec) {
+ cancel_transition();
+
+ transition_clock = TransitionEffectsManager.get_instance().create_transition_clock(effect_id);
+ if (transition_clock == null)
+ transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
+
+ transition_duration_msec = duration_msec;
+ }
+
+ // This method includes a call to pixmap_ctx.paint().
+ private void render_zoomed_to_pixmap(ZoomState zoom_state) {
+ assert(is_zoom_supported());
+
+ Gdk.Rectangle view_rect = zoom_state.get_viewing_rectangle_wrt_content();
+
+ Gdk.Pixbuf zoomed;
+ if (get_zoom_buffer() != null) {
+ zoomed = (zoom_high_quality) ? get_zoom_buffer().get_zoomed_image(zoom_state) :
+ get_zoom_buffer().get_zoom_preview_image(zoom_state);
+ } else {
+ Gdk.Rectangle view_rect_proj = zoom_state.get_viewing_rectangle_projection(unscaled);
+
+ Gdk.Pixbuf proj_subpixbuf = new Gdk.Pixbuf.subpixbuf(unscaled, view_rect_proj.x,
+ view_rect_proj.y, view_rect_proj.width, view_rect_proj.height);
+
+ zoomed = proj_subpixbuf.scale_simple(view_rect.width, view_rect.height,
+ Gdk.InterpType.BILINEAR);
+ }
+
+ if (zoomed == null) {
+ return;
+ }
+
+ int draw_x = (pixmap_dim.width - view_rect.width) / 2;
+ draw_x = draw_x.clamp(0, int.MAX);
+
+ int draw_y = (pixmap_dim.height - view_rect.height) / 2;
+ draw_y = draw_y.clamp(0, int.MAX);
+ paint_pixmap_with_background(pixmap_ctx, zoomed, draw_x, draw_y);
+ }
+
+ protected void on_interactive_zoom(ZoomState interactive_zoom_state) {
+ assert(is_zoom_supported());
+
+ set_source_color_from_string(pixmap_ctx, "#000");
+ pixmap_ctx.paint();
+
+ bool old_quality_setting = zoom_high_quality;
+ zoom_high_quality = false;
+ render_zoomed_to_pixmap(interactive_zoom_state);
+ zoom_high_quality = old_quality_setting;
+
+ canvas.queue_draw();
+ }
+
+ protected void on_interactive_pan(ZoomState interactive_zoom_state) {
+ assert(is_zoom_supported());
+
+ set_source_color_from_string(pixmap_ctx, "#000");
+ pixmap_ctx.paint();
+
+ bool old_quality_setting = zoom_high_quality;
+ zoom_high_quality = true;
+ render_zoomed_to_pixmap(interactive_zoom_state);
+ zoom_high_quality = old_quality_setting;
+
+ canvas.queue_draw();
+ }
+
+ protected virtual bool is_zoom_supported() {
+ return false;
+ }
+
+ protected virtual void cancel_zoom() {
+ if (pixmap != null) {
+ set_source_color_from_string(pixmap_ctx, "#000");
+ pixmap_ctx.paint();
+ }
+ }
+
+ protected virtual void save_zoom_state() {
+ saved_zoom_state = static_zoom_state;
+ has_saved_zoom_state = true;
+ }
+
+ protected virtual void restore_zoom_state() {
+ if (!has_saved_zoom_state)
+ return;
+
+ static_zoom_state = saved_zoom_state;
+ repaint();
+ has_saved_zoom_state = false;
+ }
+
+ protected virtual ZoomBuffer? get_zoom_buffer() {
+ return null;
+ }
+
+ protected ZoomState get_saved_zoom_state() {
+ return saved_zoom_state;
+ }
+
+ protected void set_zoom_state(ZoomState zoom_state) {
+ assert(is_zoom_supported());
+
+ static_zoom_state = zoom_state;
+ }
+
+ protected ZoomState get_zoom_state() {
+ assert(is_zoom_supported());
+
+ return static_zoom_state;
+ }
+
+ public override void switched_to() {
+ base.switched_to();
+
+ if (unscaled != null)
+ repaint();
+ }
+
+ public override void set_container(Gtk.Window container) {
+ base.set_container(container);
+
+ // scrollbar policy in fullscreen mode needs to be auto/auto, else the pixbuf will shift
+ // off the screen
+ if (container is FullscreenWindow)
+ set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+ }
+
+ // max_dim represents the maximum size of the original pixbuf (i.e. pixbuf may be scaled and
+ // the caller capable of producing larger ones depending on the viewport size). max_dim
+ // is used when scale_up_to_viewport is set to true. Pass a Dimensions with no area if
+ // max_dim should be ignored (i.e. scale_up_to_viewport is false).
+ public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim, Direction? direction = null) {
+ static_zoom_state = ZoomState(max_dim, pixmap_dim,
+ static_zoom_state.get_interpolation_factor(),
+ static_zoom_state.get_viewport_center());
+
+ cancel_transition();
+
+ this.unscaled = unscaled;
+ this.max_dim = max_dim;
+ this.old_scaled = scaled;
+ scaled = null;
+
+ // need to make sure this has happened
+ canvas.realize();
+
+ repaint(direction);
+ }
+
+ public void blank_display() {
+ unscaled = null;
+ max_dim = Dimensions();
+ scaled = null;
+ pixmap = null;
+
+ // this has to have happened
+ canvas.realize();
+
+ // force a redraw
+ invalidate_all();
+ }
+
+ public Cairo.Surface? get_surface() {
+ return pixmap;
+ }
+
+ public Dimensions get_surface_dim() {
+ return pixmap_dim;
+ }
+
+ public Cairo.Context get_cairo_context() {
+ return pixmap_ctx;
+ }
+
+ public void paint_text(Pango.Layout pango_layout, int x, int y) {
+ text_ctx.move_to(x, y);
+ Pango.cairo_show_layout(text_ctx, pango_layout);
+ }
+
+ public Scaling get_canvas_scaling() {
+ return (get_container() is FullscreenWindow) ? Scaling.for_screen(AppWindow.get_instance(),
scale_up_to_viewport)
+ : Scaling.for_widget(viewport, scale_up_to_viewport);
+ }
+
+ public Gdk.Pixbuf? get_unscaled_pixbuf() {
+ return unscaled;
+ }
+
+ public Gdk.Pixbuf? get_scaled_pixbuf() {
+ return scaled;
+ }
+
+ // Returns a rectangle describing the pixbuf in relation to the canvas
+ public Gdk.Rectangle get_scaled_pixbuf_position() {
+ return scaled_pos;
+ }
+
+ public bool is_inside_pixbuf(int x, int y) {
+ return coord_in_rectangle(x, y, scaled_pos);
+ }
+
+ public void invalidate(Gdk.Rectangle rect) {
+ if (canvas.get_window() != null)
+ canvas.get_window().invalidate_rect(rect, false);
+ }
+
+ public void invalidate_all() {
+ if (canvas.get_window() != null)
+ canvas.get_window().invalidate_rect(null, false);
+ }
+
+ private void on_viewport_resize() {
+ // do fast repaints while resizing
+ internal_repaint(true, null);
+ }
+
+ protected override void on_resize_finished(Gdk.Rectangle rect) {
+ base.on_resize_finished(rect);
+
+ // when the resize is completed, do a high-quality repaint
+ repaint();
+ }
+
+ private bool on_canvas_exposed(Cairo.Context exposed_ctx) {
+ // draw pixmap onto canvas unless it's not been instantiated, in which case draw black
+ // (so either old image or contents of another page is not left on screen)
+ if (pixmap != null)
+ exposed_ctx.set_source_surface(pixmap, 0, 0);
+ else
+ set_source_color_from_string(exposed_ctx, "#000");
+
+ exposed_ctx.rectangle(0, 0, get_allocated_width(), get_allocated_height());
+ exposed_ctx.paint();
+
+ return true;
+ }
+
+ protected virtual void new_surface(Cairo.Context ctx, Dimensions ctx_dim) {
+ }
+
+ protected virtual void updated_pixbuf(Gdk.Pixbuf pixbuf, UpdateReason reason, Dimensions old_dim) {
+ }
+
+ protected virtual void paint(Cairo.Context ctx, Dimensions ctx_dim) {
+ if (is_zoom_supported() && (!static_zoom_state.is_default())) {
+ set_source_color_from_string(ctx, "#000");
+ ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
+ ctx.fill();
+
+ render_zoomed_to_pixmap(static_zoom_state);
+ } else if (!transition_clock.paint(ctx, ctx_dim.width, ctx_dim.height)) {
+ // transition is not running, so paint the full image on a black background
+ set_source_color_from_string(ctx, "#000");
+
+ ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
+ ctx.fill();
+
+ paint_pixmap_with_background(ctx, scaled, scaled_pos.x, scaled_pos.y);
+ }
+ }
+
+ private void repaint_pixmap() {
+ if (pixmap_ctx == null)
+ return;
+
+ paint(pixmap_ctx, pixmap_dim);
+ invalidate_all();
+ }
+
+ public void repaint(Direction? direction = null) {
+ internal_repaint(false, direction);
+ }
+
+ private void internal_repaint(bool fast, Direction? direction) {
+ // if not in view, assume a full repaint needed in future but do nothing more
+ if (!is_in_view()) {
+ pixmap = null;
+ scaled = null;
+
+ return;
+ }
+
+ // no image or window, no painting
+ if (unscaled == null || canvas.get_window() == null)
+ return;
+
+ Gtk.Allocation allocation;
+ viewport.get_allocation(out allocation);
+
+ int width = allocation.width;
+ int height = allocation.height;
+
+ if (width <= 0 || height <= 0)
+ return;
+
+ bool new_pixbuf = (scaled == null);
+
+ // save if reporting an image being rescaled
+ Dimensions old_scaled_dim = Dimensions.for_rectangle(scaled_pos);
+ Gdk.Rectangle old_scaled_pos = scaled_pos;
+
+ // attempt to reuse pixmap
+ if (pixmap_dim.width != width || pixmap_dim.height != height)
+ pixmap = null;
+
+ // if necessary, create a pixmap as large as the entire viewport
+ bool new_pixmap = false;
+ if (pixmap == null) {
+ init_pixmap(width, height);
+ new_pixmap = true;
+ }
+
+ if (new_pixbuf || new_pixmap) {
+ Dimensions unscaled_dim = Dimensions.for_pixbuf(unscaled);
+
+ // determine scaled size of pixbuf ... if a max dimensions is set and not scaling up,
+ // respect it
+ Dimensions scaled_dim = Dimensions();
+ if (!scale_up_to_viewport && max_dim.has_area() && max_dim.width < width && max_dim.height <
height)
+ scaled_dim = max_dim;
+ else
+ scaled_dim = unscaled_dim.get_scaled_proportional(pixmap_dim);
+
+ assert(width >= scaled_dim.width);
+ assert(height >= scaled_dim.height);
+
+ // center pixbuf on the canvas
+ scaled_pos.x = (width - scaled_dim.width) / 2;
+ scaled_pos.y = (height - scaled_dim.height) / 2;
+ scaled_pos.width = scaled_dim.width;
+ scaled_pos.height = scaled_dim.height;
+ }
+
+ Gdk.InterpType interp = (fast) ? FAST_INTERP : QUALITY_INTERP;
+
+ // rescale if canvas rescaled or better quality is requested
+ if (scaled == null) {
+ scaled = resize_pixbuf(unscaled, Dimensions.for_rectangle(scaled_pos), interp);
+
+ UpdateReason reason = UpdateReason.RESIZED_CANVAS;
+ if (new_pixbuf)
+ reason = UpdateReason.NEW_PIXBUF;
+ else if (!new_pixmap && interp == QUALITY_INTERP)
+ reason = UpdateReason.QUALITY_IMPROVEMENT;
+
+ static_zoom_state = ZoomState(max_dim, pixmap_dim,
+ static_zoom_state.get_interpolation_factor(),
+ static_zoom_state.get_viewport_center());
+
+ updated_pixbuf(scaled, reason, old_scaled_dim);
+ }
+
+ zoom_high_quality = !fast;
+
+ if (direction != null && !transition_clock.is_in_progress()) {
+ Spit.Transitions.Visuals visuals = new Spit.Transitions.Visuals(old_scaled,
+ old_scaled_pos, scaled, scaled_pos, parse_color("#000"));
+
+ transition_clock.start(visuals, direction.to_transition_direction(), transition_duration_msec,
+ repaint_pixmap);
+ }
+
+ if (!transition_clock.is_in_progress())
+ repaint_pixmap();
+ }
+
+ private void init_pixmap(int width, int height) {
+ assert(unscaled != null);
+ assert(canvas.get_window() != null);
+
+ // Cairo backing surface (manual double-buffering)
+ pixmap = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
+ pixmap_dim = Dimensions(width, height);
+
+ // Cairo context for drawing on the pixmap
+ pixmap_ctx = new Cairo.Context(pixmap);
+
+ // need a new pixbuf to fit this scale
+ scaled = null;
+
+ // Cairo context for drawing text on the pixmap
+ text_ctx = new Cairo.Context(pixmap);
+ set_source_color_from_string(text_ctx, "#fff");
+
+
+ // no need to resize canvas, viewport does that automatically
+
+ new_surface(pixmap_ctx, pixmap_dim);
+ }
+
+ protected override bool on_context_keypress() {
+ return popup_context_menu(get_page_context_menu());
+ }
+
+ protected virtual void on_previous_photo() {
+ }
+
+ protected virtual void on_next_photo() {
+ }
+
+ public override bool key_press_event(Gdk.EventKey event) {
+ // if the user holds the arrow keys down, we will receive a steady stream of key press
+ // events for an operation that isn't designed for a rapid succession of output ...
+ // we staunch the supply of new photos to under a quarter second (#533)
+ bool nav_ok = (event.time - last_nav_key) > KEY_REPEAT_INTERVAL_MSEC;
+
+ bool handled = true;
+ switch (Gdk.keyval_name(event.keyval)) {
+ case "Left":
+ case "KP_Left":
+ case "BackSpace":
+ if (nav_ok) {
+ on_previous_photo();
+ last_nav_key = event.time;
+ }
+ break;
+
+ case "Right":
+ case "KP_Right":
+ case "space":
+ if (nav_ok) {
+ on_next_photo();
+ last_nav_key = event.time;
+ }
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled)
+ return true;
+
+ return (base.key_press_event != null) ? base.key_press_event(event) : true;
+ }
+
+ private void on_colors_changed() {
+ invalidate_transparent_background();
+ repaint();
+ }
+}
+
+
diff --git a/src/meson.build b/src/meson.build
index 3844a5b3..d5bbeb04 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -182,6 +182,10 @@ executable('shotwell',
'CheckerboardLayout.vala',
'PhotoPage.vala',
'Page.vala',
+ 'SinglePhotoPage.vala',
+ 'CheckerboardPage.vala',
+ 'DragAndDropHandler.vala',
+ 'PageMessagePane.vala',
'SortedList.vala',
'Dimensions.vala',
'Box.vala',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]