diff options
author | kaznacheev@chromium.org <kaznacheev@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-05 10:40:30 +0000 |
---|---|---|
committer | kaznacheev@chromium.org <kaznacheev@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-05 10:40:30 +0000 |
commit | 8b1ce2cbd0211c3b2126a6cc5189b864b6afb5b7 (patch) | |
tree | 0843b3d19fb934e35c6b03dc380dd5ed0d257e04 | |
parent | 6278441fe3cad3a15a61da31ca502fc3768b8b43 (diff) | |
download | chromium_src-8b1ce2cbd0211c3b2126a6cc5189b864b6afb5b7.zip chromium_src-8b1ce2cbd0211c3b2126a6cc5189b864b6afb5b7.tar.gz chromium_src-8b1ce2cbd0211c3b2126a6cc5189b864b6afb5b7.tar.bz2 |
Improved ChromeOS Gallery
The patch implements most of the desired appearance and behavior for Gallery v1.
BUG=chromium-os:19534
TEST=
Review URL: http://codereview.chromium.org/8113033
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@104085 0039d316-1c4b-4281-b951-d872f2087c98
34 files changed, 2143 insertions, 1351 deletions
diff --git a/chrome/browser/resources/component_extension_resources.grd b/chrome/browser/resources/component_extension_resources.grd index 0f43652..282e5dc 100644 --- a/chrome/browser/resources/component_extension_resources.grd +++ b/chrome/browser/resources/component_extension_resources.grd @@ -79,12 +79,19 @@ <include name="IDR_FILE_MANAGER_IMG_GALLERY_CROP_SELECTED" file="file_manager/images/gallery/icon_crop_selected.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_BRIGHTNESS" file="file_manager/images/gallery/icon_brightness.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_BRIGHTNESS_SELECTED" file="file_manager/images/gallery/icon_brightness_selected.png" type="BINDATA" /> + <include name="IDR_FILE_MANAGER_IMG_GALLERY_CONTRAST" file="file_manager/images/gallery/icon_contrast.png" type="BINDATA" /> + <include name="IDR_FILE_MANAGER_IMG_GALLERY_CONTRAST_SELECTED" file="file_manager/images/gallery/icon_contrast_selected.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_ROTATE" file="file_manager/images/gallery/icon_rotate.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_ROTATE_SELECTED" file="file_manager/images/gallery/icon_rotate_selected.png" type="BINDATA" /> + <include name="IDR_FILE_MANAGER_IMG_GALLERY_ROTATE_LEFT" file="file_manager/images/gallery/icon_rotate_left.png" type="BINDATA" /> + <include name="IDR_FILE_MANAGER_IMG_GALLERY_ROTATE_LEFT_SELECTED" file="file_manager/images/gallery/icon_rotate_left_selected.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_UNDO" file="file_manager/images/gallery/icon_undo.png" type="BINDATA" /> + <include name="IDR_FILE_MANAGER_IMG_GALLERY_REDO" file="file_manager/images/gallery/icon_redo.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_OK" file="file_manager/images/gallery/icon_ok_check.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_CANCEL" file="file_manager/images/gallery/icon_cancel_x.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_IMG_GALLERY_CLOSE" file="file_manager/images/gallery/close_x.png" type="BINDATA" /> + <include name="IDR_FILE_MANAGER_IMG_GALLERY_FADE_LEFT" file="file_manager/images/gallery/thumb_fade_left.png" type="BINDATA" /> + <include name="IDR_FILE_MANAGER_IMG_GALLERY_FADE_RIGHT" file="file_manager/images/gallery/thumb_fade_right.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_ICON_DETAIL_VIEW" file="file_manager/images/icon-detail-view.png" type="BINDATA" /> <include name="IDR_FILE_MANAGER_ICON_THUMB_VIEW" file="file_manager/images/icon-thumb-view.png" type="BINDATA" /> diff --git a/chrome/browser/resources/file_manager/images/gallery/close_x.png b/chrome/browser/resources/file_manager/images/gallery/close_x.png Binary files differindex e69de29..d9415ca 100644 --- a/chrome/browser/resources/file_manager/images/gallery/close_x.png +++ b/chrome/browser/resources/file_manager/images/gallery/close_x.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_brightness.png b/chrome/browser/resources/file_manager/images/gallery/icon_brightness.png Binary files differindex 3f2a531..fee577b 100644 --- a/chrome/browser/resources/file_manager/images/gallery/icon_brightness.png +++ b/chrome/browser/resources/file_manager/images/gallery/icon_brightness.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_brightness_selected.png b/chrome/browser/resources/file_manager/images/gallery/icon_brightness_selected.png Binary files differindex 05f14c7..96796fe 100644 --- a/chrome/browser/resources/file_manager/images/gallery/icon_brightness_selected.png +++ b/chrome/browser/resources/file_manager/images/gallery/icon_brightness_selected.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_cancel_x.png b/chrome/browser/resources/file_manager/images/gallery/icon_cancel_x.png Binary files differindex e69de29..c44ad86 100644 --- a/chrome/browser/resources/file_manager/images/gallery/icon_cancel_x.png +++ b/chrome/browser/resources/file_manager/images/gallery/icon_cancel_x.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_contrast.png b/chrome/browser/resources/file_manager/images/gallery/icon_contrast.png Binary files differnew file mode 100644 index 0000000..3f2a531 --- /dev/null +++ b/chrome/browser/resources/file_manager/images/gallery/icon_contrast.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_contrast_selected.png b/chrome/browser/resources/file_manager/images/gallery/icon_contrast_selected.png Binary files differnew file mode 100644 index 0000000..05f14c7 --- /dev/null +++ b/chrome/browser/resources/file_manager/images/gallery/icon_contrast_selected.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_ok_check.png b/chrome/browser/resources/file_manager/images/gallery/icon_ok_check.png Binary files differindex e69de29..30c4bf0 100644 --- a/chrome/browser/resources/file_manager/images/gallery/icon_ok_check.png +++ b/chrome/browser/resources/file_manager/images/gallery/icon_ok_check.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_redo.png b/chrome/browser/resources/file_manager/images/gallery/icon_redo.png Binary files differnew file mode 100644 index 0000000..beabd2f --- /dev/null +++ b/chrome/browser/resources/file_manager/images/gallery/icon_redo.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_rotate_left.png b/chrome/browser/resources/file_manager/images/gallery/icon_rotate_left.png Binary files differnew file mode 100644 index 0000000..928adc7 --- /dev/null +++ b/chrome/browser/resources/file_manager/images/gallery/icon_rotate_left.png diff --git a/chrome/browser/resources/file_manager/images/gallery/icon_rotate_left_selected.png b/chrome/browser/resources/file_manager/images/gallery/icon_rotate_left_selected.png Binary files differnew file mode 100644 index 0000000..a96e352 --- /dev/null +++ b/chrome/browser/resources/file_manager/images/gallery/icon_rotate_left_selected.png diff --git a/chrome/browser/resources/file_manager/images/gallery/thumb_fade_left.png b/chrome/browser/resources/file_manager/images/gallery/thumb_fade_left.png Binary files differnew file mode 100644 index 0000000..a261f1d --- /dev/null +++ b/chrome/browser/resources/file_manager/images/gallery/thumb_fade_left.png diff --git a/chrome/browser/resources/file_manager/images/gallery/thumb_fade_right.png b/chrome/browser/resources/file_manager/images/gallery/thumb_fade_right.png Binary files differnew file mode 100644 index 0000000..c792e19 --- /dev/null +++ b/chrome/browser/resources/file_manager/images/gallery/thumb_fade_right.png diff --git a/chrome/browser/resources/file_manager/js/exif_parser.js b/chrome/browser/resources/file_manager/js/exif_parser.js index 04c3c39c..be594e4 100644 --- a/chrome/browser/resources/file_manager/js/exif_parser.js +++ b/chrome/browser/resources/file_manager/js/exif_parser.js @@ -18,10 +18,13 @@ const EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. const EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. const EXIF_TAG_ORIENTATION = 0x0112; +const EXIF_TAG_X_DIMENSION = 0xA002; +const EXIF_TAG_Y_DIMENSION = 0xA003; function ExifParser(parent) { MetadataParser.apply(this, [parent]); this.verbose = false; + this.mimeType = 'image/jpeg'; } ExifParser.parserType = 'exif'; @@ -102,17 +105,14 @@ ExifParser.prototype.parse = function(file, callback, errorCallback) { return onError('Invalid TIFF tag: ' + tag.toString(16)); var metadata = { - metadataType: 'exif', - mimeType: 'image/jpeg', + metadataType: ExifParser.parserType, + mimeType: self.mimeType, littleEndian: (order == EXIF_ALIGN_LITTLE), ifd: { image: {}, - thumbnail: {}, - exif: {}, - gps: {} + thumbnail: {} } }; - var directoryOffset = br.readScalar(4); // Image directory. @@ -135,7 +135,19 @@ ExifParser.prototype.parse = function(file, callback, errorCallback) { self.vlog('Read EXIF directory.'); directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; br.seek(directoryOffset); + metadata.ifd.exif = {}; self.readDirectory(br, metadata.ifd.exif); + + if (EXIF_TAG_X_DIMENSION in metadata.ifd.exif && + EXIF_TAG_Y_DIMENSION in metadata.ifd.exif) { + if (metadata.imageTransform && metadata.imageTransform.rotate90) { + metadata.width = metadata.ifd.exif[EXIF_TAG_Y_DIMENSION].value; + metadata.height = metadata.ifd.exif[EXIF_TAG_X_DIMENSION].value; + } else { + metadata.width = metadata.ifd.exif[EXIF_TAG_X_DIMENSION].value; + metadata.height = metadata.ifd.exif[EXIF_TAG_Y_DIMENSION].value; + } + } } // GPS Directory may also be linked from the image directory. @@ -143,6 +155,7 @@ ExifParser.prototype.parse = function(file, callback, errorCallback) { self.vlog('Read GPS directory.'); directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; br.seek(directoryOffset); + metadata.ifd.gps = {}; self.readDirectory(br, metadata.ifd.gps); } @@ -256,7 +269,9 @@ ExifParser.prototype.readTagValue = function(br, tag) { case 2: // String safeRead(1); - if (tag.componentCount == 1) { + if (tag.componentCount == 0) { + tag.value = ''; + } else if (tag.componentCount == 1) { tag.value = String.fromCharCode(tag.value); } else { tag.value = String.fromCharCode.apply(null, tag.value); diff --git a/chrome/browser/resources/file_manager/js/file_manager.js b/chrome/browser/resources/file_manager/js/file_manager.js index 69c0d31..b8e23f4 100644 --- a/chrome/browser/resources/file_manager/js/file_manager.js +++ b/chrome/browser/resources/file_manager/js/file_manager.js @@ -2000,14 +2000,32 @@ FileManager.prototype = { galleryFrame.className = 'overlay-pane'; galleryFrame.scrolling = 'no'; + var selectedUrl; + if (urls.length == 1) { + // Single item selected. Pass to the Gallery as a selected. + selectedUrl = urls[0]; + // Pass every image in the directory so that it shows up in the ribbon. + urls = []; + for (var i = 0; i != this.dataModel_.length; i++) { + var url = this.dataModel_.item(i).toURL(); + if (url.match(iconTypes.image)) + urls.push(url); + } + } else { + // Multiple selection. Pass just those items, select the first entry. + selectedUrl = urls[0]; + } + galleryFrame.onload = function() { galleryFrame.contentWindow.Gallery.open( self.currentDirEntry_, urls, + selectedUrl, function () { // TODO(kaznacheev): keep selection. self.rescanDirectoryNow_(); // Make sure new files show up. self.dialogDom_.removeChild(galleryFrame); + self.refocus(); }, self.metadataProvider_, shareActions); @@ -2015,6 +2033,7 @@ FileManager.prototype = { galleryFrame.src = 'js/image_editor/gallery.html'; this.dialogDom_.appendChild(galleryFrame); + galleryFrame.focus(); }; /** diff --git a/chrome/browser/resources/file_manager/js/image_editor/commands.js b/chrome/browser/resources/file_manager/js/image_editor/commands.js new file mode 100644 index 0000000..3a4bfed --- /dev/null +++ b/chrome/browser/resources/file_manager/js/image_editor/commands.js @@ -0,0 +1,340 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * Command queue is the only way to modify images. + * Supports undo/redo. + * Command execution is asynchronous (callback-based). + */ +function CommandQueue(document, canvas) { + this.document_ = document; + this.undo_ = []; + this.redo_ = []; + this.subscribers_ = []; + + this.baselineImage_ = canvas; + this.currentImage_ = canvas; + this.previousImage_ = null; + + this.busy_ = false; + + this.UIContext_ = null; +} + +/** + * Attach the UI elements to the command queue. + * Once the UI is attached the results of image manipulations are displayed. + * + * @param {ImageView} imageView The ImageView object to display the results + * @param {ImageEditor.Prompt} prompt + * @param {function(boolean)} lock Function to enable/disable buttons etc. + */ +CommandQueue.prototype.attachUI = function(imageView, prompt, lock) { + this.UIContext_ = { + imageView: imageView, + prompt: prompt, + lock: lock + }; +}; + +/** + * Detach the UI. Further image modifications will not be displayed. + */ +CommandQueue.prototype.detachUI = function() { + // Instead of nulling out this.UIContext_ we null out its fields so that + // the commands currently being executed see this change. + this.UIContext_.imageView = null; + this.UIContext_.prompt = null; + this.UIContext_.lock = null; +}; + +/** + * Asynchronous getter. Does not return the image while the queue is busy. + */ +CommandQueue.prototype.requestCurrentImage = function(callback) { + if (this.isBusy()) { + this.subscribers_.push(callback); + } else { + var self = this; + setTimeout(function() { callback(self.currentImage_) }, 0); + } +}; + +CommandQueue.prototype.isBusy = function() { return this.busy_ }; + +CommandQueue.prototype.setBusy_ = function(on) { + if (this.busy_ == on) + throw new Error('Inconsistent CommandQueue lock state'); + + this.busy_ = on; + + if (!on) { + // Notify the subscribers that requested the image while the queue was busy. + while (this.subscribers_.length) { + this.subscribers_.pop()(this.currentImage_); + } + } + + if (this.UIContext_) + this.UIContext_.lock(on); + + if (on) { + ImageUtil.trace.resetTimer('command-busy'); + } else { + ImageUtil.trace.reportTimer('command-busy'); + } +}; + +CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) { + if (!this.currentImage_) + throw new Error('Cannot operate on null image'); + + // Remember one previous image so that the first undo is as fast as possible. + this.previousImage_ = this.currentImage_; + var self = this; + command.execute( + this.document_, + this.currentImage_, + function(result) { + self.currentImage_ = result; + callback(); + }, + uiContext); +}; + +/** + * Executes the command. + * + * @param {Command} command + * @param {boolean} opt_keep_redo true if redo stack should not be cleared. + */ +CommandQueue.prototype.execute = function(command, opt_keep_redo) { + this.setBusy_(true); + + if (!opt_keep_redo) + this.redo_ = []; + + this.undo_.push(command); + + this.doExecute_(command, this.UIContext_, this.setBusy_.bind(this, false)); +}; + +CommandQueue.prototype.canUndo = function() { + return this.undo_.length != 0; +}; + +CommandQueue.prototype.undo = function() { + if (!this.canUndo()) + throw new Error('Cannot undo'); + + this.setBusy_(true); + + var command = this.undo_.pop(); + this.redo_.push(command); + + var self = this; + + function complete() { + if (self.UIContext_.imageView) { + command.revertView(self.currentImage_, self.UIContext_.imageView); + } + self.setBusy_(false); + } + + if (this.previousImage_) { + // First undo after an execute call. + this.currentImage_ = this.previousImage_; + this.previousImage_ = null; + complete(); + // TODO(kaznacheev) Consider recalculating previousImage_ right here + // by replaying the commands in the background. + } else { + this.currentImage_ = this.baselineImage_; + + function replay(index) { + if (index < self.undo_.length) + self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1)); + else { + complete(); + } + } + + replay(0); + } +}; + +CommandQueue.prototype.canRedo = function() { + return this.redo_.length != 0; +}; + +CommandQueue.prototype.redo = function() { + if (!this.canRedo()) + throw new Error('Cannot redo'); + + this.execute(this.redo_.pop(), true); +}; + +/** + * Command object encapsulates an operation on an image and a way to visualize + * its result. + */ +function Command(name) { + this.name_ = name; +} + +Command.prototype.toString = function() { + return 'Command ' + this.name_; +}; + +/** + * Execute the command and visualize its results. + * + * The two actions are combined into one method because sometimes it is nice + * to be able to show partial results for slower operations. + * + * @param {Document} document + * @param {HTMLCanvasElement} srcCanvas + * @param {Object} uiContext + * @param {function(HTMLCanvasElement)} + */ +Command.prototype.execute = function(document, srcCanvas, callback, uiContext) { + setTimeout(callback.bind(null, null), 0); +}; + +/** + * Visualize reversion of the operation. + * + * @param {HTMLCanvasElement} canvas + * @param {ImageView} imageView + */ +Command.prototype.revertView = function(canvas, imageView) { + imageView.replace(canvas); +}; + +Command.prototype.createCanvas_ = function( + document, srcCanvas, opt_width, opt_height) { + var result = document.createElement('canvas'); + result.width = opt_width || srcCanvas.width; + result.height = opt_height || srcCanvas.height; + return result; +}; + + +/** + * Rotate command + * @param {number} rotate90 Rotation angle in 90 degree increments (signed) + */ +Command.Rotate = function(rotate90) { + Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)'); + this.rotate90_ = rotate90; +}; + +Command.Rotate.prototype = { __proto__: Command.prototype }; + +Command.Rotate.prototype.execute = function( + document, srcCanvas, callback, uiContext) { + var result = this.createCanvas_( + document, + srcCanvas, + (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width, + (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height); + ImageUtil.drawImageTransformed( + result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2); + if (uiContext && uiContext.imageView) { + uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_); + } + setTimeout(callback.bind(null, result), 0); +}; + +Command.Rotate.prototype.revertView = function(canvas, imageView) { + imageView.replaceAndAnimate(canvas, null, -this.rotate90_); +}; + + +/** + * Crop command. + * + * @param {Rect} imageRect Crop rectange in image coordinates. + * @param {Rect} screenRect Crop rectange in screen coordinates (for animation). + */ +Command.Crop = function(imageRect, screenRect) { + Command.call(this, 'crop' + imageRect.toString()); + this.imageRect_ = imageRect; + this.screenRect_ = screenRect; +}; + +Command.Crop.prototype = { __proto__: Command.prototype }; + +Command.Crop.prototype.execute = function( + document, srcCanvas, callback, uiContext) { + var result = this.createCanvas_( + document, srcCanvas, this.imageRect_.width, this.imageRect_.height); + Rect.drawImage(result.getContext("2d"), srcCanvas, null, this.imageRect_); + if (uiContext && uiContext.imageView) { + uiContext.imageView. + replaceAndAnimate(result, this.screenRect_, 0); + } + setTimeout(callback.bind(null, result), 0); +}; + +Command.Crop.prototype.revertView = function(canvas, imageView) { + imageView.animateAndReplace(canvas, this.screenRect_); +}; + + +/** + * Filter command. + * + * @param {string} name Command name + * @param {function(ImageData,ImageData,number,number)} filter Filter function + * @param {string} message Message to display when done + */ +Command.Filter = function(name, filter, message) { + Command.call(this, name); + this.filter_ = filter; + this.message_ = message; +}; + +Command.Filter.prototype = { __proto__: Command.prototype }; + +Command.Filter.prototype.execute = function( + document, srcCanvas, callback, uiContext) { + var result = this.createCanvas_(document, srcCanvas); + + var self = this; + + var previousRow = 0; + + function onProgressVisible(updatedRow, rowCount) { + if (updatedRow == rowCount) { + uiContext.imageView.replace(result); + if (self.message_) + uiContext.prompt.show(self.message_, 2000); + callback(result); + } else { + var viewport = uiContext.imageView.viewport_; + + var imageStrip = new Rect(viewport.getImageBounds()); + imageStrip.top = previousRow; + imageStrip.height = updatedRow - previousRow; + + var screenStrip = new Rect(viewport.getImageBoundsOnScreen()); + screenStrip.top = Math.round(viewport.imageToScreenY(previousRow)); + screenStrip.height = + Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top; + + uiContext.imageView.paintScreenRect(screenStrip, result, imageStrip); + previousRow = updatedRow; + } + } + + function onProgressInvisible(updatedRow, rowCount) { + if (updatedRow == rowCount) { + callback(result); + } + } + + filter.applyByStrips(result, srcCanvas, this.filter_, + uiContext.imageView ? onProgressVisible : onProgressInvisible); +}; diff --git a/chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js b/chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js index 7a1704d..9e60b4b 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js +++ b/chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js @@ -66,6 +66,9 @@ ExifEncoder.prototype.setImageData = function(canvas) { ExifEncoder.findOrCreateTag(exif, EXIF_TAG_X_DIMENSION).value = canvas.width; ExifEncoder.findOrCreateTag(exif, EXIF_TAG_Y_DIMENSION).value = canvas.height; + this.metadata_.width = canvas.width; + this.metadata_.height = canvas.height; + // Always save in default orientation. delete this.metadata_.imageTransform; ExifEncoder.findOrCreateTag(image, EXIF_TAG_ORIENTATION).value = 1; @@ -486,7 +489,7 @@ ByteWriter.prototype.forward = function(key, width) { */ ByteWriter.prototype.resolve = function(key, value) { if (!(key in this.forwards_)) - throw new Error('Undeclared forward key ' + key); + throw new Error('Undeclared forward key ' + key.toString(16)); var forward = this.forwards_[key]; var curPos = this.pos_; this.pos_ = forward.pos; @@ -507,6 +510,6 @@ ByteWriter.prototype.resolveOffset = function(key) { */ ByteWriter.prototype.checkResolved = function() { for (var key in this.forwards_) { - throw new Error('Unresolved forward pointer ' + key); + throw new Error ('Unresolved forward pointer ' + key.toString(16)); } };
\ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/image_editor/filter.js b/chrome/browser/resources/file_manager/js/image_editor/filter.js index 6976367..324fecb 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/filter.js +++ b/chrome/browser/resources/file_manager/js/image_editor/filter.js @@ -30,21 +30,23 @@ filter.create = function(name, options) { * * To be used with large images to avoid freezing up the UI. * - * @param {CanvasRenderingContext2D} context + * @param {HTMLCanvasElement} dstCanvas + * @param {HTMLCanvasElement} srcCanvas * @param {function(ImageData,ImageData,number,number)} filterFunc * @param {function(number,number} progressCallback * @param {number} maxPixelsPerStrip */ filter.applyByStrips = function( - context, filterFunc, progressCallback, maxPixelsPerStrip) { - var source = context.getImageData( - 0, 0, context.canvas.width, context.canvas.height); + dstCanvas, srcCanvas, filterFunc, progressCallback, maxPixelsPerStrip) { + var dstContext = dstCanvas.getContext('2d'); + var srcContext = srcCanvas.getContext('2d'); + var source = srcContext.getImageData(0, 0, srcCanvas.width, srcCanvas.height); - var stripCount = Math.ceil (source.width * source.height / + var stripCount = Math.ceil (srcCanvas.width * srcCanvas.height / (maxPixelsPerStrip || 1000000)); // 1 Mpix is a reasonable default. - var strip = context.getImageData(0, 0, - source.width, Math.ceil (source.height / stripCount)); + var strip = srcContext.getImageData(0, 0, + srcCanvas.width, Math.ceil (srcCanvas.height / stripCount)); var offset = 0; @@ -59,16 +61,20 @@ filter.applyByStrips = function( } filterFunc(strip, source, 0, offset); - context.putImageData(strip, 0, offset); + dstContext.putImageData(strip, 0, offset); offset += strip.height; - progressCallback(offset, source.height); if (offset < source.height) { setTimeout(filterStrip, 0); + } else { + ImageUtil.trace.reportTimer('filter-commit'); } + + progressCallback(offset, source.height); } + ImageUtil.trace.resetTimer('filter-commit'); filterStrip(); }; diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery.css b/chrome/browser/resources/file_manager/js/image_editor/gallery.css index b71d373..b2af0f0 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery.css +++ b/chrome/browser/resources/file_manager/js/image_editor/gallery.css @@ -6,6 +6,9 @@ body { margin: 0; + -webkit-user-select: none; + font-family: Open Sans,Droid Sans Fallback,sans-serif; + font-size: 84%; } .gallery { @@ -16,62 +19,102 @@ body { overflow: hidden; } +/* Close button */ .gallery > .close { position: absolute; - padding: 1px 5px 1px 21px; - right: 10px; - top: 10px; + right: 5px; + top: 5px; height: 20px; + width: 20px; color: white; cursor: pointer; + opacity: 0.4; + z-index: 200; background-image: url(../../images/gallery/close_x.png); background-repeat: no-repeat; - background-position: 5px center; + background-position: center center; } .gallery > .close:hover { + opacity: 0.7; background-color: rgba(81,81,81,1); } .gallery > .image-container { position: absolute; - top: 30px; - bottom: 0px; + height: 100%; width: 100%; background-color: rgba(0,0,0,1); } +/* Image container and canvas elements */ + +.gallery > .image-container > canvas { + position: absolute; + pointer-events: none; + + -webkit-transition-property: -webkit-transform, opacity; + -webkit-transition-timing-function: ease-in-out; + /* -webkit-transition-duration is set in image_view.js*/ +} + +.gallery > .image-container > canvas[fade] { + opacity: 0; +} + +.gallery > .image-container > canvas[fade='left'] { + -webkit-transform: translate(-40px,0); +} + +.gallery > .image-container > canvas[fade='right'] { + -webkit-transform: translate(40px,0); +} + +/* Toolbar */ + .gallery > .toolbar { position: absolute; bottom: 0px; width: 100%; - height: 60px; - padding: 3px; + height: 55px; display: -webkit-box; -webkit-box-orient: horizontal; -webkit-box-pack: start; -webkit-box-align: stretch; - opacity: 1.0; - -webkit-transition: opacity 0.5s ease-in-out; background-color: rgba(18,18,18,0.75); border-top: 1px solid rgba(31,31,31,0.75); + + pointer-events: none; + opacity: 0; + -webkit-transform: translate(0, 0px); + + -webkit-transition-property: webkit-transform, opacity; + -webkit-transition-duration: 220ms; + -webkit-transition-timing-function: ease; } -.gallery > .toolbar[hidden] { - opacity: 0.0; +.gallery[tools] > .toolbar { + pointer-events: auto; + opacity: 1; + -webkit-transform: translate(0, 0); } -.gallery .ribbon { - -webkit-box-flex: 1; - overflow: hidden; - height: 100%; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: left; +.gallery[tools][mousedrag] > .toolbar { + pointer-events: none; + opacity: 0.2; +} + +.gallery[tools][locked] > .toolbar { + pointer-events: none; +} + +.gallery[locked] { + cursor: wait; } -.gallery .ribbon-left, -.gallery .ribbon-right { +.gallery .arrow { + position: absolute; + z-index: 100; width: 20px; height: 100%; cursor: pointer; @@ -80,25 +123,60 @@ body { -webkit-box-pack: center; background-repeat: no-repeat; background-position: center center; + pointer-events: none; + opacity: 0; } -.gallery .ribbon-left { - background-image: url(../../images/gallery/arrow_left.png); +.gallery[tools] .arrow { + opacity: 1; +} + +.gallery[tools] .arrow[active] { + pointer-events: auto; } -.gallery .ribbon-left[disabled] { +.gallery .arrow.left { + left: 0; background-image: url(../../images/gallery/arrow_left_disabled.png); } -.gallery .ribbon-right { - background-image: url(../../images/gallery/arrow_right.png); - border-right: 1px solid rgba(87,87,87,0.5); +.gallery .arrow.left[active] { + background-image: url(../../images/gallery/arrow_left.png); } -.gallery .ribbon-right[disabled] { +.gallery .arrow.right { + right: 0; background-image: url(../../images/gallery/arrow_right_disabled.png); } +.gallery .arrow.right[active] { + background-image: url(../../images/gallery/arrow_right.png); +} + +/* Thumbnails */ + +.gallery .ribbon-spacer { + position: relative; + display: -webkit-box; + -webkit-box-flex: 1; + -webkit-box-orient: horizontal; +} + +.gallery .toolbar .ribbon { + overflow: hidden; + height: 100%; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: left; + + max-width: 100%; + -webkit-transition: max-width 0.5s ease-in-out; +} + +.gallery[editing] .toolbar .ribbon { + max-width: 0; +} + .gallery .ribbon-image { display: -webkit-box; -webkit-box-orient: horizontal; @@ -110,58 +188,89 @@ body { height: 47px; margin: 2px; border: 2px solid rgba(255,255,255,0); /* transparent white */ + + margin-left: 2px; + -webkit-transition: margin-left 0.3s ease-in-out; +} + +.gallery .ribbon-image[shifted] { + margin-left: -53px; } .gallery .ribbon-image[selected] { border: 2px solid rgba(255,233,168,1); } -.gallery .ribbon-image > img { - max-width: 45px; - max-height: 45px; +.gallery .image-wrapper { + position: relative; + overflow: hidden; + width: 45px; + height: 45px; + border: 1px solid rgba(0,0,0,0); /* transparent black */ } -.gallery .ribbon-spacer { - -webkit-box-flex: 1; - height: 100%; +.gallery .toolbar .fade { + background-repeat: no-repeat; + background-position: center center; + position: relative; + z-index: 10; + width: 55px; + pointer-events: none; + opacity: 0; } -.gallery .edit-bar { - -webkit-box-flex: 0; - max-width: 60%; - height: 100%; - color: white; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-transition: max-width 0.5s ease-in-out; +.gallery .toolbar .fade[active] { + opacity: 1; } -.gallery .edit-bar > .edit-main{ +.gallery .toolbar .fade.left { + margin-right: -55px; /* Fully overlap the next thumbnail */ + background-image: url(../../images/gallery/thumb_fade_left.png); +} + +.gallery .toolbar .fade.right { + margin-left: -55px; /* Fully overlap the previous thumbnail */ + background-image: url(../../images/gallery/thumb_fade_right.png); +} + +/* Editor buttons */ + +.gallery .toolbar .edit-bar { + position: absolute; + overflow: hidden; + pointer-events: none; + right: 0; + width: 75%; + height: 55px; + color: white; display: -webkit-box; -webkit-box-orient: horizontal; - opacity: 1.0; - -webkit-transition: opacity 0.25s ease-in-out 0.25s; + -webkit-box-pack: center; + -webkit-transition: width 0.5s ease-in-out; } -.gallery .edit-bar[hidden] { - max-width: 0%; - -webkit-transition: max-width 0.5s ease-in-out; +.gallery[editing] .toolbar .edit-bar { + width: 100%; } -.gallery .edit-bar[hidden] > .edit-main{ +.gallery .toolbar .edit-bar > .edit-main{ + display: -webkit-box; + -webkit-box-orient: horizontal; opacity: 0; -webkit-transition: opacity 0.25s ease-in-out; } -.gallery .edit-bar[hidden] > .edit-main[hidden] { - display: none; +.gallery[editing] .toolbar .edit-bar > .edit-main{ + pointer-events: auto; + opacity: 1.0; } .gallery > .toolbar .button { -webkit-box-flex: 0; - padding: 10px 10px 10px 33px; + padding: 0px 10px 0px 35px; cursor: pointer; - margin: 10px 5px; + margin: 8px 0px 7px 3px; + height: 40px; display: -webkit-box; -webkit-box-orient: horizontal; @@ -216,19 +325,44 @@ body { background-image: url(../../images/gallery/icon_brightness_selected.png); } -.gallery > .toolbar .button.rotate { +.gallery > .toolbar .button.rotate-right { background-image: url(../../images/gallery/icon_rotate.png); } -.gallery > .toolbar .button.rotate[pressed] { +.gallery > .toolbar .button.rotate-right[pressed] { background-image: url(../../images/gallery/icon_rotate_selected.png); } +.gallery > .toolbar .button.rotate-left { + background-image: url(../../images/gallery/icon_rotate_left.png); +} + +.gallery > .toolbar .button.rotate-left[pressed] { + background-image: url(../../images/gallery/icon_rotate_left_selected.png); +} + .gallery > .toolbar .button.undo { background-image: url(../../images/gallery/icon_undo.png); } +.gallery > .toolbar .button.redo { + position: absolute; /* Exclude from center-packing*/ + background-image: url(../../images/gallery/icon_redo.png); +} + +.gallery > .toolbar .button[disabled] { + pointer-events: none; + opacity: 0.5; +} + +.gallery > .toolbar .button[hidden] { + display: none; +} + .gallery > .toolbar > .button.edit { + position: relative; + z-index: 10; + margin-left: 25px; background-image: url(../../images/gallery/icon_edit.png); } @@ -237,6 +371,9 @@ body { } .gallery > .toolbar > .button.share { + position: relative; + z-index: 10; + margin-right: 8px; background-image: url(../../images/gallery/icon_share.png); } @@ -244,53 +381,180 @@ body { background-image: url(../../images/gallery/icon_share_selected.png); } +/* Secondary toolbar (mode-specific tools) */ + .gallery .edit-modal { position: absolute; - left: 0; - bottom: 53px; - height: 30px; - color: black; - background-color: white; + width: 100%; + bottom: 80px; + height: 40px; display: -webkit-box; -webkit-box-orient: horizontal; + -webkit-box-pack: center; + pointer-events: none; } -.gallery .edit-modal[hidden] { +.gallery .edit-modal-wrapper[hidden] { display: none; } -.gallery .edit-modal > .button.mode { - color: black; - background-color: rgba(240,240,240,1); - padding: 0 5px 0 30px; - margin: 2px 2px; +.gallery .edit-modal-wrapper { + color: white; + padding-right: 5px; + background-color: rgba(0, 0, 0, 0.75); + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + pointer-events: auto; +} + +.gallery .edit-modal .label { + padding: 0 10px; + + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; background-repeat: no-repeat; - background-position: 5px center; + background-position: 20px center; } -.gallery .edit-modal > .button.mode:hover { - background-color: rgba(200,200,200,1); +.gallery .edit-modal .label.brightness { + padding-left: 50px; + background-image: url(../../images/gallery/icon_brightness.png); } -.gallery .edit-modal .button.ok { - background-image: url(../../images/gallery/icon_ok_check.png); +.gallery .edit-modal .label.contrast { + margin-left: 15px; + padding-left: 50px; + background-image: url(../../images/gallery/icon_contrast.png); } -.gallery .edit-modal .button.cancel { - background-image: url(../../images/gallery/icon_cancel_x.png); +.gallery .edit-modal .range { + margin: 9px 10px 9px 0; } -.gallery .edit-modal > .label { - padding: 0 10px; +/* Crop frame */ + +.gallery .crop-overlay { + position: absolute; + pointer-events: none; + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.gallery .crop-overlay .shadow { + background-color: rgba(0,0,0,0.4); +} + +.gallery .crop-overlay .middle-box { + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-flex: 1; +} + +.gallery .crop-frame { + position: relative; + display: -webkit-box; + -webkit-box-flex: 1; +} + +.gallery .crop-frame div{ + position: absolute; + background-color: rgba(255, 255, 255, 0.8); + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.75); +} + +.gallery .crop-frame .horizontal { + left: 7px; + right: 7px; + height: 1px; +} + +.gallery .crop-frame .horizontal.top { + top: 0; +} + +.gallery .crop-frame .horizontal.bottom { + bottom: 0; +} + +.gallery .crop-frame .vertical { + top: 7px; + bottom: 7px; + width: 1px; +} + +.gallery .crop-frame .vertical.left { + left: 0; +} + +.gallery .crop-frame .vertical.right { + right: 0; +} + +.gallery .crop-frame .corner { + border-radius: 6px; + width: 13px; + height: 13px; +} + +.gallery .crop-frame .corner.left { + left: -6px; +} + +.gallery .crop-frame .corner.right { + right: -6px; +} + +.gallery .crop-frame .corner.top { + top: -6px; +} + +.gallery .crop-frame .corner.bottom { + bottom: -6px; +} + +/* Prompt/notification panel */ + +.gallery .prompt-wrapper { + position: absolute; + pointer-events: none; + + width: 100%; + height: 100%; display: -webkit-box; -webkit-box-orient: horizontal; -webkit-box-align: center; + -webkit-box-pack: center; } -.gallery .edit-modal > .range { - margin: 4px 10px 4px 0; +.gallery .prompt { + font-size: 120%; + height: 40px; + padding: 0 20px; + color: white; + background-color: rgba(0, 0, 0, 0.8); + + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + + position: relative; + top: 5px; + opacity: 0; + -webkit-transition: all 180ms ease; +} + +.gallery .prompt[state='fadein'] { + top: 0; + opacity: 1; +} + +.gallery .prompt[state='fadeout'] { + top: 0; + opacity: 0; } .gallery .share-menu { diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery.html b/chrome/browser/resources/file_manager/js/image_editor/gallery.html index 28e5406..1f57e31 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery.html +++ b/chrome/browser/resources/file_manager/js/image_editor/gallery.html @@ -17,6 +17,8 @@ <script type="text/javascript" src="image_util.js"></script> <script type="text/javascript" src="viewport.js"></script> <script type="text/javascript" src="image_buffer.js"></script> + <script type="text/javascript" src="image_view.js"></script> + <script type="text/javascript" src="commands.js"></script> <script type="text/javascript" src="image_editor.js"></script> <script type="text/javascript" src="image_transform.js"></script> <script type="text/javascript" src="image_adjust.js"></script> diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery.js b/chrome/browser/resources/file_manager/js/image_editor/gallery.js index 30af34a..12843df 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery.js +++ b/chrome/browser/resources/file_manager/js/image_editor/gallery.js @@ -23,47 +23,65 @@ function Gallery(container, closeCallback, metadataProvider, shareActions) { this.initDom_(shareActions); } -Gallery.open = function( - parentDirEntry, urls, closeCallback, metadataProvider, shareActions) { +Gallery.open = function(parentDirEntry, items, selectedItem, + closeCallback, metadataProvider, shareActions) { var container = document.querySelector('.gallery'); container.innerHTML = ''; - var gallery = new Gallery(container, closeCallback, metadataProvider, - shareActions); - gallery.load(parentDirEntry, urls); + var gallery = new Gallery( + container, closeCallback, metadataProvider, shareActions); + gallery.load(parentDirEntry, items, selectedItem); }; // TODO(kaznacheev): localization. Gallery.displayStrings = { - close: 'Close', edit: 'Edit', share: 'Share', autofix: 'Auto-fix', crop: 'Crop', - exposure: 'Brightness / contrast', + exposure: 'Brightness', brightness: 'Brightness', contrast: 'Contrast', - rotate: 'Rotate', - undo: 'Undo' + 'rotate-left': 'Left', + 'rotate-right': 'Right', + 'enter-when-done': 'Press Enter when done', + fixed: 'Fixed', + undo: 'Undo', + redo: 'Redo' }; Gallery.editorModes = [ - ImageEditor.Mode.InstantAutofix, - ImageEditor.Mode.Crop, - ImageEditor.Mode.Exposure, - ImageEditor.Mode.InstantRotate + new ImageEditor.Mode.InstantAutofix(), + new ImageEditor.Mode.Crop(), + new ImageEditor.Mode.Exposure(), + new ImageEditor.Mode.OneClick('rotate-left', new Command.Rotate(-1)), + new ImageEditor.Mode.OneClick('rotate-right', new Command.Rotate(1)) ]; -Gallery.FADE_TIMEOUT = 5000; +Gallery.FADE_TIMEOUT = 3000; +Gallery.FIRST_FADE_TIMEOUT = 1000; Gallery.prototype.initDom_ = function(shareActions) { var doc = this.document_; - this.container_.addEventListener('keydown', this.onKeyDown_.bind(this)); - this.container_.addEventListener('mousemove', this.onMouseMove_.bind(this)); + + // Clean up after the previous instance of Gallery. + this.container_.removeAttribute('editing'); + this.container_.removeAttribute('tools'); + if (window.galleryKeyDown) { + doc.body.removeEventListener('keydown', window.galleryKeyDown); + } + if (window.galleryMouseMove) { + doc.body.removeEventListener('keydown', window.galleryMouseMove); + } + + window.galleryKeyDown = this.onKeyDown_.bind(this); + doc.body.addEventListener('keydown', window.galleryKeyDown); + + window.galleryMouseMove = this.onMouseMove_.bind(this); + doc.body.addEventListener('mousemove', window.galleryMouseMove); this.closeButton_ = doc.createElement('div'); this.closeButton_.className = 'close'; - this.closeButton_.textContent = Gallery.displayStrings['close']; this.closeButton_.addEventListener('click', this.onClose_.bind(this)); this.container_.appendChild(this.closeButton_); @@ -75,11 +93,28 @@ Gallery.prototype.initDom_ = function(shareActions) { this.toolbar_.className = 'toolbar'; this.container_.appendChild(this.toolbar_); - this.ribbon_ = new Ribbon(this.toolbar_, this.onSelect_.bind(this)); + this.ribbonSpacer_ = doc.createElement('div'); + this.ribbonSpacer_.className = 'ribbon-spacer'; + this.toolbar_.appendChild(this.ribbonSpacer_); + + this.toolbar_.addEventListener('mouseover', + this.enableFading_.bind(this, false)); + this.toolbar_.addEventListener('mouseout', + this.enableFading_.bind(this, true)); + + this.arrowLeft_ = this.document_.createElement('div'); + this.arrowLeft_.className = 'arrow left'; + this.container_.appendChild(this.arrowLeft_); + + this.arrowRight_ = this.document_.createElement('div'); + this.arrowRight_.className = 'arrow right'; + this.container_.appendChild(this.arrowRight_); + + this.ribbon_ = new Ribbon(this.ribbonSpacer_, this.onSelect_.bind(this), + this.arrowLeft_, this.arrowRight_); this.editBar_ = doc.createElement('div'); this.editBar_.className = 'edit-bar'; - this.editBar_.setAttribute('hidden', 'hidden'); this.toolbar_.appendChild(this.editBar_); this.editButton_ = doc.createElement('div'); @@ -98,13 +133,16 @@ Gallery.prototype.initDom_ = function(shareActions) { this.editBarMain_ = doc.createElement('div'); this.editBarMain_.className = 'edit-main'; - this.editBarMain_.setAttribute('hidden', 'hidden'); this.editBar_.appendChild(this.editBarMain_); this.editBarMode_ = doc.createElement('div'); this.editBarMode_.className = 'edit-modal'; - this.editBarMode_.setAttribute('hidden', 'hidden'); - this.editBar_.appendChild(this.editBarMode_); + this.container_.appendChild(this.editBarMode_); + + this.editBarModeWrapper_ = doc.createElement('div'); + ImageUtil.setAttribute(this.editBarModeWrapper_, 'hidden', true); + this.editBarModeWrapper_.className = 'edit-modal-wrapper'; + this.editBarMode_.appendChild(this.editBarModeWrapper_); this.shareMenu_ = doc.createElement('div'); this.shareMenu_.className = 'share-menu'; @@ -126,88 +164,83 @@ Gallery.prototype.initDom_ = function(shareActions) { this.editor_ = new ImageEditor( this.imageContainer_, this.editBarMain_, - this.editBarMode_, + this.editBarModeWrapper_, Gallery.editorModes, Gallery.displayStrings); + + this.imageView_ = this.editor_.getImageView(); + + this.editor_.trackWindow(doc.defaultView); }; -Gallery.prototype.load = function(parentDirEntry, urls) { - this.editBar_.setAttribute('hidden', 'hidden'); - this.editBarMain_.setAttribute('hidden', 'hidden'); +Gallery.prototype.load = function(parentDirEntry, items, selectedItem) { + this.parentDirEntry_ = parentDirEntry; + this.editButton_.removeAttribute('pressed'); this.shareButton_.removeAttribute('pressed'); - this.toolbar_.removeAttribute('hidden'); + this.shareMenu_.setAttribute('hidden', 'hidden'); this.editing_ = false; this.sharing_ = false; - if (urls.length == 0) - return; + this.cancelFading_(); + this.initiateFading_(); - // TODO(kaznacheev): instead of always selecting the 0-th url - // select the url passed from the FileManager. - this.ribbon_.load(urls, urls[0], this.metadataProvider_); - this.parentDirEntry_ = parentDirEntry; + var urls = []; + var selectedURL; - this.initiateFading_(); -}; + // Convert canvas and blob items to blob urls. + for (var i = 0; i != items.length; i++) { + var item = items[i]; + var selected = (item == selectedItem); -Gallery.prototype.saveChanges_ = function(opt_callback) { - if (!this.editor_.isModified()) { - if (opt_callback) opt_callback(); - return; + if (item.constructor.name == 'HTMLCanvasElement') { + item = ImageEncoder.getBlob(item, + ImageEncoder.encodeMetadata({mimeType: 'image/jpeg'}, item), 1); + } + if (item.constructor.name == 'Blob' || + item.constructor.name == 'File') { + item = window.webkitURL.createObjectURL(item); + } + if (typeof item == 'string') { + urls.push(item); + if (selected) selectedURL = item; + } else { + console.error('Unsupported image type'); + } } - var currentItem = this.currentItem_; - - var metadataEncoder = this.editor_.encodeMetadata(); - var canvas = this.editor_.getBuffer().getContent().detachCanvas(); + if (urls.length == 0) + throw new Error('Cannot open the gallery for 0 items'); - currentItem.overrideContent(canvas, metadataEncoder.getMetadata()); + selectedURL = selectedURL || urls[0]; - if (currentItem.isFromLocalFile()) { - var newFile = currentItem.isOriginal(); - var name = currentItem.getCopyName(); + var self = this; - var self = this; + function initRibbon() { + self.currentItem_ = + self.ribbon_.load(urls, selectedURL, self.metadataProvider_); + // Flash the ribbon briefly to let the user know it is there. + self.initiateFading_(Gallery.FIRST_FADE_TIMEOUT); + } - function onSuccess(url) { - console.log('Saved from gallery', name); - // Force the metadata provider to reread the metadata from the file. - self.metadataProvider_.reset(url); - currentItem.onSaveSuccess(url); - if (opt_callback) opt_callback(); - } + // Initialize the ribbon only after the selected image is fully loaded. + this.metadataProvider_.fetch(selectedURL, function (metadata) { + self.editor_.openSession(selectedURL, metadata, 0, initRibbon); + }); +}; - function onError(error) { - console.log('Error saving from gallery', name, error); - currentItem.onSaveError(error); +Gallery.prototype.saveChanges_ = function(opt_callback) { + var item = this.currentItem_; + var self = this; + this.editor_.closeSession(function(canvas, modified) { + if (modified) { + item.save( + self.parentDirEntry_, self.metadataProvider_, canvas, opt_callback); + } else { if (opt_callback) opt_callback(); } - - this.parentDirEntry_.getFile( - name, {create: newFile, exclusive: newFile}, function(fileEntry) { - fileEntry.createWriter(function(fileWriter) { - function writeContent() { - fileWriter.onwriteend = onSuccess.bind(null, fileEntry.toURL()); - fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder)); - } - fileWriter.onerror = onError; - if (newFile) { - writeContent(); - } else { - fileWriter.onwriteend = writeContent; - fileWriter.truncate(0); - } - }, - onError); - }, onError); - } else { - // This branch is needed only for gallery_demo.js - currentItem.onSaveSuccess( - canvas.toDataURL(metadataEncoder.getMetadata().mimeType)); - if (opt_callback) opt_callback(); - } + }); }; Gallery.prototype.onActionExecute_ = function(action) { @@ -226,47 +259,32 @@ Gallery.prototype.onClose_ = function() { }; Gallery.prototype.onSelect_ = function(item) { - if (this.currentItem_) { - this.saveChanges_(); - } + if (this.currentItem_ == item) + return; + + this.saveChanges_(); + var slide = item.getIndex() - this.currentItem_.getIndex(); this.currentItem_ = item; - this.editor_.load(this.currentItem_.getContent(), - ImageUtil.deepCopy(this.currentItem_.getMetadata())); + this.editor_.openSession( + this.currentItem_.getContent(), this.currentItem_.getMetadata(), slide); }; -Gallery.prototype.onEdit_ = function(event) { - this.toolbar_.removeAttribute('hidden'); - - var self = this; +Gallery.prototype.onEdit_ = function() { if (this.editing_) { - this.editor_.onModeLeave(); - this.editBar_.setAttribute('hidden', 'hidden'); - this.editButton_.removeAttribute('pressed'); - this.editing_ = false; + this.editor_.leaveModeGently(); this.initiateFading_(); - window.setTimeout(function() { - // Hide the toolbar, so it will not overlap with buttons. - self.editBarMain_.setAttribute('hidden', 'hidden'); - self.ribbon_.redraw(); - }, 500); } else { this.cancelFading_(); - // Show the toolbar. - this.editBarMain_.removeAttribute('hidden'); - // Use setTimeout, so computed style will be recomputed. - window.setTimeout(function() { - self.editBar_.removeAttribute('hidden'); - self.editButton_.setAttribute('pressed', 'pressed'); - self.editing_ = true; - self.ribbon_.redraw(); - }, 0); } + this.editing_ = !this.editing_; + ImageUtil.setAttribute(this.container_, 'editing', this.editing_); + ImageUtil.setAttribute(this.editButton_, 'pressed', this.editing_); }; Gallery.prototype.onShare_ = function(event) { - this.toolbar_.removeAttribute('hidden'); + ImageUtil.setAttribute(this.container_, 'tools', true); if (this.sharing_) { this.shareMenu_.setAttribute('hidden', 'hidden'); @@ -277,198 +295,364 @@ Gallery.prototype.onShare_ = function(event) { }; Gallery.prototype.onKeyDown_ = function(event) { - if (this.editing_ || this.sharing_) + if (this.sharing_) + return; + + if (this.editing_ && this.editor_.onKeyDown(event)) return; + switch (event.keyIdentifier) { + case 'U+001B': // Escape + if (this.editing_) { + this.onEdit_(); + } else { + this.onClose_(); + } + break; + + case 'U+0045': // 'e' + if (!this.editing_) + this.onEdit_(); + break; + case 'Home': - this.ribbon_.scrollToFirst(); + this.ribbon_.selectFirst(); break; case 'Left': - this.ribbon_.scrollLeft(); + this.ribbon_.selectPrevious(); break; case 'Right': - this.ribbon_.scrollRight(); + this.ribbon_.selectNext(); break; case 'End': - this.ribbon_.scrollToLast(); + this.ribbon_.selectLast(); break; } }; Gallery.prototype.onMouseMove_ = function(e) { this.cancelFading_(); - this.toolbar_.removeAttribute('hidden'); this.initiateFading_(); }; Gallery.prototype.onFadeTimeout_ = function() { this.fadeTimeoutId_ = null; if (this.editing_ || this.sharing_) return; - this.toolbar_.setAttribute('hidden', 'hidden'); + this.container_.removeAttribute('tools'); }; -Gallery.prototype.initiateFading_ = function() { +Gallery.prototype.enableFading_ = function(on) { + this.fadingEnabled_ = on; + if (this.fadingEnabled_) + this.initiateFading_(); + else + this.cancelFading_(); +}; + +Gallery.prototype.initiateFading_ = function(opt_timeout) { + if (!this.fadingEnabled_) + return; + if (this.editing_ || this.sharing_ || this.fadeTimeoutId_) { return; } this.fadeTimeoutId_ = window.setTimeout( - this.onFadeTimeoutBound_, Gallery.FADE_TIMEOUT); + this.onFadeTimeoutBound_, opt_timeout || Gallery.FADE_TIMEOUT); }; Gallery.prototype.cancelFading_ = function() { + ImageUtil.setAttribute(this.container_, 'tools', true); + if (this.fadeTimeoutId_) { window.clearTimeout(this.fadeTimeoutId_); this.fadeTimeoutId_ = null; } }; -function Ribbon(parentNode, onSelect) { - this.container_ = parentNode; - this.document_ = parentNode.ownerDocument; +function Ribbon(container, onSelect, arrowLeft, arrowRight) { + this.container_ = container; + this.document_ = container.ownerDocument; + + this.arrowLeft_ = arrowLeft; + this.arrowLeft_.addEventListener('click', this.selectPrevious.bind(this)); - this.left_ = this.document_.createElement('div'); - this.left_.className = 'ribbon-left'; - this.left_.addEventListener('click', this.scrollLeft.bind(this)); - this.container_.appendChild(this.left_); + this.arrowRight_ = arrowRight; + this.arrowRight_.addEventListener('click', this.selectNext.bind(this)); + + this.fadeLeft_ = this.document_.createElement('div'); + this.fadeLeft_.className = 'fade left'; + this.container_.appendChild(this.fadeLeft_); this.bar_ = this.document_.createElement('div'); this.bar_.className = 'ribbon'; this.container_.appendChild(this.bar_); - this.right_ = this.document_.createElement('div'); - this.right_.className = 'ribbon-right'; - this.right_.addEventListener('click', this.scrollRight.bind(this)); - this.container_.appendChild(this.right_); - - this.spacer_ = this.document_.createElement('div'); - this.spacer_.className = 'ribbon-spacer'; + this.fadeRight_ = this.document_.createElement('div'); + this.fadeRight_.className = 'fade right'; + this.container_.appendChild(this.fadeRight_); this.onSelect_ = onSelect; this.items_ = []; - this.selectedItem_ = null; - this.firstIndex_ = 0; + this.selectedIndex_ = -1; + this.firstVisibleIndex_ = 0; + this.lastVisibleIndex_ = 0; } Ribbon.prototype.clear = function() { this.bar_.textContent = ''; this.items_ = []; - this.selectedItem_ = null; - this.firstIndex_ = 0; + this.selectedIndex_ = -1; + this.firstVisibleIndex_ = 0; + this.lastVisibleIndex_ = 0; }; -Ribbon.prototype.add = function(url, selected, metadataProvider) { +Ribbon.prototype.add = function(url, metadataProvider) { var index = this.items_.length; - var selectClosure = this.select.bind(this); - var item = - new Ribbon.Item(this.document_, url, selectClosure); + var selectClosure = this.select.bind(this, index); + var item = new Ribbon.Item(index, url, this.document_, selectClosure); this.items_.push(item); + var self = this; metadataProvider.fetch(url, function(metadata) { item.setMetadata(metadata); - if (selected) selectClosure(item); + if (item.isSelected()) { + self.onSelect_(item); + } }); + return item; }; -Ribbon.prototype.load = function(urls, selectedUrl, metadataProvider) { +Ribbon.prototype.load = function(urls, selectedURL, metadataProvider) { this.clear(); + var selectedIndex = -1; for (var index = 0; index < urls.length; ++index) { - this.add(urls[index], urls[index] == selectedUrl, metadataProvider); + var item = this.add(urls[index], metadataProvider); + if (urls[index] == selectedURL) + selectedIndex = index; } - + this.select(selectedIndex); window.setTimeout(this.redraw.bind(this), 0); + return this.items_[this.selectedIndex_]; }; -Ribbon.prototype.select = function(item) { - if (this.selectedItem_) { - this.selectedItem_.select(false); +Ribbon.prototype.select = function(index) { + if (index == this.selectedIndex_) + return; // Do not reselect. + + if (this.selectedIndex_ != -1) { + this.items_[this.selectedIndex_].select(false); } - this.selectedItem_ = item; + this.selectedIndex_ = index; + + if (this.selectedIndex_ < this.firstVisibleIndex_) { + if (this.selectedIndex_ == this.firstVisibleIndex_ - 1) { + this.scrollLeft(); + } else { + this.redraw(); + } + } + if (this.selectedIndex_ > this.lastVisibleIndex_) { + if (this.selectedIndex_ == this.lastVisibleIndex_ + 1) { + this.scrollRight(); + } else { + this.redraw(); + } + } - if (this.selectedItem_) { - this.selectedItem_.select(true); - if (this.onSelect_) - this.onSelect_(this.selectedItem_); + if (this.selectedIndex_ != -1) { + var selectedItem = this.items_[this.selectedIndex_]; + selectedItem.select(true); + if (selectedItem.getMetadata()) { + this.onSelect_(selectedItem); + } // otherwise onSelect is called from the metadata callback. } + + ImageUtil.setAttribute(this.arrowLeft_, 'active', this.selectedIndex_ > 0); + ImageUtil.setAttribute(this.arrowRight_, 'active', + this.selectedIndex_ + 1 < this.items_.length); + + ImageUtil.setAttribute(this.fadeLeft_, 'active', this.firstVisibleIndex_ > 0); + ImageUtil.setAttribute(this.fadeRight_, 'active', + this.lastVisibleIndex_ + 1 < this.items_.length); }; Ribbon.prototype.redraw = function() { this.bar_.textContent = ''; - // TODO(dgozman): get rid of these constants. - var itemWidth = 49; - var width = this.bar_.clientWidth - 40; + // The thumbnails are square. + var itemWidth = this.bar_.parentNode.clientHeight; + var width = this.bar_.parentNode.clientWidth; + + var fullItems = Math.floor(width / itemWidth); + + this.bar_.style.width = fullItems * itemWidth + 'px'; + + this.firstVisibleIndex_ = + Math.min(this.firstVisibleIndex_, this.selectedIndex_); + + this.lastVisibleIndex_ = + Math.min(this.items_.length, this.firstVisibleIndex_ + fullItems) - 1; + + this.lastVisibleIndex_ = + Math.max(this.lastVisibleIndex_, this.selectedIndex_); + + this.firstVisibleIndex_ = + Math.max(0, this.lastVisibleIndex_ - fullItems + 1); - var fit = Math.round(Math.floor(width / itemWidth)); - var lastIndex = Math.min(this.items_.length, this.firstIndex_ + fit); - this.firstIndex_ = Math.max(0, lastIndex - fit); - for (var index = this.firstIndex_; index < lastIndex; ++index) { + fullItems = this.lastVisibleIndex_ - this.firstVisibleIndex_ + 1; + + for (var index = this.firstVisibleIndex_; + index <= this.lastVisibleIndex_; + ++index) { this.bar_.appendChild(this.items_[index].getBox()); } - this.bar_.appendChild(this.spacer_); }; +// TODO(kaznacheev) The animation logic below does not really work for fast +// repetitive scrolling. + Ribbon.prototype.scrollLeft = function() { - if (this.firstIndex_ > 0) { - this.firstIndex_--; - this.redraw(); - } + console.log('scrollLeft'); + this.firstVisibleIndex_--; + var fadeIn = this.items_[this.firstVisibleIndex_].getBox(); + ImageUtil.setAttribute(fadeIn, 'shifted', true); + this.bar_.insertBefore(fadeIn, this.bar_.firstElementChild); + setTimeout( + function() { ImageUtil.setAttribute(fadeIn, 'shifted', false) }, + 0); + + var pushOut = this.bar_.lastElementChild; + pushOut.parentNode.removeChild(pushOut); + + this.lastVisibleIndex_--; }; Ribbon.prototype.scrollRight = function() { - if (this.firstIndex_ < this.items_.length - 1) { - this.firstIndex_++; - this.redraw(); - } + console.log('scrollRight'); + + var fadeOut = this.items_[this.firstVisibleIndex_].getBox(); + ImageUtil.setAttribute(fadeOut, 'shifted', true); + setTimeout(function() { + fadeOut.parentNode.removeChild(fadeOut); + ImageUtil.setAttribute(fadeOut, 'shifted', false); + }, 500); + + this.firstVisibleIndex_++; + this.lastVisibleIndex_++; + + var pushIn = this.items_[this.lastVisibleIndex_].getBox(); + this.bar_.appendChild(pushIn); }; -Ribbon.prototype.scrollToFirst = function() { - if (this.firstIndex_ > 0) { - this.firstIndex_ = 0; - this.redraw(); - } +Ribbon.prototype.selectPrevious = function() { + if (this.selectedIndex_ > 0) + this.select(this.selectedIndex_ - 1); }; -Ribbon.prototype.scrollToLast = function() { - if (this.firstIndex_ < this.items_.length - 1) { - this.firstIndex_ = this.items_.length - 1; - this.redraw(); - } +Ribbon.prototype.selectNext = function() { + if (this.selectedIndex_ < this.items_.length - 1) + this.select(this.selectedIndex_ + 1); }; +Ribbon.prototype.selectFirst = function() { + this.select(0); +}; -Ribbon.Item = function(document, url, selectClosure) { - this.url_ = url; +Ribbon.prototype.selectLast = function() { + this.select(this.items_.length - 1); +}; - this.img_ = document.createElement('img'); + +Ribbon.Item = function(index, url, document, selectClosure) { + this.index_ = index; + this.url_ = url; this.box_ = document.createElement('div'); this.box_.className = 'ribbon-image'; - this.box_.addEventListener('click', selectClosure.bind(null, this)); - this.box_.appendChild(this.img_); + this.box_.addEventListener('click', selectClosure); + + this.wrapper_ = document.createElement('div'); + this.wrapper_.className = 'image-wrapper'; + this.box_.appendChild(this.wrapper_); + + this.img_ = document.createElement('img'); + this.wrapper_.appendChild(this.img_); this.original_ = true; }; +Ribbon.Item.prototype.getIndex = function () { return this.index_ }; + Ribbon.Item.prototype.getBox = function () { return this.box_ }; Ribbon.Item.prototype.isOriginal = function () { return this.original_ }; Ribbon.Item.prototype.getUrl = function () { return this.url_ }; +Ribbon.Item.prototype.isSelected = function() { + return this.box_.hasAttribute('selected'); +}; + Ribbon.Item.prototype.select = function(on) { - if (on) - this.box_.setAttribute('selected', 'selected'); - else - this.box_.removeAttribute('selected'); + ImageUtil.setAttribute(this.box_, 'selected', on); }; -// TODO: Localize? -Ribbon.Item.COPY_SIGNATURE = '_Edited_'; +Ribbon.Item.prototype.save = function( + dirEntry, metadataProvider, canvas, opt_callback) { + var metadataEncoder = + ImageEncoder.encodeMetadata(this.getMetadata(), canvas, 1); + + this.overrideContent(canvas, metadataEncoder.getMetadata()); + + var self = this; + + if (!dirEntry) { // Happens only in gallery_demo.js + self.onSaveSuccess( + window.webkitURL.createObjectURL( + ImageEncoder.getBlob(canvas, metadataEncoder))); + if (opt_callback) opt_callback(); + return; + } + + var newFile = this.isOriginal(); + var name = this.getCopyName(); -Ribbon.Item.prototype.isFromLocalFile = function () { - return this.url_.indexOf('filesystem:') == 0; + function onSuccess(url) { + console.log('Saved from gallery', name); + // Force the metadata provider to reread the metadata from the file. + metadataProvider.reset(url); + self.onSaveSuccess(url); + if (opt_callback) opt_callback(); + } + + function onError(error) { + console.log('Error saving from gallery', name, error); + self.onSaveError(error); + if (opt_callback) opt_callback(); + } + + dirEntry.getFile( + name, {create: newFile, exclusive: newFile}, function(fileEntry) { + fileEntry.createWriter(function(fileWriter) { + function writeContent() { + fileWriter.onwriteend = onSuccess.bind(null, fileEntry.toURL()); + fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder)); + } + fileWriter.onerror = onError; + if (newFile) { + writeContent(); + } else { + fileWriter.onwriteend = writeContent; + fileWriter.truncate(0); + } + }, + onError); + }, onError); }; +// TODO: Localize? +Ribbon.Item.COPY_SIGNATURE = '_Edited_'; + Ribbon.Item.prototype.getCopyName = function () { // When saving a modified image we never overwrite the original file (the one // that existed prior to opening the Gallery. Instead we save to a file named @@ -553,11 +737,45 @@ Ribbon.Item.prototype.setMetadata = function(metadata) { metadata.thumbnailTransform : metadata.imageTransform; - this.box_.style.webkitTransform = transform ? + this.wrapper_.style.webkitTransform = transform ? ('scaleX(' + transform.scaleX + ') ' + 'scaleY(' + transform.scaleY + ') ' + 'rotate(' + transform.rotate90 * 90 + 'deg)') : ''; + function percent(ratio) { return Math.round(ratio * 100) + '%' } + + function resizeToFill(img, aspect) { + if ((aspect > 1)) { + img.style.height = percent(1); + img.style.width = percent(aspect); + img.style.marginLeft = percent((1 - aspect) / 2); + } else { + aspect = 1 / aspect; + img.style.width = percent(1); + img.style.height = percent(aspect); + img.style.marginTop = percent((1 - aspect) / 2); + } + } + + if (metadata.width && metadata.height) { + var aspect = metadata.width / metadata.height; + if (transform && transform.rotate90) { + aspect = 1 / aspect; + } + resizeToFill(this.img_, aspect); + } else { + // No metadata available, loading the thumbnail first, then adjust the size. + this.img_.maxWidth = '100%'; + this.img_.maxHeight = '100%'; + + var img = this.img_; + this.img_.onload = function() { + img.maxWidth = 'none'; + img.maxHeight = 'none'; + resizeToFill(img, img.width / img.height); + } + } + this.img_.setAttribute('src', metadata.thumbnailURL || this.url_); }; diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html b/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html index ced0553..d4c357dc 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html +++ b/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html @@ -5,10 +5,12 @@ --> <html> <head> + <script type="text/javascript" src="../metadata_provider.js"></script> <script type="text/javascript" src="gallery_demo.js"></script> <style type="text/css"> body { + -webkit-user-select: none; margin: 0 } @@ -18,8 +20,14 @@ border: none; } + .gallery-frame.chromebook { + width: 1280px; + height: 700px; + } + .debug-buttons { position: absolute; + color: white; left: 1px; top: 1px; } @@ -30,6 +38,11 @@ top: 28px; text-align: left; color: white; + opacity: 0.3; + } + + .debug-output:hover { + opacity: 1; } </style> @@ -37,12 +50,15 @@ <body> <iframe class="gallery-frame" scrolling="no" - src="gallery.html"> + src="gallery.html" + onload="loadGallery()"/> </iframe> <div class="debug-buttons"> - <button onclick="loadTestGrid()">Test grid</button> - <input type="file" multiple onchange="openFiles(this.files)"/> + <input type="checkbox" onchange="toggleSize(this);"/> + <span>Chromebook size</span> + <button onclick="loadGallery()">Test grid</button> + <input type="file" multiple onchange="loadGallery(this.files)"/> </div> <div class="debug-output"></div> diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.js b/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.js index 2788023..ab004d9 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.js +++ b/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.js @@ -2,40 +2,38 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -var mockDirEntry = { - getFile: function(name, options, onSuccess, onError) { - onError('File write not supported'); - } -}; +var metadataProvider = new MetadataProvider('../metadata_dispatcher.js'); + +function toggleSize(check) { + var iframe = document.querySelector('.gallery-frame'); + iframe.classList.toggle('chromebook'); +} -var mockMetadataProvider = { - fetch: function(url, callback) { - setTimeout(function(){ callback({mimeType: 'image/jpeg'}) }, 0); - }, +var mockActions = [ + { + title: 'Send', + iconUrl: 'http://google.com/favicon.ico', + execute: function() { alert('Sending is not supported') } + }]; - reset: function() {} -}; +function loadGallery(items) { + if (!items) items = [createTestGrid()]; -function initGallery(urls) { - var contentWindow = document.querySelector('.gallery-frame').contentWindow; + var iframe = document.querySelector('.gallery-frame'); + var contentWindow = iframe.contentWindow; contentWindow.ImageUtil.trace.bindToDOM( document.querySelector('.debug-output')); contentWindow.Gallery.open( - mockDirEntry, urls, function(){}, mockMetadataProvider); -} - -function openFiles(files) { - var urls = []; - for (var i = 0; i != files.length; i++) { - urls.push(window.webkitURL.createObjectURL(files[i])); - } - initGallery(urls); -} - -function loadTestGrid() { - initGallery([createTestGrid().toDataURL('image/jpeg')]); + null, // No local file access + items, + items[0], + function() {}, // Do nothing on Close + metadataProvider, + mockActions); + + iframe.focus(); } function createTestGrid() { diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js b/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js index cf6b47d..716cd96 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js +++ b/chrome/browser/resources/file_manager/js/image_editor/image_adjust.js @@ -9,6 +9,7 @@ */ ImageEditor.Mode.Adjust = function(arglist) { ImageEditor.Mode.apply(this, arguments); + this.implicitCommit = true; this.viewportGeneration_ = 0; }; @@ -18,47 +19,18 @@ ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype}; * ImageEditor.Mode methods overridden. */ -ImageEditor.Mode.Adjust.prototype.commit = function() { - if (!this.filter_) return; // Did not do anything yet. +ImageEditor.Mode.Adjust.prototype.getCommand = function() { + if (!this.filter_) return null; - // Applying the filter to the entire image takes some time, so we do - // it in small increments, providing visual feedback. - // TODO: provide modal progress indicator. - - // First hide the preview and show the original image. - this.repaint(); - - var self = this; - - function repaintStrip(fromRow, toRow) { - var imageStrip = new Rect(self.getViewport().getImageBounds()); - imageStrip.top = fromRow; - imageStrip.height = toRow - fromRow; - - var screenStrip = new Rect(self.getViewport().getImageBoundsOnScreen()); - screenStrip.top = Math.round(self.getViewport().imageToScreenY(fromRow)); - screenStrip.height = Math.round(self.getViewport().imageToScreenY(toRow)) - - screenStrip.top; + return new Command.Filter(this.name, this.filter_, this.message_); +}; - self.getBuffer().repaintScreenRect(screenStrip, imageStrip); +ImageEditor.Mode.Adjust.prototype.cleanUpUI = function() { + ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments); + if (this.canvas_) { + this.canvas_.parentNode.removeChild(this.canvas_); + this.canvas_ = null; } - - ImageUtil.trace.resetTimer('filter'); - - var lastUpdatedRow = 0; - - filter.applyByStrips( - this.getContent().getCanvas().getContext('2d'), - this.filter_, - function (updatedRow, rowCount) { - repaintStrip(lastUpdatedRow, updatedRow); - lastUpdatedRow = updatedRow; - if (updatedRow == rowCount) { - ImageUtil.trace.reportTimer('filter'); - self.getContent().invalidateCaches(); - self.repaint(); - } - }); }; ImageEditor.Mode.Adjust.prototype.cleanUpCaches = function() { @@ -67,15 +39,21 @@ ImageEditor.Mode.Adjust.prototype.cleanUpCaches = function() { }; ImageEditor.Mode.Adjust.prototype.update = function(options) { + ImageEditor.Mode.prototype.update.apply(this, arguments); + // We assume filter names are used in the UI directly. // This will have to change with i18n. this.filter_ = this.createFilter(options); - this.previewValid_ = false; - this.repaint(); + this.updatePreviewImage(); + ImageUtil.trace.resetTimer('preview'); + this.filter_(this.previewImageData_, this.originalImageData, 0, 0); + ImageUtil.trace.reportTimer('preview'); + this.canvas_.getContext('2d').putImageData( + this.previewImageData_, 0, 0); }; /** - * Clip and scale the source image data for the preview. + * Copy the source image data for the preview. * Use the cached copy if the viewport has not changed. */ ImageEditor.Mode.Adjust.prototype.updatePreviewImage = function() { @@ -83,48 +61,23 @@ ImageEditor.Mode.Adjust.prototype.updatePreviewImage = function() { this.viewportGeneration_ != this.getViewport().getCacheGeneration()) { this.viewportGeneration_ = this.getViewport().getCacheGeneration(); - var imageRect = this.getPreviewRect(this.getViewport().getImageClipped()); - var screenRect = this.getPreviewRect(this.getViewport().getScreenClipped()); - - // Copy the visible part of the image at the current screen scale. - var canvas = this.getContent().createBlankCanvas( - screenRect.width, screenRect.height); - var context = canvas.getContext('2d'); - Rect.drawImage(context, this.getContent().getCanvas(), null, imageRect); - this.originalImageData = - context.getImageData(0, 0, screenRect.width, screenRect.height); - this.previewImageData_ = - context.getImageData(0, 0, screenRect.width, screenRect.height); - this.previewValid_ = false; - } + if (!this.canvas_) { + var container = this.getImageView().container_; + this.canvas_ = container.ownerDocument.createElement('canvas'); + container.appendChild(this.canvas_); + } - if (this.filter_ && !this.previewValid_) { - ImageUtil.trace.resetTimer('preview'); - this.filter_(this.previewImageData_, this.originalImageData, 0, 0); - ImageUtil.trace.reportTimer('preview'); - this.previewValid_ = true; - } -}; + var screenClipped = this.getViewport().getScreenClipped(); -ImageEditor.Mode.Adjust.prototype.draw = function(context) { - this.updatePreviewImage(); + this.canvas_.style.left = screenClipped.left + 'px'; + this.canvas_.style.top = screenClipped.top + 'px'; + if (this.canvas_.width != screenClipped.width) + this.canvas_.width = screenClipped.width; + if (this.canvas_.height != screenClipped.height) + this.canvas_.height = screenClipped.height; - var screenClipped = this.getViewport().getScreenClipped(); - - var previewRect = this.getPreviewRect(screenClipped); - context.putImageData( - this.previewImageData_, previewRect.left, previewRect.top); - - if (previewRect.width < screenClipped.width && - previewRect.height < screenClipped.height) { - // Some part of the original image is not covered by the preview, - // shade it out. - context.globalAlpha = 0.75; - context.fillStyle = '#000000'; - context.strokeStyle = '#000000'; - Rect.fillBetween( - context, previewRect, this.getViewport().getScreenBounds()); - Rect.outline(context, previewRect); + this.originalImageData = this.getImageView().copyScreenImageData(); + this.previewImageData_ = this.getImageView().copyScreenImageData(); } }; @@ -136,21 +89,6 @@ ImageEditor.Mode.Adjust.prototype.createFilter = function(options) { return filter.create(this.name, options); }; -ImageEditor.Mode.Adjust.prototype.getPreviewRect = function(rect) { - if (this.getViewport().getScale() >= 1) { - return rect; - } else { - var bounds = this.getViewport().getImageBounds(); - var screen = this.getViewport().getScreenClipped(); - - screen = screen.inflate(-screen.width / 8, -screen.height / 8); - - return rect.inflate(-rect.width / 2, -rect.height / 2). - inflate(Math.min(screen.width, bounds.width) / 2, - Math.min(screen.height, bounds.height) / 2); - } -}; - /** * A base class for color filters that are scale independent (i.e. can * be applied to a scaled image with basicaly the same effect). @@ -166,12 +104,8 @@ ImageEditor.Mode.ColorFilter.prototype = ImageEditor.Mode.ColorFilter.prototype.setUp = function() { ImageEditor.Mode.Adjust.prototype.setUp.apply(this, arguments); - this.histogram_ = - new ImageEditor.Mode.Histogram(this.getViewport(), this.getContent()); -}; - -ImageEditor.Mode.ColorFilter.prototype.getPreviewRect = function(rect) { - return rect; + this.histogram_ = new ImageEditor.Mode.Histogram( + this.getViewport(), this.getImageView().getCanvas()); }; ImageEditor.Mode.ColorFilter.prototype.createFilter = function(options) { @@ -190,14 +124,16 @@ ImageEditor.Mode.ColorFilter.prototype.cleanUpUI = function() { * A histogram container. * @constructor */ -ImageEditor.Mode.Histogram = function(viewport, content) { +ImageEditor.Mode.Histogram = function(viewport, canvas) { this.viewport_ = viewport; - var canvas = content.getCanvas(); var downScale = Math.max(1, Math.sqrt(canvas.width * canvas.height / 10000)); - var thumbnail = content.copyCanvas(canvas.width / downScale, - canvas.height / downScale); + + var thumbnail = canvas.ownerDocument.createElement('canvas'); + thumbnail.width = canvas.width / downScale; + thumbnail.height = canvas.height / downScale; var context = thumbnail.getContext('2d'); + Rect.drawImage(context, canvas); this.originalImageData_ = context.getImageData(0, 0, thumbnail.width, thumbnail.height); @@ -282,8 +218,6 @@ ImageEditor.Mode.Exposure = function() { ImageEditor.Mode.Exposure.prototype = {__proto__: ImageEditor.Mode.ColorFilter.prototype}; -ImageEditor.Mode.register(ImageEditor.Mode.Exposure); - ImageEditor.Mode.Exposure.prototype.createTools = function(toolbar) { toolbar.addRange('brightness', -1, 0, 1, 100); toolbar.addRange('contrast', -1, 0, 1, 100); @@ -295,13 +229,12 @@ ImageEditor.Mode.Exposure.prototype.createTools = function(toolbar) { */ ImageEditor.Mode.Autofix = function() { ImageEditor.Mode.ColorFilter.call(this, 'autofix'); + this.message_ = 'fixed'; }; ImageEditor.Mode.Autofix.prototype = {__proto__: ImageEditor.Mode.ColorFilter.prototype}; -ImageEditor.Mode.register(ImageEditor.Mode.Autofix); - ImageEditor.Mode.Autofix.prototype.createTools = function(toolbar) { var self = this; toolbar.addButton('Apply', this.apply.bind(this)); @@ -317,12 +250,14 @@ ImageEditor.Mode.Autofix.prototype.apply = function() { */ ImageEditor.Mode.InstantAutofix = function() { ImageEditor.Mode.Autofix.apply(this, arguments); + this.instant = true; }; ImageEditor.Mode.InstantAutofix.prototype = {__proto__: ImageEditor.Mode.Autofix.prototype}; -ImageEditor.Mode.InstantAutofix.prototype.oneClick = function() { +ImageEditor.Mode.InstantAutofix.prototype.setUp = function() { + ImageEditor.Mode.Autofix.prototype.setUp.apply(this, arguments); this.apply(); }; @@ -337,8 +272,6 @@ ImageEditor.Mode.Blur = function() { ImageEditor.Mode.Blur.prototype = {__proto__: ImageEditor.Mode.Adjust.prototype}; -// TODO(dgozman): register Mode.Blur in v2. - ImageEditor.Mode.Blur.prototype.createTools = function(toolbar) { toolbar.addRange('strength', 0, 0, 1, 100); toolbar.addRange('radius', 1, 1, 3); @@ -355,8 +288,6 @@ ImageEditor.Mode.Sharpen = function() { ImageEditor.Mode.Sharpen.prototype = {__proto__: ImageEditor.Mode.Adjust.prototype}; -// TODO(dgozman): register Mode.Sharpen in v2. - ImageEditor.Mode.Sharpen.prototype.createTools = function(toolbar) { toolbar.addRange('strength', 0, 0, 1, 100); toolbar.addRange('radius', 1, 1, 3); diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_buffer.js b/chrome/browser/resources/file_manager/js/image_editor/image_buffer.js index 46fd65f..46c1407 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/image_buffer.js +++ b/chrome/browser/resources/file_manager/js/image_editor/image_buffer.js @@ -3,81 +3,16 @@ // found in the LICENSE file. /** - * The ImageBuffer object holds an offscreen canvas object and - * draws its content on the screen canvas applying scale and offset. - * Supports pluggable overlays that modify the image appearance and behavior. + * A stack of overlays that display itself and handle mouse events. + * TODO(kaznacheev) Consider disbanding this class and moving + * the functionality to individual objects that display anything or handle + * mouse events. * @constructor */ -function ImageBuffer(screenCanvas) { - this.screenCanvas_ = screenCanvas; - - this.viewport_ = new Viewport(this.repaint.bind(this)); - this.viewport_.setScreenSize(screenCanvas.width, screenCanvas.height); - - this.content_ = new ImageBuffer.Content( - this.viewport_, screenCanvas.ownerDocument); - +function ImageBuffer() { this.overlays_ = []; - this.addOverlay(new ImageBuffer.Margin(this.viewport_)); - this.addOverlay(this.content_); - // TODO(dgozman): consider adding overview in v2. } -ImageBuffer.prototype.getViewport = function() { return this.viewport_ }; - -ImageBuffer.prototype.getContent = function() { return this.content_ }; - -/** - * Loads the new content. - * A string parameter is treated as an image url. - * @param {String|HTMLImageElement|HTMLCanvasElement} source - * @param {{scaleX: number, scaleY: number, rotate90: number}} opt_transform - */ -ImageBuffer.prototype.load = function(source, opt_transform) { - if (typeof source == 'string') { - var self = this; - var image = new Image(); - image.onload = function(e) { self.load(e.target, opt_transform) }; - image.src = source; - } else { - this.content_.load(source, opt_transform); - this.repaint(); - } -}; - -ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) { - this.screenCanvas_.width = width; - this.screenCanvas_.height = height; - - var wasFitting = - this.viewport_.getScale() == this.viewport_.getFittingScale(); - - this.viewport_.setScreenSize(width, height); - - var minScale = this.viewport_.getFittingScale(); - if ((wasFitting && keepFitting) || this.viewport_.getScale() < minScale) { - this.viewport_.setScale(minScale, true); - } - this.repaint(); -}; - -/** - * Paints the content on the screen canvas taking the current scale and offset - * into account. - */ -ImageBuffer.prototype.repaint = function (opt_fromOverlay) { - this.viewport_.update(); - this.drawOverlays(this.screenCanvas_.getContext("2d"), opt_fromOverlay); -}; - -ImageBuffer.prototype.repaintScreenRect = function (screenRect, imageRect) { - Rect.drawImage( - this.screenCanvas_.getContext('2d'), - this.getContent().getCanvas(), - screenRect || this.getViewport().imageToScreenRect(screenRect), - imageRect || this.getViewport().screenToImageRect(screenRect)); -}; - /** * @param {ImageBuffer.Overlay} overlay */ @@ -106,18 +41,10 @@ ImageBuffer.prototype.removeOverlay = function (overlay) { /** * Draws overlays in the ascending Z-order. - * Skips overlays below opt_startFrom. */ -ImageBuffer.prototype.drawOverlays = function (context, opt_fromOverlay) { - var skip = true; +ImageBuffer.prototype.draw = function () { for (var i = 0; i != this.overlays_.length; i++) { - var overlay = this.overlays_[i]; - if (!opt_fromOverlay || opt_fromOverlay == overlay) skip = false; - if (skip) continue; - - context.save(); - overlay.draw(context); - context.restore(); + this.overlays_[i].draw(); } }; @@ -170,287 +97,4 @@ ImageBuffer.Overlay.prototype.getCursorStyle = function() { return null }; ImageBuffer.Overlay.prototype.onClick = function() { return false }; -ImageBuffer.Overlay.prototype.getDragHandler = function() { return null }; - - -/** - * The margin overlay draws the image outline and paints the margins. - */ -ImageBuffer.Margin = function(viewport) { - this.viewport_ = viewport; -}; - -ImageBuffer.Margin.prototype = {__proto__: ImageBuffer.Overlay.prototype}; - -// Draw below everything including the content. -ImageBuffer.Margin.prototype.getZIndex = function() { return -2 }; - -ImageBuffer.Margin.prototype.draw = function(context) { - context.save(); - context.fillStyle = '#000000'; - context.strokeStyle = '#000000'; - Rect.fillBetween(context, - this.viewport_.getImageBoundsOnScreen(), - this.viewport_.getScreenBounds()); - - Rect.outline(context, this.viewport_.getImageBoundsOnScreen()); - context.restore(); -}; - -/** - * The overlay containing the image. - */ -ImageBuffer.Content = function(viewport, document) { - this.viewport_ = viewport; - this.document_ = document; - - this.generation_ = 0; - - this.setCanvas(this.createBlankCanvas(0, 0)); -}; - -ImageBuffer.Content.prototype = {__proto__: ImageBuffer.Overlay.prototype}; - -// Draw below overlays with the default zIndex. -ImageBuffer.Content.prototype.getZIndex = function() { return -1 }; - -ImageBuffer.Content.prototype.draw = function(context) { - Rect.drawImage( - context, this.canvas_, this.viewport_.getImageBoundsOnScreen()); -}; - -ImageBuffer.Content.prototype.getCursorStyle = function (x, y, mouseDown) { - // Indicate that the image is draggable. - if (this.viewport_.isClipped() && - this.viewport_.getScreenClipped().inside(x, y)) - return 'move'; - - return null; -}; - -ImageBuffer.Content.prototype.getDragHandler = function (x, y) { - var cursor = this.getCursorStyle(x, y); - if (cursor == 'move') { - // Return the handler that drags the entire image. - return this.viewport_.createOffsetSetter(x, y); - } - - return null; -}; - -ImageBuffer.Content.prototype.getCacheGeneration = function() { - return this.generation_; -}; - -ImageBuffer.Content.prototype.invalidateCaches = function() { - this.generation_++; -}; - -ImageBuffer.Content.prototype.getCanvas = function() { return this.canvas_ }; - -ImageBuffer.Content.prototype.detachCanvas = function() { - var canvas = this.canvas_; - this.setCanvas(this.createBlankCanvas(0, 0)); - return canvas; -}; - -/** - * Replaces the off-screen canvas. - * To be used when the editor modifies the image dimensions. - * If the logical width/height are supplied they override the canvas dimensions - * and the canvas contents is scaled when displayed. - * @param {HTMLCanvasElement} canvas - * @param {number} opt_width Logical width (=canvas.width by default) - * @param {number} opt_height Logical height (=canvas.height by default) - */ -ImageBuffer.Content.prototype.setCanvas = function( - canvas, opt_width, opt_height) { - this.canvas_ = canvas; - this.viewport_.setImageSize(opt_width || canvas.width, - opt_height || canvas.height); - - this.invalidateCaches(); -}; - -/** - * @return {HTMLCanvasElement} A new blank canvas of the required size. - */ -ImageBuffer.Content.prototype.createBlankCanvas = function (width, height) { - var canvas = this.document_.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; -}; - -/** - * @param {number} opt_width Width of the copy, original width by default. - * @param {number} opt_height Height of the copy, original height by default. - * @return {HTMLCanvasElement} A new canvas with a copy of the content. - */ -ImageBuffer.Content.prototype.copyCanvas = function (opt_width, opt_height) { - var canvas = this.createBlankCanvas(opt_width || this.canvas_.width, - opt_height || this.canvas_.height); - Rect.drawImage(canvas.getContext('2d'), this.canvas_); - return canvas; -}; - -/** - * @return {ImageData} A new ImageData object with a copy of the content. - */ -ImageBuffer.Content.prototype.copyImageData = function (opt_width, opt_height) { - return this.canvas_.getContext("2d").getImageData( - 0, 0, opt_width || this.canvas_.width, opt_height || this.canvas_.height); -}; - -/** - * @param {HTMLImageElement|HTMLCanvasElement} image - * @param {{scaleX: number, scaleY: number, rotate90: number}} opt_transform - */ -ImageBuffer.Content.prototype.load = function(image, opt_transform) { - opt_transform = opt_transform || { scaleX: 1, scaleY: 1, rotate90: 0}; - - if (opt_transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions. - this.canvas_.width = image.height; - this.canvas_.height = image.width; - } else { - this.canvas_.width = image.width; - this.canvas_.height = image.height; - } - - this.clear(); - ImageUtil.drawImageTransformed( - this.canvas_, - image, - opt_transform.scaleX, - opt_transform.scaleY, - opt_transform.rotate90 * Math.PI / 2); - this.invalidateCaches(); - - this.viewport_.setImageSize(this.canvas_.width, this.canvas_.height); - this.viewport_.fitImage(); -}; - -ImageBuffer.Content.prototype.clear = function() { - var context = this.canvas_.getContext("2d"); - context.globalAlpha = 1; - context.fillStyle = '#FFFFFF'; - Rect.fill(context, new Rect(this.canvas_)); -}; - -/** - * @param {ImageData} imageData - */ -ImageBuffer.Content.prototype.drawImageData = function (imageData, x, y) { - this.canvas_.getContext("2d").putImageData(imageData, x, y); - this.invalidateCaches(); -}; - -/** - * The overview overlay draws the image thumbnail in the bottom right corner. - * Indicates the currently visible part. Supports panning by dragging. - */ -ImageBuffer.Overview = function(viewport, content) { - this.viewport_ = viewport; - this.content_ = content; - this.contentGeneration_ = 0; -}; - -ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype}; - -// Draw above everything. -ImageBuffer.Overview.prototype.getZIndex = function() { return 100 }; - -ImageBuffer.Overview.MAX_SIZE = 150; -ImageBuffer.Overview.RIGHT = 7; -ImageBuffer.Overview.BOTTOM = 50; - -ImageBuffer.Overview.prototype.update = function() { - var imageBounds = this.viewport_.getImageBounds(); - - if (this.contentGeneration_ != this.content_.getCacheGeneration()) { - this.contentGeneration_ = this.content_.getCacheGeneration(); - - var aspect = imageBounds.width / imageBounds.height; - - this.canvas_ = this.content_.copyCanvas( - ImageBuffer.Overview.MAX_SIZE * Math.min(aspect, 1), - ImageBuffer.Overview.MAX_SIZE / Math.max(aspect, 1)); - } - - this.bounds_ = null; - this.clipped_ = null; - - if (this.viewport_.isClipped()) { - var screenBounds = this.viewport_.getScreenBounds(); - - this.bounds_ = new Rect( - screenBounds.width - ImageBuffer.Overview.RIGHT - this.canvas_.width, - screenBounds.height - ImageBuffer.Overview.BOTTOM - this.canvas_.height, - this.canvas_.width, - this.canvas_.height); - - this.scale_ = this.bounds_.width / imageBounds.width; - - this.clipped_ = this.viewport_.getImageClipped(). - scale(this.scale_). - shift(this.bounds_.left, this.bounds_.top); - } -}; - -ImageBuffer.Overview.prototype.draw = function(context) { - this.update(); - - if (!this.clipped_) return; - - // Draw the thumbnail. - Rect.drawImage(context, this.canvas_, this.bounds_); - - // Draw the shadow over the off-screen part of the thumbnail. - context.globalAlpha = 0.3; - context.fillStyle = '#000000'; - Rect.fillBetween(context, this.clipped_, this.bounds_); - - // Outline the on-screen part of the thumbnail. - context.strokeStyle = '#FFFFFF'; - Rect.outline(context, this.clipped_); - - context.globalAlpha = 1; - // Draw the thumbnail border. - context.strokeStyle = '#000000'; - Rect.outline(context, this.bounds_); -}; - -ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) { - if (!this.bounds_ || !this.bounds_.inside(x, y)) return null; - - // Indicate that the on-screen part is draggable. - if (this.clipped_ && this.clipped_.inside(x, y)) return 'move'; - - // Indicate that the rest of the thumbnail is clickable. - return 'crosshair'; -}; - -ImageBuffer.Overview.prototype.onClick = function(x, y) { - if (this.getCursorStyle(x, y) != 'crosshair') return false; - this.viewport_.setCenter( - (x - this.bounds_.left) / this.scale_, - (y - this.bounds_.top) / this.scale_); - this.viewport_.repaint(); - return true; -}; - -ImageBuffer.Overview.prototype.getDragHandler = function(x, y) { - var cursor = this.getCursorStyle(x, y); - - if (cursor == 'move') { - var self = this; - function scale() { return -self.scale_;} - function hit(x, y) { return self.bounds_ && self.bounds_.inside(x, y); } - return this.viewport_.createOffsetSetter(x, y, scale, hit); - } else if (cursor == 'crosshair') { - // Force non-draggable behavior. - return function() {}; - } else { - return null; - } -}; +ImageBuffer.Overlay.prototype.getDragHandler = function() { return null };
\ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_editor.js b/chrome/browser/resources/file_manager/js/image_editor/image_editor.js index 8e89b37..8304e1c 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/image_editor.js +++ b/chrome/browser/resources/file_manager/js/image_editor/image_editor.js @@ -8,35 +8,30 @@ * @param {HTMLElement} container * @param {HTMLElement} mainToolbarContainer * @param {HTMLElement} modeToolbarContainer - * @param {Array.<ImageEditor.Mode>} tools + * @param {Array.<ImageEditor.Mode>} modes * @param {Object} displayStrings */ function ImageEditor( container, mainToolbarContainer, modeToolbarContainer, - tools, displayStrings) { + modes, displayStrings) { this.container_ = container; - this.tools_ = tools || ImageEditor.Mode.constructors; this.displayStrings_ = displayStrings; this.container_.innerHTML = ''; var document = this.container_.ownerDocument; - this.canvasWrapper_ = document.createElement('div'); - this.canvasWrapper_.className = 'canvas-wrapper'; - container.appendChild(this.canvasWrapper_); + this.viewport_ = new Viewport(); + this.viewport_.sizeByFrame(this.container_); - var canvas = document.createElement('canvas'); - this.canvasWrapper_.appendChild(canvas); - canvas.width = this.canvasWrapper_.clientWidth; - canvas.height = this.canvasWrapper_.clientHeight; + this.buffer_ = new ImageBuffer(); + this.viewport_.addRepaintCallback(this.buffer_.draw.bind(this.buffer_)); - this.buffer_ = new ImageBuffer(canvas); - this.modified_ = false; + this.imageView_ = new ImageView(this.container_, this.viewport_); + this.buffer_.addOverlay(this.imageView_); - // TODO(dgozman): consider adding a ScaleControl in v2. - - this.panControl_ = new ImageEditor.MouseControl(canvas, this.getBuffer()); + this.panControl_ = new ImageEditor.MouseControl( + this.container_, this.getBuffer()); this.mainToolbar_ = new ImageEditor.Toolbar( mainToolbarContainer, displayStrings); @@ -44,95 +39,132 @@ function ImageEditor( this.modeToolbar_ = new ImageEditor.Toolbar( modeToolbarContainer, displayStrings, this.onOptionsChange.bind(this)); - this.createToolButtons(); + this.prompt_ = new ImageEditor.Prompt( + this.container_, this.getDisplayString.bind(this)); + + this.createToolButtons(modes); + + this.commandQueue_ = null; } -/** - * Create an ImageEditor instance bound to a current web page, load the content. - * - * Use this method when image_editor.html is loaded into an iframe. - * - * @param {function(Blob)} saveCallback - * @param {function()} closeCallback - * @param {HTMLCanvasElement|HTMLImageElement|String} source - * @param {Object} opt_metadata - * @return {ImageEditor} - */ -ImageEditor.open = function(saveCallback, closeCallback, source, opt_metadata) { - var container = document.getElementsByClassName('image-editor')[0]; - var toolbar = document.getElementsByClassName('toolbar-container')[0]; - var editor = new ImageEditor(container, toolbar, saveCallback, closeCallback); - if (ImageEditor.resizeListener) { +ImageEditor.prototype.trackWindow = function(window) { + if (window.resizeListener) { // Make sure we do not leak the previous instance. - window.removeEventListener('resize', ImageEditor.resizeListener, false); + window.removeEventListener('resize', window.resizeListener, false); } - ImageEditor.resizeListener = editor.resizeFrame.bind(editor); - window.addEventListener('resize', ImageEditor.resizeListener, false); - editor.load(source, opt_metadata); - return editor; + window.resizeListener = this.resizeFrame.bind(this); + window.addEventListener('resize', window.resizeListener, false); }; -/** - * Loads a new image and its metadata. - * - * Takes into account the image orientation encoded in metadata. - * - * @param {HTMLCanvasElement|HTMLImageElement|String} source - * @param {Object} opt_metadata - */ -ImageEditor.prototype.load = function(source, opt_metadata) { - this.onModeLeave(); - this.originalSource_ = source; - this.originalMetadata_ = opt_metadata || {}; - this.getBuffer().load( - this.originalSource_, this.originalMetadata_.imageTransform); - this.modified_ = false; +ImageEditor.prototype.isLocked = function() { + return !this.commandQueue_ || this.commandQueue_.isBusy(); +}; + +ImageEditor.prototype.lockUI = function(on) { + ImageUtil.setAttribute(this.container_.parentNode, 'locked', on); + this.container_.style.cursor = on ? 'wait' : 'default'; + return true; }; -ImageEditor.prototype.reload = function() { - this.load(this.originalSource_, this.originalMetadata_); +ImageEditor.prototype.openSession = function( + source, metadata, slide, opt_callback) { + var self = this; + this.imageView_.load(source, metadata, slide, function() { + self.commandQueue_ = new CommandQueue( + self.container_.ownerDocument, self.imageView_.getCanvas()); + self.commandQueue_.attachUI( + self.getImageView(), self.getPrompt(), self.lockUI.bind(self)); + self.updateUndoRedo(); + if (opt_callback) opt_callback(); + }); }; /** - * Create a metadata encoder object that holds metadata corresponding to - * the current image. - * - * @param {number} quality + * @param {function(HTMLCanvasElement,boolean) opt_callback Passes the current + * image and the modified flag. */ -ImageEditor.prototype.encodeMetadata = function(quality) { - return ImageEncoder.encodeMetadata(this.originalMetadata_, - this.getBuffer().getContent().getCanvas(), quality || 1); +ImageEditor.prototype.closeSession = function(opt_callback) { + if (this.imageView_.isLoading()) { + this.imageView_.cancelLoad(); + return; + } + + if (!this.commandQueue_) return; + + this.leaveModeGently(); + + this.commandQueue_.detachUI(); + var detachedQueue = this.commandQueue_; + this.commandQueue_ = null; + + if (opt_callback) { + // The detached command queue can still be busy. Let it finish. + detachedQueue.requestCurrentImage(function(canvas) { + opt_callback(canvas, detachedQueue.canUndo()); + }); + } +}; + +ImageEditor.prototype.undo = function() { + if (this.commandQueue_.isBusy()) return; + this.getPrompt().hide(); + this.leaveMode(false); + this.commandQueue_.undo(); + this.updateUndoRedo(); }; -ImageEditor.prototype.isModified = function() { return this.modified_ }; +ImageEditor.prototype.redo = function() { + if (this.commandQueue_.isBusy()) return; + this.getPrompt().hide(); + this.leaveMode(false); + this.commandQueue_.redo(); + this.updateUndoRedo(); +}; + +ImageEditor.prototype.updateUndoRedo = function() { + var canUndo = this.commandQueue_ && this.commandQueue_.canUndo(); + var canRedo = this.commandQueue_ && this.commandQueue_.canRedo(); + ImageUtil.setAttribute(this.undoButton_, 'disabled', !canUndo); + ImageUtil.setAttribute(this.redoButton_, 'hidden', !canRedo); +}; + +ImageEditor.prototype.getCanvas = function() { + return this.getImageView().getCanvas(); +}; /** * Window resize handler. */ ImageEditor.prototype.resizeFrame = function() { - this.getBuffer().resizeScreen( - this.canvasWrapper_.clientWidth, this.canvasWrapper_.clientHeight, true); + this.getViewport().sizeByFrameAndFit(this.container_); + this.getViewport().repaint(); }; /** * @return {ImageBuffer} */ -ImageEditor.prototype.getBuffer = function () { - return this.buffer_; -}; +ImageEditor.prototype.getBuffer = function () { return this.buffer_ }; /** - * Destroys the UI and calls the close callback. + * @return {ImageView} */ -ImageEditor.prototype.close = function() { - this.container_.innerHTML = ''; - this.closeCallback_(); -}; +ImageEditor.prototype.getImageView = function () { return this.imageView_ }; + +/** + * @return {Viewport} + */ +ImageEditor.prototype.getViewport = function () { return this.viewport_ }; + +/** + * @return {ImageEditor.Prompt} + */ +ImageEditor.prototype.getPrompt = function () { return this.prompt_ }; ImageEditor.prototype.onOptionsChange = function(options) { ImageUtil.trace.resetTimer('update'); - if (this.currentMode_) + if (this.currentMode_) { this.currentMode_.update(options); + } ImageUtil.trace.reportTimer('update'); }; @@ -153,30 +185,20 @@ ImageEditor.Mode = function(name, displayName) { ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype }; -ImageEditor.Mode.prototype.getBuffer = function() { - return this.buffer_; -}; - -ImageEditor.Mode.prototype.repaint = function(opt_fromOverlay) { - return this.buffer_.repaint(opt_fromOverlay); -}; - -ImageEditor.Mode.prototype.getViewport = function() { - return this.viewport_; -}; +ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_ }; -ImageEditor.Mode.prototype.getContent = function() { - return this.content_; -}; +ImageEditor.Mode.prototype.getImageView = function() { return this.imageView_ }; /** * Called before entering the mode. */ -ImageEditor.Mode.prototype.setUp = function(buffer) { - this.buffer_ = buffer; - this.viewport_ = buffer.getViewport(); - this.content_ = buffer.getContent(); - this.buffer_.addOverlay(this); +ImageEditor.Mode.prototype.setUp = function(editor) { + this.editor_ = editor; + this.viewport_ = editor.getViewport(); + this.imageView_ = editor.getImageView(); + this.editor_.getBuffer().addOverlay(this); + + this.updated_ = false; }; /** @@ -188,7 +210,7 @@ ImageEditor.Mode.prototype.createTools = function(toolbar) {}; * Called before exiting the mode. */ ImageEditor.Mode.prototype.cleanUpUI = function() { - this.buffer_.removeOverlay(this); + this.editor_.getBuffer().removeOverlay(this); }; /** @@ -199,93 +221,140 @@ ImageEditor.Mode.prototype.cleanUpCaches = function() {}; /** * Called when any of the controls changed its value. */ -ImageEditor.Mode.prototype.update = function(options) {}; +ImageEditor.Mode.prototype.update = function(options) { + this.markUpdated(); +}; -/** - * The user clicked 'OK'. Finalize the change. - */ -ImageEditor.Mode.prototype.commit = function() {}; +ImageEditor.Mode.prototype.markUpdated = function() { + this.editor_.getPrompt().hide(); + this.updated_ = true; +}; /** - * The user clicker 'Reset' or 'Cancel'. Undo the change. + * One-click editor tool, requires no interaction, just executes the command. + * @param {string} name + * @param {Command} command + * @constructor */ -ImageEditor.Mode.prototype.rollback = function() {}; - +ImageEditor.Mode.OneClick = function(name, command) { + ImageEditor.Mode.call(this, name); + this.instant = true; + this.command_ = command; +}; -ImageEditor.Mode.constructors = []; +ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype}; -ImageEditor.Mode.register = function(constructor) { - ImageEditor.Mode.constructors.push(constructor); +ImageEditor.Mode.OneClick.prototype.getCommand = function() { + return this.command_; }; -ImageEditor.prototype.createToolButtons = function() { + +ImageEditor.prototype.createToolButtons = function(modes) { this.mainToolbar_.clear(); - for (var i = 0; i != this.tools_.length; i++) { - var mode = new this.tools_[i]; + for (var i = 0; i != modes.length; i++) { + var mode = modes[i]; this.mainToolbar_.addButton(this.getDisplayString(mode.name), - this.onModeEnter.bind(this, mode), mode.name); + this.enterMode.bind(this, mode), mode.name); } - this.mainToolbar_.addButton(this.getDisplayString('undo'), - this.reload.bind(this), 'undo'); + this.undoButton_ = this.mainToolbar_.addButton(this.getDisplayString('undo'), + this.undo.bind(this), 'undo'); + this.redoButton_ = this.mainToolbar_.addButton(this.getDisplayString('redo'), + this.redo.bind(this), 'redo'); }; +ImageEditor.prototype.isModal = function() { return !!this.currentMode_ }; + /** * The user clicked on the mode button. */ -ImageEditor.prototype.onModeEnter = function(mode, event) { - var previousMode = this.currentMode_; - this.onModeLeave(false); +ImageEditor.prototype.enterMode = function(mode, event) { + if (this.commandQueue_.isBusy()) return; + + if (this.currentMode_ == mode) { + // Currently active editor tool clicked, commit if modified. + this.leaveMode(this.currentMode_.updated_); + return; + } - if (previousMode == mode) return; + this.leaveModeGently(); this.currentTool_ = event.target; - this.currentTool_.setAttribute('pressed', 'pressed'); + + ImageUtil.setAttribute(this.currentTool_, 'pressed', true); this.currentMode_ = mode; - this.currentMode_.setUp(this.getBuffer()); + this.currentMode_.setUp(this); - if (this.currentMode_.oneClick) { - this.currentMode_.oneClick(); - this.onModeLeave(true); + if (this.currentMode_.instant) { // Instant tool. + this.leaveMode(true); return; } this.modeToolbar_.clear(); this.currentMode_.createTools(this.modeToolbar_); - this.modeToolbar_.addButton(this.getDisplayString('OK'), - this.onModeLeave.bind(this, true), 'mode', 'ok'), - this.modeToolbar_.addButton(this.getDisplayString('Cancel'), - this.onModeLeave.bind(this, false), 'mode', 'cancel'); - - this.modeToolbar_.show(this.currentTool_); - - this.getBuffer().repaint(); + this.modeToolbar_.show(true); + this.getPrompt().show('enter-when-done'); }; /** * The user clicked on 'OK' or 'Cancel' or on a different mode button. */ -ImageEditor.prototype.onModeLeave = function(save) { +ImageEditor.prototype.leaveMode = function(commit) { if (!this.currentMode_) return; - this.modeToolbar_.hide(); + if (!this.currentMode_.instant) { + this.getPrompt().hide(); + } + + this.modeToolbar_.show(false); this.currentMode_.cleanUpUI(); - if (save) { - this.currentMode_.commit(); - this.modified_ = true; - } else { - this.currentMode_.rollback(); + if (commit) { + var self = this; + this.commandQueue_.execute(this.currentMode_.getCommand()); + this.updateUndoRedo(); } this.currentMode_.cleanUpCaches(); this.currentMode_ = null; - this.currentTool_.removeAttribute('pressed'); + ImageUtil.setAttribute(this.currentTool_, 'pressed', false); this.currentTool_ = null; +}; - this.getBuffer().repaint(); - +ImageEditor.prototype.leaveModeGently = function() { + this.leaveMode(this.currentMode_ && + this.currentMode_.updated_ && + this.currentMode_.implicitCommit); +}; + +ImageEditor.prototype.onKeyDown = function(event) { + switch(event.keyIdentifier) { + case 'U+001B': // Escape + case 'Enter': + if (this.isModal()) { + this.leaveMode(event.keyIdentifier == 'Enter'); + return true; + } + break; + + case 'U+005A': // 'z' + if (event.ctrlKey) { + if (event.shiftKey) { + if (this.commandQueue_.canRedo()) { + this.redo(); + return true; + } + } else { + if (this.commandQueue_.canUndo()) { + this.undo(); + return true; + } + } + } + break; + } + return false; }; /** @@ -440,12 +509,12 @@ ImageEditor.ScaleControl.prototype.on1to1Button = function () { * A helper object for panning the ImageBuffer. * @constructor */ -ImageEditor.MouseControl = function(canvas, buffer) { - this.canvas_ = canvas; +ImageEditor.MouseControl = function(container, buffer) { + this.container_ = container; this.buffer_ = buffer; - canvas.addEventListener('mousedown', this.onMouseDown.bind(this), false); - canvas.addEventListener('mouseup', this.onMouseUp.bind(this), false); - canvas.addEventListener('mousemove', this.onMouseMove.bind(this), false); + container.addEventListener('mousedown', this.onMouseDown.bind(this), false); + container.addEventListener('mouseup', this.onMouseUp.bind(this), false); + container.addEventListener('mousemove', this.onMouseMove.bind(this), false); }; ImageEditor.MouseControl.getPosition = function(e) { @@ -461,8 +530,7 @@ ImageEditor.MouseControl.prototype.onMouseDown = function(e) { this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y); this.dragHappened_ = false; - this.canvas_.style.cursor = - this.buffer_.getCursorStyle(position.x, position.y, !!this.dragHandler_); + this.updateCursor_(position); e.preventDefault(); }; @@ -474,21 +542,39 @@ ImageEditor.MouseControl.prototype.onMouseUp = function(e) { } this.dragHandler_ = null; this.dragHappened_ = false; + this.lockMouse_(false); e.preventDefault(); }; ImageEditor.MouseControl.prototype.onMouseMove = function(e) { var position = ImageEditor.MouseControl.getPosition(e); - this.canvas_.style.cursor = - this.buffer_.getCursorStyle(position.x, position.y, !!this.dragHandler_); + if (this.dragHandler_ && !e.which) { + // mouseup must have happened while the mouse was outside our window. + this.dragHandler_ = null; + this.lockMouse_(false); + } + + this.updateCursor_(position); if (this.dragHandler_) { this.dragHandler_(position.x, position.y); this.dragHappened_ = true; + this.lockMouse_(true); } e.preventDefault(); }; +ImageEditor.MouseControl.prototype.lockMouse_ = function(on) { + ImageUtil.setAttribute(this.container_.parentNode, 'mousedrag', on); +}; + +ImageEditor.MouseControl.prototype.updateCursor_ = function(position) { + this.container_.style.cursor = + this.container_.parentNode.hasAttribute('locked') ? + '' : + this.buffer_.getCursorStyle(position.x, position.y, !!this.dragHandler_); +}; + /** * A toolbar for the ImageEditor. * @constructor @@ -523,11 +609,10 @@ ImageEditor.Toolbar.prototype.addLabel = function(text) { }; ImageEditor.Toolbar.prototype.addButton = function( - text, handler, opt_class1, opt_class2) { + text, handler, opt_class1) { var button = this.create_('div'); button.classList.add('button'); if (opt_class1) button.classList.add(opt_class1); - if (opt_class2) button.classList.add(opt_class2); button.textContent = this.getDisplayString(text); button.addEventListener('click', handler, false); return this.add(button); @@ -586,7 +671,7 @@ ImageEditor.Toolbar.prototype.addRange = function( var label = this.create_('div'); label.textContent = this.getDisplayString(name); - label.className = 'label'; + label.className = 'label ' + name; this.add(label); this.add(range); if (opt_showNumeric) this.add(numeric); @@ -609,19 +694,71 @@ ImageEditor.Toolbar.prototype.reset = function() { } }; -ImageEditor.Toolbar.prototype.show = function(parentButton) { - this.wrapper_.removeAttribute('hidden'); +ImageEditor.Toolbar.prototype.show = function(on) { + if (!this.wrapper_.firstChild) + return; // Do not show empty toolbar; + + ImageUtil.setAttribute(this.wrapper_, 'hidden', !on); +}; + +/** A prompt panel for the editor. + * + * @param {HTMLElement} container + */ +ImageEditor.Prompt = function(container, localizeFunction) { + this.container_ = container.parentNode; + this.localizeFunction_ = localizeFunction; +}; - this.wrapper_.style.left = '0'; +ImageEditor.Prompt.prototype.reset = function() { + this.cancelTimer(); + if (this.wrapper_) { + this.container_.removeChild(this.wrapper_); + this.wrapper_ = null; + this.prompt_ = null; + } +}; - var parentRect = parentButton.getBoundingClientRect(); - var wrapperRect = this.wrapper_.getBoundingClientRect(); +ImageEditor.Prompt.prototype.cancelTimer = function() { + if (this.timer_) { + clearTimeout(this.timer_); + this.timer_ = null; + } +}; - // Align the horizontal center of the toolbar with the center of the parent. - this.wrapper_.style.left = - (parentRect.left + (parentRect.width - wrapperRect.width) / 2) + 'px'; +ImageEditor.Prompt.prototype.setTimer = function(callback, timeout) { + this.cancelTimer(); + var self = this; + this.timer_ = setTimeout(function() { + self.timer_ = null; + callback(); + }, timeout); }; -ImageEditor.Toolbar.prototype.hide = function() { - this.wrapper_.setAttribute('hidden', 'hidden'); +ImageEditor.Prompt.prototype.show = function(text, timeout) { + this.reset(); + + var document = this.container_.ownerDocument; + this.wrapper_ = document.createElement('div'); + this.wrapper_.className = 'prompt-wrapper'; + this.container_.appendChild(this.wrapper_); + + this.prompt_ = document.createElement('div'); + this.prompt_.className = 'prompt'; + this.wrapper_.appendChild(this.prompt_); + + this.prompt_.textContent = this.localizeFunction_(text); + + setTimeout( + this.prompt_.setAttribute.bind(this.prompt_, 'state', 'fadein'), 0); + + if (timeout) + this.setTimer(this.hide.bind(this), timeout); }; + +ImageEditor.Prompt.prototype.hide = function() { + if (!this.prompt_) return; + this.prompt_.setAttribute('state', 'fadeout'); + // Allow some time for the animation to play out. + this.setTimer(this.reset.bind(this), 500); +};
\ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_encoder.js b/chrome/browser/resources/file_manager/js/image_editor/image_encoder.js index 5a43509..319fbff 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/image_encoder.js +++ b/chrome/browser/resources/file_manager/js/image_editor/image_encoder.js @@ -24,8 +24,9 @@ ImageEncoder.registerMetadataEncoder = function(constructor, mimeType) { * @return {ImageEncoder.MetadataEncoder} */ ImageEncoder.createMetadataEncoder = function(metadata) { - var constructor = ImageEncoder.metadataEncoders[metadata.mimeType]; - return constructor ? new constructor(ImageUtil.deepCopy(metadata)) : null; + var constructor = ImageEncoder.metadataEncoders[metadata.mimeType] || + ImageEncoder.MetadataEncoder; + return new constructor(metadata); }; @@ -41,7 +42,7 @@ ImageEncoder.encodeMetadata = function(metadata, canvas, quality) { var encoder = ImageEncoder.createMetadataEncoder(metadata); if (encoder) { encoder.setImageData(canvas); - ImageEncoder.encodeThumbnail(canvas, encoder, quality); + ImageEncoder.encodeThumbnail(canvas, encoder, quality || 1); } return encoder; }; @@ -55,10 +56,11 @@ ImageEncoder.encodeMetadata = function(metadata, canvas, quality) { * @return {Blob} */ ImageEncoder.getBlob = function(canvas, metadataEncoder, quality) { + var mimeType = metadataEncoder.getMetadata().mimeType; var blobBuilder = new WebKitBlobBuilder(); - ImageEncoder.buildBlob(blobBuilder, canvas, - metadataEncoder, metadataEncoder.getMetadata().mimeType, quality); - return blobBuilder.getBlob(); + ImageEncoder.buildBlob( + blobBuilder, canvas, metadataEncoder, mimeType, quality); + return blobBuilder.getBlob(mimeType); }; /** @@ -228,7 +230,9 @@ ImageEncoder.MetadataEncoder.prototype.setImageData = function(canvas) {}; * @param {String} dataUrl Data url containing the thumbnail. */ ImageEncoder.MetadataEncoder.prototype. - setThumbnailData = function(canvas, dataUrl) {}; + setThumbnailData = function(canvas, dataUrl) { + this.metadata_.thumbnailURL = dataUrl; +}; /** * Return a range where the metadata is (or should be) located. @@ -243,4 +247,6 @@ ImageEncoder.MetadataEncoder.prototype. * The return type is optimized for passing to Blob.append. * @return {ArrayBuffer} */ -ImageEncoder.MetadataEncoder.prototype.encode = function() { return null };
\ No newline at end of file +ImageEncoder.MetadataEncoder.prototype.encode = function() { + return new Uint8Array(0).buffer; +}; diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_transform.js b/chrome/browser/resources/file_manager/js/image_editor/image_transform.js index 3ae0db5..039114a 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/image_transform.js +++ b/chrome/browser/resources/file_manager/js/image_editor/image_transform.js @@ -3,366 +3,105 @@ // found in the LICENSE file. /** - * Resize mode. - */ - -ImageEditor.Mode.Resize = function() { - ImageEditor.Mode.call(this, 'resize'); -}; - -ImageEditor.Mode.Resize.prototype = {__proto__: ImageEditor.Mode.prototype}; - -// TODO(dgozman): register Mode.Resize in v2. - -ImageEditor.Mode.Resize.prototype.createTools = function(toolbar) { - var canvas = this.getContent().getCanvas(); - this.widthRange_ = - toolbar.addRange('width', 0, canvas.width, canvas.width * 2); - this.heightRange_ = - toolbar.addRange('height', 0, canvas.height, canvas.height * 2); -}; - -ImageEditor.Mode.Resize.prototype.commit = function() { - ImageUtil.trace.resetTimer('transform'); - var newCanvas = this.getContent().copyCanvas( - this.widthRange_.getValue(), this.heightRange_.getValue()); - ImageUtil.trace.reportTimer('transform'); - this.getContent().setCanvas(newCanvas); - this.getViewport().fitImage(); -}; - -/** - * Rotate mode. + * Crop mode. + * @constructor */ - -ImageEditor.Mode.Rotate = function() { - ImageEditor.Mode.call(this, 'rotate'); -}; - -ImageEditor.Mode.Rotate.prototype = {__proto__: ImageEditor.Mode.prototype}; - -ImageEditor.Mode.register(ImageEditor.Mode.Rotate); - -ImageEditor.Mode.Rotate.prototype.cleanUpCaches = function() { - this.backup_ = null; - this.transform_ = null; -}; - -ImageEditor.Mode.Rotate.prototype.commit = function() {}; - -ImageEditor.Mode.Rotate.prototype.rollback = function() { - if (this.backup_) { - this.getContent().setCanvas(this.backup_); - } -}; - -ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) { - toolbar.addButton("Left", this.modifyTransform.bind(this, 1, 1, 3)); - toolbar.addButton("Right", this.modifyTransform.bind(this, 1, 1, 1)); - toolbar.addButton("Flip V", this.modifyTransform.bind(this, 1, -1, 0)); - toolbar.addButton("Flip H", this.modifyTransform.bind(this, -1, 1, 0)); - - var srcCanvas = this.getContent().getCanvas(); - - var width = srcCanvas.width; - var height = srcCanvas.height; - var maxTg = Math.min(width / height, height / width); - var maxTilt = Math.floor(Math.atan(maxTg) * 180 / Math.PI); - this.tiltRange_ = - toolbar.addRange('angle', -maxTilt, 0, maxTilt, 10); - - this.tiltRange_. - addEventListener('mousedown', this.onTiltStart.bind(this), false); - this.tiltRange_. - addEventListener('mouseup', this.onTiltStop.bind(this), false); -}; - -ImageEditor.Mode.Rotate.prototype.getOriginal = function() { - if (!this.backup_) { - this.backup_ = this.getContent().getCanvas(); - } - return this.backup_; -}; - -ImageEditor.Mode.Rotate.prototype.getTransform = function() { - if (!this.transform_) { - this.transform_ = new ImageEditor.Mode.Rotate.Transform(); - } - return this.transform_; -}; - -ImageEditor.Mode.Rotate.prototype.onTiltStart = function() { - this.tiltDrag_ = true; - - var original = this.getOriginal(); - - // Downscale the original image to the overview thumbnail size. - var downScale = ImageBuffer.Overview.MAX_SIZE / - Math.max(original.width, original.height); - - this.preScaledOriginal_ = this.getContent().createBlankCanvas( - original.width * downScale, original.height * downScale); - Rect.drawImage(this.preScaledOriginal_.getContext('2d'), original); - - // Translate the current offset into the original image coordinate space. - var viewport = this.getViewport(); - var originalOffset = this.getTransform().transformOffsetToBaseline( - viewport.getOffsetX(), viewport.getOffsetY()); - - // Find the part of the original image that is sufficient to pre-render - // the rotation results. - var screenClipped = viewport.getScreenClipped(); - var diagonal = viewport.screenToImageSize( - Math.sqrt(screenClipped.width * screenClipped.width + - screenClipped.height * screenClipped.height)); - - var originalBounds = new Rect(original); - - var originalPreclipped = new Rect( - originalBounds.width / 2 - originalOffset.x - diagonal / 2, - originalBounds.height / 2 - originalOffset.y - diagonal / 2, - diagonal, - diagonal).clamp(originalBounds); - - // We assume that the scale is not changing during the mouse drag. - var scale = viewport.getScale(); - this.preClippedOriginal_ = this.getContent().createBlankCanvas( - originalPreclipped.width * scale, originalPreclipped.height * scale); - - Rect.drawImage(this.preClippedOriginal_.getContext('2d'), original, null, - originalPreclipped); - - this.repaint(); -}; - -ImageEditor.Mode.Rotate.prototype.onTiltStop = function() { - this.tiltDrag_ = false; - if (this.preScaledOriginal_) { - this.preScaledOriginal_ = false; - this.preClippedOriginal_ = false; - this.applyTransform(); - } else { - this.repaint(); - } -}; - -ImageEditor.Mode.Rotate.prototype.draw = function(context) { - if (!this.tiltDrag_) return; - - var screenClipped = this.getViewport().getScreenClipped(); - - if (this.preClippedOriginal_) { - ImageUtil.trace.resetTimer('preview'); - var transformed = this.getContent().createBlankCanvas( - screenClipped.width, screenClipped.height); - this.getTransform().apply(transformed, this.preClippedOriginal_); - Rect.drawImage(context, transformed, screenClipped); - ImageUtil.trace.reportTimer('preview'); - } - - const STEP = 50; - context.save(); - context.globalAlpha = 0.4; - context.strokeStyle = "#C0C0C0"; - - context.beginPath(); - var top = screenClipped.top + 0.5; - var left = screenClipped.left + 0.5; - for(var x = Math.ceil(screenClipped.left / STEP) * STEP; - x < screenClipped.left + screenClipped.width; - x += STEP) { - context.moveTo(x + 0.5, top); - context.lineTo(x + 0.5, top + screenClipped.height); - } - for(var y = Math.ceil(screenClipped.top / STEP) * STEP; - y < screenClipped.top + screenClipped.height; - y += STEP) { - context.moveTo(left, y + 0.5); - context.lineTo(left + screenClipped.width, y + 0.5); - } - context.closePath(); - context.stroke(); - - context.restore(); -}; - -ImageEditor.Mode.Rotate.prototype.modifyTransform = - function(scaleX, scaleY, turn90) { - - var transform = this.getTransform(); - var viewport = this.getViewport(); - - var baselineOffset = transform.transformOffsetToBaseline( - viewport.getOffsetX(), viewport.getOffsetY()); - - transform.modify(scaleX, scaleY, turn90, this.tiltRange_.getValue()); - - var newOffset = transform.transformOffsetFromBaseline( - baselineOffset.x, baselineOffset.y); - - // Ignoring offset clipping makes rotation behave more naturally. - viewport.setOffset(newOffset.x, newOffset.y, true /*ignore clipping*/); - - if (scaleX * scaleY < 0) { - this.tiltRange_.setValue(transform.tilt); - } - - this.applyTransform(); +ImageEditor.Mode.Crop = function() { + ImageEditor.Mode.call(this, 'crop'); }; -ImageEditor.Mode.Rotate.prototype.applyTransform = function() { - var srcCanvas = this.getOriginal(); - - var newSize = this.transform_.getTiltedRectSize( - srcCanvas.width, srcCanvas.height); - - var scale = 1; - - if (this.preScaledOriginal_) { - scale = this.preScaledOriginal_.width / srcCanvas.width; - srcCanvas = this.preScaledOriginal_; - } - - var dstCanvas = this.getContent().createBlankCanvas( - newSize.width * scale, newSize.height * scale); - ImageUtil.trace.resetTimer('transform'); - this.transform_.apply(dstCanvas, srcCanvas); - ImageUtil.trace.reportTimer('transform'); - this.getContent().setCanvas(dstCanvas, newSize.width, newSize.height); - - this.repaint(); -}; +ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype}; -ImageEditor.Mode.Rotate.prototype.update = function(values) { - this.modifyTransform(1, 1, 0); -}; +ImageEditor.Mode.Crop.prototype.setUp = function() { + ImageEditor.Mode.prototype.setUp.apply(this, arguments); -ImageEditor.Mode.Rotate.Transform = function() { - this.scaleX = 1; - this.scaleY = 1; - this.turn90 = 0; - this.tilt = 0; -}; + this.createDefaultCrop(); -ImageEditor.Mode.Rotate.Transform.prototype.modify = - function(scaleX, scaleY, turn90, tilt) { - this.scaleX *= scaleX; - this.scaleY *= scaleY; - this.turn90 += turn90; - this.tilt = (scaleX * scaleY > 0) ? tilt : -tilt; -}; + var container = this.getImageView().container_; + var doc = container.ownerDocument; -const DEG_IN_RADIAN = 180 / Math.PI; + this.domOverlay_ = doc.createElement('div'); + this.domOverlay_.className = 'crop-overlay'; + container.appendChild(this.domOverlay_); -ImageEditor.Mode.Rotate.Transform.prototype.getAngle = function() { - return (this.turn90 * 90 + this.tilt) / DEG_IN_RADIAN; -}; + this.shadowTop_ = doc.createElement('div'); + this.shadowTop_.className = 'shadow'; + this.domOverlay_.appendChild(this.shadowTop_); -ImageEditor.Mode.Rotate.Transform.prototype.transformOffsetFromBaseline = - function(x, y) { - var angle = this.getAngle(); - var sin = Math.sin(angle); - var cos = Math.cos(angle); + this.middleBox_ = doc.createElement('div'); + this.middleBox_.className = 'middle-box'; + this.domOverlay_.appendChild(this.middleBox_); - x *= this.scaleX; - y *= this.scaleY; + this.shadowLeft_ = doc.createElement('div'); + this.shadowLeft_.className = 'shadow'; + this.middleBox_.appendChild(this.shadowLeft_); - return { - x: (x * cos - y * sin), - y: (x * sin + y * cos) - }; -}; + this.cropFrame_ = doc.createElement('div'); + this.cropFrame_.className = 'crop-frame'; + this.middleBox_.appendChild(this.cropFrame_); -ImageEditor.Mode.Rotate.Transform.prototype.transformOffsetToBaseline = - function(x, y) { - var angle = -this.getAngle(); - var sin = Math.sin(angle); - var cos = Math.cos(angle); + this.shadowRight_ = doc.createElement('div'); + this.shadowRight_.className = 'shadow'; + this.middleBox_.appendChild(this.shadowRight_); - return { - x: (x * cos - y * sin) / this.scaleX, - y: (x * sin + y * cos) / this.scaleY - }; -}; + this.shadowBottom_ = doc.createElement('div'); + this.shadowBottom_.className = 'shadow'; + this.domOverlay_.appendChild(this.shadowBottom_); -ImageEditor.Mode.Rotate.Transform.prototype.getTiltedRectSize = - function(width, height) { - if (this.turn90 & 1) { - var temp = width; - width = height; - height = temp; + var cropFrame = this.cropFrame_; + function addCropFrame(className) { + var div = doc.createElement('div'); + div.className = className; + cropFrame.appendChild(div); } - var angle = Math.abs(this.tilt) / DEG_IN_RADIAN; + addCropFrame('left top corner'); + addCropFrame('top horizontal'); + addCropFrame('right top corner'); + addCropFrame('left vertical'); + addCropFrame('right vertical'); + addCropFrame('left bottom corner'); + addCropFrame('bottom horizontal'); + addCropFrame('right bottom corner'); - var sin = Math.sin(angle); - var cos = Math.cos(angle); - var denom = cos * cos - sin * sin; - - return { - width: Math.floor((width * cos - height * sin) / denom), - height: Math.floor((height * cos - width * sin) / denom) - } + this.positionDOM(); }; -ImageEditor.Mode.Rotate.Transform.prototype.apply = function( - dstCanvas, srcCanvas) { - ImageUtil.drawImageTransformed( - dstCanvas, srcCanvas, this.scaleX, this.scaleY, this.getAngle()); -}; +ImageEditor.Mode.Crop.prototype.positionDOM = function() { + var screenClipped = this.viewport_.getScreenClipped(); -/** - * Instant rotate. - * @constructor - */ -ImageEditor.Mode.InstantRotate = function() { - ImageEditor.Mode.Rotate.apply(this, arguments); -}; + this.domOverlay_.style.left = screenClipped.left + 'px'; + this.domOverlay_.style.top = screenClipped.top + 'px'; + this.domOverlay_.style.width = screenClipped.width + 'px'; + this.domOverlay_.style.height = screenClipped.height + 'px'; -ImageEditor.Mode.InstantRotate.prototype = - {__proto__: ImageEditor.Mode.Rotate.prototype}; + this.shadowLeft_.style.width = + this.viewport_.imageToScreenX(this.cropRect_.getLeft()) + - screenClipped.left + 'px'; -ImageEditor.Mode.InstantRotate.prototype.oneClick = function() { - this.tiltRange_ = { - getValue: function() { return 0 }, - setValue: function() {} - }; - this.modifyTransform(1, 1, 1); - this.getBuffer().getViewport().fitImage(); -}; + this.shadowTop_.style.height = + this.viewport_.imageToScreenY(this.cropRect_.getTop()) + - screenClipped.top + 'px'; -/** - * Crop mode. - */ + this.shadowRight_.style.width = screenClipped.left + screenClipped.width - + this.viewport_.imageToScreenX(this.cropRect_.getRight()) + 'px'; -ImageEditor.Mode.Crop = function() { - ImageEditor.Mode.call(this, 'crop'); + this.shadowBottom_.style.height = screenClipped.top + screenClipped.height - + this.viewport_.imageToScreenY(this.cropRect_.getBottom()) + 'px'; }; -ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype}; - -ImageEditor.Mode.register(ImageEditor.Mode.Crop); - -ImageEditor.Mode.Crop.prototype.createTools = function() { - this.createDefaultCrop(); +ImageEditor.Mode.Crop.prototype.cleanUpUI = function() { + ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments); + this.domOverlay_.parentNode.removeChild(this.domOverlay_); + this.domOverlay_ = null; }; -ImageEditor.Mode.Crop.GRAB_RADIUS = 5; +ImageEditor.Mode.Crop.GRAB_RADIUS = 6; -ImageEditor.Mode.Crop.prototype.commit = function() { +ImageEditor.Mode.Crop.prototype.getCommand = function() { var cropImageRect = this.cropRect_.getRect(); - - var newCanvas = this.getContent(). - createBlankCanvas(cropImageRect.width, cropImageRect.height); - - var newContext = newCanvas.getContext("2d"); - ImageUtil.trace.resetTimer('transform'); - Rect.drawImage(newContext, this.getContent().getCanvas(), - new Rect(newCanvas), cropImageRect); - ImageUtil.trace.reportTimer('transform'); - - this.getContent().setCanvas(newCanvas); - this.getViewport().fitImage(); + var cropScreenRect = this.viewport_.imageToScreenRect(cropImageRect); + return new Command.Crop(cropImageRect, cropScreenRect); }; ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() { @@ -373,49 +112,6 @@ ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() { rect, this.getViewport(), ImageEditor.Mode.Crop.GRAB_RADIUS); }; -ImageEditor.Mode.Crop.prototype.draw = function(context) { - var R = ImageEditor.Mode.Crop.GRAB_RADIUS; - - var inner = this.getViewport().imageToScreenRect(this.cropRect_.getRect()); - var outer = this.getViewport().getScreenClipped(); - - var inner_bottom = inner.top + inner.height; - var inner_right = inner.left + inner.width; - - context.globalAlpha = 0.25; - context.fillStyle = '#000000'; - Rect.fillBetween(context, inner, outer); - - context.fillStyle = '#FFFFFF'; - context.beginPath(); - context.moveTo(inner.left, inner.top); - context.arc(inner.left, inner.top, R, 0, Math.PI * 2); - context.moveTo(inner.left, inner_bottom); - context.arc(inner.left, inner_bottom, R, 0, Math.PI * 2); - context.moveTo(inner_right, inner.top); - context.arc(inner_right, inner.top, R, 0, Math.PI * 2); - context.moveTo(inner_right, inner_bottom); - context.arc(inner_right, inner_bottom, R, 0, Math.PI * 2); - context.closePath(); - context.fill(); - - context.globalAlpha = 0.5; - context.strokeStyle = '#FFFFFF'; - - context.beginPath(); - context.closePath(); - for (var i = 0; i <= 3; i++) { - var y = inner.top - 0.5 + Math.round((inner.height + 1) * i / 3); - context.moveTo(inner.left, y); - context.lineTo(inner.left + inner.width, y); - - var x = inner.left - 0.5 + Math.round((inner.width + 1) * i / 3); - context.moveTo(x, inner.top); - context.lineTo(x, inner.top + inner.height); - } - context.stroke(); -}; - ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) { return this.cropRect_.getCursorStyle(x, y, mouseDown); }; @@ -427,7 +123,8 @@ ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y) { var self = this; return function(x, y) { cropDragHandler(x, y); - self.repaint(); + self.markUpdated(); + self.positionDOM(); }; }; @@ -470,6 +167,22 @@ DraggableRect.TOP = 'top'; DraggableRect.BOTTOM = 'bottom'; DraggableRect.NONE = 'none'; +DraggableRect.prototype.getLeft = function () { + return this.bounds_[DraggableRect.LEFT]; +}; + +DraggableRect.prototype.getRight = function() { + return this.bounds_[DraggableRect.RIGHT]; +}; + +DraggableRect.prototype.getTop = function () { + return this.bounds_[DraggableRect.TOP]; +}; + +DraggableRect.prototype.getBottom = function() { + return this.bounds_[DraggableRect.BOTTOM]; +}; + DraggableRect.prototype.getRect = function() { return new Rect(this.bounds_) }; DraggableRect.prototype.getDragMode = function(x, y) { diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_util.js b/chrome/browser/resources/file_manager/js/image_editor/image_util.js index 4274f28..b37bbd0 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/image_util.js +++ b/chrome/browser/resources/file_manager/js/image_editor/image_util.js @@ -182,6 +182,10 @@ Rect.prototype.clamp = function(bounds) { return rect; }; +Rect.prototype.toString = function() { + return '(' + this.left + ',' + this.top + '):' + + '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')'; +}; /* * Useful shortcuts for drawing (static functions). */ @@ -275,7 +279,7 @@ ImageUtil.drawImageTransformed = function(dst, src, scaleX, scaleY, angle) { ImageUtil.deepCopy = function(obj) { - if (typeof obj != 'object') + if (obj == null || typeof obj != 'object') return obj; // Copy built-in types as is. var res; @@ -294,3 +298,89 @@ ImageUtil.deepCopy = function(obj) { } return res; }; + +ImageUtil.setAttribute = function(element, attribute, on) { + if (on) + element.setAttribute(attribute, attribute); + else + element.removeAttribute(attribute); +}; + +/** + * Load image into a canvas taking the transform into account. + * + * The source image is copied to the canvas stripe-by-stripe to avoid + * freezing up the UI. + * + * @param {HTMLCanvasElement} canvas + * @param {string|HTMLImageElement|HTMLCanvasElement} source + * @param {{scaleX: number, scaleY: number, rotate90: number}} transform + * @param {number} delay + * @param {function} callback + * @return {function()} Function to call to cancel the load. + */ +ImageUtil.loadImageAsync = function( + canvas, source, transform, delay, callback) { + var image; + var timeout = setTimeout(resolveURL, delay); + + function resolveURL() { + timeout = null; + if (typeof source == 'string') { + image = new Image(); + image.onload = function(e) { image = null; loadImage(e.target); }; + image.src = source; + } else { + loadImage(source); + } + } + + function loadImage(image) { + transform = transform || { scaleX: 1, scaleY: 1, rotate90: 0}; + + if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions. + canvas.width = image.height; + canvas.height = image.width; + } else { + canvas.width = image.width; + canvas.height = image.height; + } + + ImageUtil.trace.resetTimer('load-draw'); + + var context = canvas.getContext('2d'); + context.save(); + context.translate(context.canvas.width / 2, context.canvas.height / 2); + context.rotate(transform.rotate90 * Math.PI/2); + context.scale(transform.scaleX, transform.scaleY); + + var stripCount = Math.ceil (image.width * image.height / ( 1 << 20)); + var to = 0; + var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0; + + function copyStrip() { + var from = to; + to = Math.min (from + step, image.height); + + context.drawImage(image, + 0, from, image.width, to - from, + - image.width/2, from - image.height/2, image.width, to - from); + + if (to == image.height) { + context.restore(); + timeout = null; + ImageUtil.trace.reportTimer('load-draw'); + callback(); + } else { + timeout = setTimeout(copyStrip, 0); + } + } + + copyStrip(); + } + + return function () { + if (image) image.onload = function(){}; + if (timeout) clearTimeout(timeout); + }; +}; diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_view.js b/chrome/browser/resources/file_manager/js/image_editor/image_view.js new file mode 100644 index 0000000..0509cbe --- /dev/null +++ b/chrome/browser/resources/file_manager/js/image_editor/image_view.js @@ -0,0 +1,324 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * The overlay displaying the image. + */ +function ImageView(container, viewport) { + this.container_ = container; + this.viewport_ = viewport; + this.document_ = container.ownerDocument; + this.contentGeneration_ = 0; + this.displayedContentGeneration_ = 0; + this.displayedViewportGeneration_ = 0; +} + +ImageView.ANIMATION_DURATION = 180; +ImageView.ANIMATION_WAIT_INTERVAL = ImageView.ANIMATION_DURATION + 100; +ImageView.FAST_SCROLL_INTERVAL = 300; + +ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype}; + +// Draw below overlays with the default zIndex. +ImageView.prototype.getZIndex = function() { return -1 }; + +ImageView.prototype.draw = function() { + var forceRepaint = false; + + var screenClipped = this.viewport_.getScreenClipped(); + + if (this.displayedViewportGeneration_ != + this.viewport_.getCacheGeneration()) { + this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration(); + + if (this.screenCanvas_.width != screenClipped.width) + this.screenCanvas_.width = screenClipped.width; + + if (this.screenCanvas_.height != screenClipped.height) + this.screenCanvas_.height = screenClipped.height; + + this.screenCanvas_.style.left = screenClipped.left + 'px'; + this.screenCanvas_.style.top = screenClipped.top + 'px'; + + forceRepaint = true; + } + + if (forceRepaint || + this.displayedContentGeneration_ != this.contentGeneration_) { + this.displayedContentGeneration_ = this.contentGeneration_; + + ImageUtil.trace.resetTimer('paint'); + this.paintScreenRect( + screenClipped, this.contentCanvas_, this.viewport_.getImageClipped()); + ImageUtil.trace.reportTimer('paint'); + } +}; + +ImageView.prototype.getCursorStyle = function (x, y, mouseDown) { + // Indicate that the image is draggable. + if (this.viewport_.isClipped() && + this.viewport_.getScreenClipped().inside(x, y)) + return 'move'; + + return null; +}; + +ImageView.prototype.getDragHandler = function (x, y) { + var cursor = this.getCursorStyle(x, y); + if (cursor == 'move') { + // Return the handler that drags the entire image. + return this.viewport_.createOffsetSetter(x, y); + } + + return null; +}; + +ImageView.prototype.getCacheGeneration = function() { + return this.contentGeneration_; +}; + +ImageView.prototype.invalidateCaches = function() { + this.contentGeneration_++; +}; + +ImageView.prototype.getCanvas = function() { return this.contentCanvas_ }; + +ImageView.prototype.paintScreenRect = function (screenRect, canvas, imageRect) { + // Map screen canvas (0,0) to (screenClipped.left, screenClipped.top) + var screenClipped = this.viewport_.getScreenClipped(); + screenRect = screenRect.shift(-screenClipped.left, -screenClipped.top); + + // The source canvas may have different physical size than the image size + // set at the viewport. Adjust imageRect accordingly. + var bounds = this.viewport_.getImageBounds(); + var scaleX = canvas.width / bounds.width; + var scaleY = canvas.height / bounds.height; + imageRect = new Rect(imageRect.left * scaleX, imageRect.top * scaleY, + imageRect.width * scaleX, imageRect.height * scaleY); + Rect.drawImage( + this.screenCanvas_.getContext("2d"), canvas, screenRect, imageRect); +}; + +/** + * @return {ImageData} A new ImageData object with a copy of the content. + */ +ImageView.prototype.copyScreenImageData = function () { + return this.screenCanvas_.getContext("2d").getImageData( + 0, 0, this.screenCanvas_.width, this.screenCanvas_.height); +}; + +ImageView.prototype.isLoading = function() { + return this.cancelThumbnailLoad_ || this.cancelImageLoad_; +}; + +ImageView.prototype.cancelLoad = function() { + if (this.cancelThumbnailLoad_) { + this.cancelThumbnailLoad_(); + this.cancelThumbnailLoad_ = null; + } + if (this.cancelImageLoad_) { + this.cancelImageLoad_(); + this.cancelImageLoad_ = null; + } +}; + +/** + * Load and display a new image. + * + * Loads the thumbnail first, then replaces it with the main image. + * Takes into account the image orientation encoded in the metadata. + * + * @param {string|HTMLCanvasElement|HTMLImageElement} source + * @param {Object} metadata + * @param {Object} slide Slide-in animation direction. + * @param {function} opt_callback + */ +ImageView.prototype.load = function( + source, metadata, slide, opt_callback) { + + metadata = metadata|| {}; + + this.cancelLoad(); + + ImageUtil.trace.resetTimer('load'); + var canvas = this.container_.ownerDocument.createElement('canvas'); + + var self = this; + + if (metadata.thumbnailURL) { + this.cancelImageLoad_ = ImageUtil.loadImageAsync( + canvas, + metadata.thumbnailURL, + metadata.thumbnailTransform, + 0, /* no delay */ + displayThumbnail); + } else { + loadMainImage(0); + } + + function displayThumbnail() { + self.cancelThumbnailLoad_ = null; + + // The thumbnail may have different aspect ratio than the main image. + // Force the main image proportions to avoid flicker. + var time = Date.now(); + + var mainImageLoadDelay = ImageView.ANIMATION_DURATION; + + // Do not do slide-in animation when scrolling very fast. + if (self.lastLoadTime_ && + (time - self.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) { + slide = 0; + } + self.lastLoadTime_ = time; + + self.replace(canvas, slide, metadata.width, metadata.height); + if (!slide) mainImageLoadDelay = 0; + slide = 0; + loadMainImage(mainImageLoadDelay); + } + + function loadMainImage(delay) { + self.cancelImageLoad_ = ImageUtil.loadImageAsync( + canvas, source, metadata.imageTransform, delay, displayMainImage); + } + + function displayMainImage() { + self.cancelImageLoad_ = null; + self.replace(canvas, slide); + ImageUtil.trace.reportTimer('load'); + if (opt_callback) opt_callback(); + } +}; + +ImageView.prototype.replaceContent_ = function( + canvas, opt_reuseScreenCanvas, opt_width, opt_height) { + if (!opt_reuseScreenCanvas || !this.screenCanvas_) { + this.screenCanvas_ = this.document_.createElement('canvas'); + this.screenCanvas_.style.webkitTransitionDuration = + ImageView.ANIMATION_DURATION + 'ms'; + } + + this.contentCanvas_ = canvas; + this.invalidateCaches(); + this.viewport_.setImageSize( + opt_width || this.contentCanvas_.width, + opt_height || this.contentCanvas_.height); + this.viewport_.fitImage(); + this.viewport_.update(); + this.draw(); + + if (opt_reuseScreenCanvas && !this.screenCanvas_.parentNode) { + this.container_.appendChild(this.screenCanvas_); + } +}; + +/** + * Replace the displayed image, possibly with slide-in animation. + * + * @param {HTMLCanvasElement} canvas + * @param {number} opt_slide Slide-in animation direction. + * <0 for right-to-left, > 0 for left-to-right, 0 for no animation. + */ +ImageView.prototype.replace = function( + canvas, opt_slide, opt_width, opt_height) { + var oldScreenCanvas = this.screenCanvas_; + + this.replaceContent_(canvas, !opt_slide, opt_width, opt_height); + if (!opt_slide) return; + + var newScreenCanvas = this.screenCanvas_; + + function numToSlideAttr(num) { + return num < 0 ? 'left' : num > 0 ? 'right' : 'center'; + } + + newScreenCanvas.setAttribute('fade', numToSlideAttr(opt_slide)); + this.container_.appendChild(newScreenCanvas); + + setTimeout(function() { + newScreenCanvas.removeAttribute('fade'); + if (oldScreenCanvas) { + oldScreenCanvas.setAttribute('fade', numToSlideAttr(-opt_slide)); + setTimeout(function() { + oldScreenCanvas.parentNode.removeChild(oldScreenCanvas); + }, ImageView.ANIMATION_WAIT_INTERVAL); + } + }, 0); +}; + +ImageView.makeTransform = function(rect1, rect2, scale, rotate90) { + var shiftX = (rect1.left + rect1.width / 2) - (rect2.left + rect2.width / 2); + var shiftY = (rect1.top + rect1.height / 2) - (rect2.top + rect2.height / 2); + + return 'rotate(' + (rotate90 || 0) * 90 + 'deg) ' + + 'translate(' + shiftX + 'px,' + shiftY + 'px)' + + 'scaleX(' + scale + ') ' + + 'scaleY(' + scale + ')'; +}; + +/** + * Hide the old image instantly, animate the new image to visualize + * cropping and/or rotation. + */ +ImageView.prototype.replaceAndAnimate = function(canvas, cropRect, rotate90) { + cropRect = cropRect || this.viewport_.getScreenClipped(); + var oldScale = this.viewport_.getScale(); + + var oldScreenCanvas = this.screenCanvas_; + this.replaceContent_(canvas); + var newScreenCanvas = this.screenCanvas_; + + // Display the new canvas, initially transformed. + + // Transform instantly. + var duration = newScreenCanvas.style.webkitTransitionDuration; + newScreenCanvas.style.webkitTransitionDuration = '0ms'; + + newScreenCanvas.style.webkitTransform = ImageView.makeTransform( + cropRect, + this.viewport_.getScreenClipped(), + oldScale / this.viewport_.getScale(), + -rotate90); + + oldScreenCanvas.parentNode.appendChild(newScreenCanvas); + oldScreenCanvas.parentNode.removeChild(oldScreenCanvas); + + // Let the layout fire. + setTimeout(function() { + // Animated back to non-transformed state. + newScreenCanvas.style.webkitTransitionDuration = duration; + newScreenCanvas.style.webkitTransform = ''; + }, 0); +}; + +/** + * Shrink the given current image to the given crop rectangle while fading in + * the new image. + */ +ImageView.prototype.animateAndReplace = function(canvas, cropRect) { + var fullRect = this.viewport_.getScreenClipped(); + var oldScale = this.viewport_.getScale(); + + var oldScreenCanvas = this.screenCanvas_; + this.replaceContent_(canvas); + var newCanvas = this.screenCanvas_; + + newCanvas.setAttribute('fade', 'center'); + oldScreenCanvas.parentNode.insertBefore(newCanvas, oldScreenCanvas); + + // Animate to the transformed state. + + oldScreenCanvas.style.webkitTransform = ImageView.makeTransform( + cropRect, + fullRect, + this.viewport_.getScale() / oldScale, + 0); + + setTimeout(function() { newCanvas.removeAttribute('fade') }, 0); + + setTimeout(function() { + oldScreenCanvas.parentNode.removeChild(oldScreenCanvas); + }, ImageView.ANIMATION_WAIT_INTERVAL); +}; diff --git a/chrome/browser/resources/file_manager/js/image_editor/viewport.js b/chrome/browser/resources/file_manager/js/image_editor/viewport.js index d7282db..ff780ff 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/viewport.js +++ b/chrome/browser/resources/file_manager/js/image_editor/viewport.js @@ -5,9 +5,7 @@ /** * Viewport class controls the way the image is displayed (scale, offset etc). */ -function Viewport(repaintCallback) { - this.repaintCallback_ = repaintCallback; - +function Viewport() { this.imageBounds_ = new Rect(); this.screenBounds_ = new Rect(); @@ -18,7 +16,7 @@ function Viewport(repaintCallback) { this.generation_ = 0; this.scaleControl_ = null; - + this.repaintCallbacks_ = []; this.update(); } @@ -43,6 +41,19 @@ Viewport.prototype.setScreenSize = function(width, height) { this.invalidateCaches(); }; +Viewport.prototype.sizeByFrame = function(frame) { + this.setScreenSize(frame.clientWidth, frame.clientHeight); +}; + +Viewport.prototype.sizeByFrameAndFit = function(frame) { + var wasFitting = this.getScale() == this.getFittingScale(); + this.sizeByFrame(frame); + var minScale = this.getFittingScale(); + if (wasFitting || (this.getScale() < minScale)) { + this.setScale(minScale, true); + } +}; + Viewport.prototype.getScale = function() { return this.scale_ }; Viewport.prototype.setScale = function(scale, notify) { @@ -55,7 +66,7 @@ Viewport.prototype.setScale = function(scale, notify) { Viewport.prototype.getFittingScale = function() { var scaleX = this.screenBounds_.width / this.imageBounds_.width; var scaleY = this.screenBounds_.height / this.imageBounds_.height; - return Math.min(scaleX, scaleY) * 0.85; + return Math.min(scaleX, scaleY); }; Viewport.prototype.fitImage = function() { @@ -199,8 +210,8 @@ Viewport.prototype.imageToScreenRect = function(rect) { return new Rect( this.imageToScreenX(rect.left), this.imageToScreenY(rect.top), - this.imageToScreenSize(rect.width), - this.imageToScreenSize(rect.height)); + Math.round(this.imageToScreenSize(rect.width)), + Math.round(this.imageToScreenSize(rect.height))); }; /** @@ -277,6 +288,12 @@ Viewport.prototype.update = function() { } }; +Viewport.prototype.addRepaintCallback = function (callback) { + this.repaintCallbacks_.push(callback); +}; + Viewport.prototype.repaint = function () { - if (this.repaintCallback_) this.repaintCallback_(); + this.update(); + for (var i = 0; i != this.repaintCallbacks_.length; i++) + this.repaintCallbacks_[i](); }; diff --git a/chrome/browser/resources/file_manager/js/metadata_dispatcher.js b/chrome/browser/resources/file_manager/js/metadata_dispatcher.js index 65ac1bc..151d58c 100644 --- a/chrome/browser/resources/file_manager/js/metadata_dispatcher.js +++ b/chrome/browser/resources/file_manager/js/metadata_dispatcher.js @@ -21,7 +21,7 @@ function MetadataDispatcher() { importScripts('mpeg_parser.js'); importScripts('id3_parser.js'); - var patterns = []; + var patterns = ['blob:']; // We use blob urls in gallery_demo.js for (var i = 0; i < MetadataDispatcher.parserClasses_.length; i++) { var parserClass = MetadataDispatcher.parserClasses_[i]; @@ -160,6 +160,43 @@ MetadataDispatcher.prototype.processOneFile = function(fileURL, callback) { } ]; + if (fileURL.indexOf('blob:') == 0) { + // Blob urls require different steps: + steps = + [ // Read the blob into an array buffer and get the content type + function readBlob() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', fileURL, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function(e) { + if (xhr.status == 200) { + nextStep(xhr.getResponseHeader('Content-Type'), xhr.response); + } else { + onError('HTTP ' + xhr.status); + } + }; + xhr.send(); + }, + + // Step two, find the parser matching the content type. + function detectFormat(mimeType, arrayBuffer) { + for (var i = 0; i != self.parserInstances_.length; i++) { + var parser = self.parserInstances_[i]; + if (parser.mimeType && mimeType.match(parser.mimeType)) { + var blobBuilder = new WebKitBlobBuilder(); + blobBuilder.append(arrayBuffer); + nextStep(blobBuilder.getBlob(), parser); + return; + } + } + callback({mimeType: mimeType}); // Unrecognized mime type. + }, + + // Reuse the last step from the standard sequence. + steps[steps.length - 1] + ]; + } + nextStep(); }; diff --git a/chrome/browser/resources/file_manager/js/metadata_provider.js b/chrome/browser/resources/file_manager/js/metadata_provider.js index 1ec8325..8ae303d 100644 --- a/chrome/browser/resources/file_manager/js/metadata_provider.js +++ b/chrome/browser/resources/file_manager/js/metadata_provider.js @@ -2,13 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -function MetadataProvider() { +/** + * @param {string} opt_workerPath path to the worker source JS file. + */ +function MetadataProvider(opt_workerPath) { this.cache_ = {}; // Pass all URLs to the metadata reader until we have a correct filter. this.urlFilter = /.*/; - this.dispatcher_ = new Worker('js/metadata_dispatcher.js'); + this.dispatcher_ = new Worker(opt_workerPath || 'js/metadata_dispatcher.js'); this.dispatcher_.onmessage = this.onMessage_.bind(this); this.dispatcher_.postMessage({verb: 'init'}); // Initialization is not complete until the Worker sends back the diff --git a/chrome/browser/resources/file_manager/js/mock_chrome.js b/chrome/browser/resources/file_manager/js/mock_chrome.js index 395bbb7..2afd04d 100644 --- a/chrome/browser/resources/file_manager/js/mock_chrome.js +++ b/chrome/browser/resources/file_manager/js/mock_chrome.js @@ -89,8 +89,10 @@ chrome.fileBrowserPrivate = { regexp: /\.(zip)$/i, iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAYAAAAmL5yKAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sEEBcOAw9XftIAAADFSURBVCjPrZKxCsIwEIa/FHFwsvYxROjSQXAoqLiIL+xgBtvZ91A6uOnQc2hT0zRqkR4c3P25+/PfJTCwLU6wEpgBWkDXuInDPSwF5r7mJIeNQFTnIiCeONpVdYlLoK9wEUhNg8+B9FDVaZcgCKAovjTXfvPJFwGZtKW60pt8bOGBzfLouemnFY/MAs8wDeEI4NzaybewBu4AysKVgrK0gfe5iB9vjdAUqQ/S1Y/R3IX9Zc1zxc7zxe2/0Iskt7AsG0hhx14W8XV43FgV4gAAAABJRU5ErkJggg==' }, - { taskId: 'gallery', - title: 'Gallery', + { + internal: true, + taskId: 'mock|gallery', + title: 'View and edit', regexp: /\.(jpe?g|gif|png|cr2?|tiff)$/i, iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAYAAAAmL5yKAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sEEBcOAw9XftIAAADFSURBVCjPrZKxCsIwEIa/FHFwsvYxROjSQXAoqLiIL+xgBtvZ91A6uOnQc2hT0zRqkR4c3P25+/PfJTCwLU6wEpgBWkDXuInDPSwF5r7mJIeNQFTnIiCeONpVdYlLoK9wEUhNg8+B9FDVaZcgCKAovjTXfvPJFwGZtKW60pt8bOGBzfLouemnFY/MAs8wDeEI4NzaybewBu4AysKVgrK0gfe5iB9vjdAUqQ/S1Y/R3IX9Zc1zxc7zxe2/0Iskt7AsG0hhx14W8XV43FgV4gAAAABJRU5ErkJggg==' } @@ -231,7 +233,7 @@ chrome.fileBrowserPrivate = { UNMOUNT_ARCHIVE: 'Close archive', FORMAT_DEVICE: 'Format device', - GALLERY: 'Gallery', + GALLERY: 'View and edit', CONFIRM_OVERWRITE_FILE: 'A file named "$1" already exists. Do you want to replace it?', FILE_ALREADY_EXISTS: 'The file named "$1" already exists. Please choose a different name.', |