From d443a3c2509889533ca812c163056bace396b586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Wed, 14 Jun 2023 20:35:58 +0200 Subject: New upstream version 0.32.1 --- src/Page.vala | 1557 +-------------------------------------------------------- 1 file changed, 20 insertions(+), 1537 deletions(-) (limited to 'src/Page.vala') diff --git a/src/Page.vala b/src/Page.vala index 65b263b..6b07568 100644 --- a/src/Page.vala +++ b/src/Page.vala @@ -13,13 +13,13 @@ public class InjectionGroup { } public string name; public string action; - public string? accellerator; + public string? accelerator; public ItemType kind; - public Element(string name, string? action, string? accellerator, ItemType kind) { + public Element(string name, string? action, string? accelerator, ItemType kind) { this.name = name; this.action = action != null ? action : name; - this.accellerator = accellerator; + this.accelerator = accelerator; this.kind = kind; } } @@ -40,8 +40,8 @@ public class InjectionGroup { return elements; } - public void add_menu_item(string name, string? action = null, string? accellerator = null) { - elements.add(new Element(name, action, accellerator, Element.ItemType.MENUITEM)); + public void add_menu_item(string name, string? action = null, string? accelerator = null) { + elements.add(new Element(name, action, accelerator, Element.ItemType.MENUITEM)); } public void add_menu(string name, string? action = null) { @@ -68,7 +68,6 @@ public abstract class Page : Gtk.ScrolledWindow { private string toolbar_path; private Gdk.Rectangle last_position = Gdk.Rectangle(); private Gtk.Widget event_source = null; - private bool dnd_enabled = false; private ulong last_configure_ms = 0; private bool report_move_finished = false; private bool report_resize_finished = false; @@ -85,6 +84,9 @@ public abstract class Page : Gtk.ScrolledWindow { private int cursor_hide_time_cached = 0; private bool are_actions_attached = false; private OneShotScheduler? update_actions_scheduler = null; + + protected double wheel_factor = 0.0; + protected double modified_wheel_factor = 1.0; protected Page(string page_name) { this.page_name = page_name; @@ -193,8 +195,6 @@ public abstract class Page : Gtk.ScrolledWindow { event_source.leave_notify_event.disconnect(on_leave_notify_event); event_source.scroll_event.disconnect(on_mousewheel_internal); - disable_drag_source(); - event_source = null; } @@ -230,10 +230,10 @@ public abstract class Page : Gtk.ScrolledWindow { case InjectionGroup.Element.ItemType.MENUITEM: var item = new GLib.MenuItem (element.name, "win." + element.action); - if (element.accellerator != null) { + if (element.accelerator != null) { item.set_attribute ("accel", "s", - element.accellerator); + element.accelerator); } menu.append_item (item); @@ -592,76 +592,6 @@ public abstract class Page : Gtk.ScrolledWindow { protected virtual void update_actions(int selected_count, int count) { } - // This method enables drag-and-drop on the event source and routes its events through this - // object - public void enable_drag_source(Gdk.DragAction actions, Gtk.TargetEntry[] source_target_entries) { - if (dnd_enabled) - return; - - assert(event_source != null); - - Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, source_target_entries, actions); - - // hook up handlers which route the event_source's DnD signals to the Page's (necessary - // because Page is a NO_WINDOW widget and cannot support DnD on its own). - event_source.drag_begin.connect(on_drag_begin); - event_source.drag_data_get.connect(on_drag_data_get); - event_source.drag_data_delete.connect(on_drag_data_delete); - event_source.drag_end.connect(on_drag_end); - event_source.drag_failed.connect(on_drag_failed); - - dnd_enabled = true; - } - - public void disable_drag_source() { - if (!dnd_enabled) - return; - - assert(event_source != null); - - event_source.drag_begin.disconnect(on_drag_begin); - event_source.drag_data_get.disconnect(on_drag_data_get); - event_source.drag_data_delete.disconnect(on_drag_data_delete); - event_source.drag_end.disconnect(on_drag_end); - event_source.drag_failed.disconnect(on_drag_failed); - Gtk.drag_source_unset(event_source); - - dnd_enabled = false; - } - - public bool is_dnd_enabled() { - return dnd_enabled; - } - - private void on_drag_begin(Gdk.DragContext context) { - drag_begin(context); - } - - private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data, - uint info, uint time) { - drag_data_get(context, selection_data, info, time); - } - - private void on_drag_data_delete(Gdk.DragContext context) { - drag_data_delete(context); - } - - private void on_drag_end(Gdk.DragContext context) { - drag_end(context); - } - - // wierdly, Gtk 2.16.1 doesn't supply a drag_failed virtual method in the GtkWidget impl ... - // Vala binds to it, but it's not available in gtkwidget.h, and so gcc complains. Have to - // makeshift one for now. - // https://bugzilla.gnome.org/show_bug.cgi?id=584247 - public virtual bool source_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) { - return false; - } - - private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) { - return source_drag_failed(context, drag_result); - } - // Use this function rather than GDK or GTK's get_pointer, especially if called during a // button-down mouse drag (i.e. a window grab). // @@ -1032,13 +962,13 @@ public abstract class Page : Gtk.ScrolledWindow { double dx, dy; event.get_scroll_deltas(out dx, out dy); - if (dy < 0) + if (dy < -1.0 * this.wheel_factor) return on_mousewheel_up(event); - else if (dy > 0) + else if (dy > this.wheel_factor) return on_mousewheel_down(event); - else if (dx < 0) + else if (dx < -1.0 * this.wheel_factor) return on_mousewheel_left(event); - else if (dx > 0) + else if (dx > this.wheel_factor) return on_mousewheel_right(event); else return false; @@ -1115,15 +1045,19 @@ public abstract class Page : Gtk.ScrolledWindow { } public void stop_cursor_hiding() { - if (last_timeout_id != 0) + if (last_timeout_id != 0) { Source.remove(last_timeout_id); + last_timeout_id = 0; + } } public void suspend_cursor_hiding() { cursor_hide_time_cached = cursor_hide_msec; - if (last_timeout_id != 0) + if (last_timeout_id != 0) { Source.remove(last_timeout_id); + last_timeout_id = 0; + } cursor_hide_msec = 0; } @@ -1209,1454 +1143,3 @@ public abstract class Page : Gtk.ScrolledWindow { } -public abstract class CheckerboardPage : Page { - private const int AUTOSCROLL_PIXELS = 50; - private const int AUTOSCROLL_TICKS_MSEC = 50; - - private CheckerboardLayout layout; - 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 previously_selected = 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; - } - - protected CheckerboardPage(string page_name) { - base (page_name); - - layout = new CheckerboardLayout(get_view()); - layout.set_name(page_name); - - 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(layout); - - // 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); - } - - 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_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 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 - 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); - - } - } - - base.switched_to(); - } - - private void on_view_filter_refresh() { - update_view_filter_message(); - } - - private void on_contents_altered(Gee.Iterable? added, - Gee.Iterable? removed) { - update_view_filter_message(); - } - - private void on_items_state_changed(Gee.Iterable changed) { - update_view_filter_message(); - } - - private void on_items_visibility_changed(Gee.Collection 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) { - layout.set_message(message); - if (is_in_view()) - layout.queue_draw(); - } - - public void unset_page_message() { - layout.unset_message(); - if (is_in_view()) - layout.queue_draw(); - } - - 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 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(); - 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(); - 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(); - - 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? 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); - - // if item is in any way out of view, scroll to it - 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)) - return; - - // 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 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; - - protected 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 sources = - (Gee.Collection) 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) 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; - } - -} -- cgit v1.2.3