Commit 49dd545e authored by Jens Georg's avatar Jens Georg

Split-up Page.vala

parent 83d11b3a
......@@ -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
......
......@@ -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
......
This diff is collapsed.
/* 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;
}
}
This diff is collapsed.
/* 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();
}
}
/* 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;