Supervised user whitelists: Cleanup
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / commands.js
blob07baabae78b30eddcc9f5d685ab249b640003513
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6 * Command queue is the only way to modify images.
7 * Supports undo/redo.
8 * Command execution is asynchronous (callback-based).
10 * @param {!Document} document Document to create canvases in.
11 * @param {!HTMLCanvasElement} canvas The canvas with the original image.
12 * @param {function(function())} saveFunction Function to save the image.
13 * @constructor
14 * @struct
16 function CommandQueue(document, canvas, saveFunction) {
17 this.document_ = document;
18 this.undo_ = [];
19 this.redo_ = [];
20 this.subscribers_ = [];
21 this.currentImage_ = canvas;
23 // Current image may be null or not-null but with width = height = 0.
24 // Copying an image with zero dimensions causes js errors.
25 if (this.currentImage_) {
26 this.baselineImage_ = document.createElement('canvas');
27 this.baselineImage_.width = this.currentImage_.width;
28 this.baselineImage_.height = this.currentImage_.height;
29 if (this.currentImage_.width > 0 && this.currentImage_.height > 0) {
30 var context = this.baselineImage_.getContext('2d');
31 context.drawImage(this.currentImage_, 0, 0);
33 } else {
34 this.baselineImage_ = null;
37 this.previousImage_ = document.createElement('canvas');
38 this.previousImageAvailable_ = false;
40 this.saveFunction_ = saveFunction;
41 this.busy_ = false;
42 this.UIContext_ = {};
45 /**
46 * Attach the UI elements to the command queue.
47 * Once the UI is attached the results of image manipulations are displayed.
49 * @param {!ImageView} imageView The ImageView object to display the results.
50 * @param {!ImageEditor.Prompt} prompt Prompt to use with this CommandQueue.
51 * @param {function(boolean)} lock Function to enable/disable buttons etc.
53 CommandQueue.prototype.attachUI = function(imageView, prompt, lock) {
54 this.UIContext_ = {
55 imageView: imageView,
56 prompt: prompt,
57 lock: lock
61 /**
62 * Execute the action when the queue is not busy.
63 * @param {function()} callback Callback.
65 CommandQueue.prototype.executeWhenReady = function(callback) {
66 if (this.isBusy())
67 this.subscribers_.push(callback);
68 else
69 setTimeout(callback, 0);
72 /**
73 * @return {boolean} True if the command queue is busy.
75 CommandQueue.prototype.isBusy = function() { return this.busy_ };
77 /**
78 * Set the queue state to busy. Lock the UI.
79 * @private
81 CommandQueue.prototype.setBusy_ = function() {
82 if (this.busy_)
83 throw new Error('CommandQueue already busy');
85 this.busy_ = true;
87 if (this.UIContext_.lock)
88 this.UIContext_.lock(true);
90 ImageUtil.trace.resetTimer('command-busy');
93 /**
94 * Set the queue state to not busy. Unlock the UI and execute pending actions.
95 * @private
97 CommandQueue.prototype.clearBusy_ = function() {
98 if (!this.busy_)
99 throw new Error('Inconsistent CommandQueue already not busy');
101 this.busy_ = false;
103 // Execute the actions requested while the queue was busy.
104 while (this.subscribers_.length)
105 this.subscribers_.shift()();
107 if (this.UIContext_.lock)
108 this.UIContext_.lock(false);
110 ImageUtil.trace.reportTimer('command-busy');
114 * Commit the image change: save and unlock the UI.
115 * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation).
116 * @private
118 CommandQueue.prototype.commit_ = function(opt_delay) {
119 setTimeout(this.saveFunction_.bind(null, this.clearBusy_.bind(this)),
120 opt_delay || 0);
124 * Internal function to execute the command in a given context.
126 * @param {!Command} command The command to execute.
127 * @param {!Object} uiContext The UI context.
128 * @param {function(number=)} callback Completion callback.
129 * @private
131 CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) {
132 if (!this.currentImage_)
133 throw new Error('Cannot operate on null image');
135 // Remember one previous image so that the first undo is as fast as possible.
136 this.previousImage_.width = this.currentImage_.width;
137 this.previousImage_.height = this.currentImage_.height;
138 this.previousImageAvailable_ = true;
139 var context = this.previousImage_.getContext('2d');
140 context.drawImage(this.currentImage_, 0, 0);
142 command.execute(
143 this.document_,
144 this.currentImage_,
146 * @type {function(HTMLCanvasElement, number=)}
148 (function(result, opt_delay) {
149 this.currentImage_ = result;
150 callback(opt_delay);
151 }.bind(this)),
152 uiContext);
156 * Executes the command.
158 * @param {!Command} command Command to execute.
159 * @param {boolean=} opt_keep_redo True if redo stack should not be cleared.
161 CommandQueue.prototype.execute = function(command, opt_keep_redo) {
162 this.setBusy_();
164 if (!opt_keep_redo)
165 this.redo_ = [];
167 this.undo_.push(command);
169 this.doExecute_(command, this.UIContext_, this.commit_.bind(this));
173 * @return {boolean} True if Undo is applicable.
175 CommandQueue.prototype.canUndo = function() {
176 return this.undo_.length != 0;
180 * Undo the most recent command.
182 CommandQueue.prototype.undo = function() {
183 if (!this.canUndo())
184 throw new Error('Cannot undo');
186 this.setBusy_();
188 var command = this.undo_.pop();
189 this.redo_.push(command);
191 var self = this;
193 function complete() {
194 var delay = command.revertView(
195 self.currentImage_, self.UIContext_.imageView);
196 self.commit_(delay);
199 if (this.previousImageAvailable_) {
200 // First undo after an execute call.
201 this.currentImage_.width = this.previousImage_.width;
202 this.currentImage_.height = this.previousImage_.height;
203 var context = this.currentImage_.getContext('2d');
204 context.drawImage(this.previousImage_, 0, 0);
206 // Free memory.
207 this.previousImage_.width = 0;
208 this.previousImage_.height = 0;
209 this.previousImageAvailable_ = false;
211 complete();
212 // TODO(kaznacheev) Consider recalculating previousImage_ right here
213 // by replaying the commands in the background.
214 } else {
215 this.currentImage_.width = this.baselineImage_.width;
216 this.currentImage_.height = this.baselineImage_.height;
217 var context = this.currentImage_.getContext('2d');
218 context.drawImage(this.baselineImage_, 0, 0);
220 var replay = function(index) {
221 if (index < self.undo_.length)
222 self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1));
223 else {
224 complete();
228 replay(0);
233 * @return {boolean} True if Redo is applicable.
235 CommandQueue.prototype.canRedo = function() {
236 return this.redo_.length != 0;
240 * Repeat the command that was recently un-done.
242 CommandQueue.prototype.redo = function() {
243 if (!this.canRedo())
244 throw new Error('Cannot redo');
246 this.execute(this.redo_.pop(), true);
250 * Closes internal buffers. Call to ensure, that internal buffers are freed
251 * as soon as possible.
253 CommandQueue.prototype.close = function() {
254 // Free memory used by the undo buffer.
255 this.previousImage_.width = 0;
256 this.previousImage_.height = 0;
257 this.previousImageAvailable_ = false;
259 if (this.baselineImage_) {
260 this.baselineImage_.width = 0;
261 this.baselineImage_.height = 0;
266 * Command object encapsulates an operation on an image and a way to visualize
267 * its result.
269 * @param {string} name Command name.
270 * @constructor
271 * @struct
273 function Command(name) {
274 this.name_ = name;
278 * @return {string} String representation of the command.
280 Command.prototype.toString = function() {
281 return 'Command ' + this.name_;
285 * Execute the command and visualize its results.
287 * The two actions are combined into one method because sometimes it is nice
288 * to be able to show partial results for slower operations.
290 * @param {!Document} document Document on which to execute command.
291 * @param {!HTMLCanvasElement} srcCanvas Canvas to execute on.
292 * @param {function(HTMLCanvasElement, number=)} callback Callback to call on
293 * completion.
294 * @param {!Object} uiContext Context to work in.
296 Command.prototype.execute = function(document, srcCanvas, callback, uiContext) {
297 console.error('Command.prototype.execute not implemented');
301 * Visualize reversion of the operation.
303 * @param {!HTMLCanvasElement} canvas Image data to use.
304 * @param {!ImageView} imageView ImageView to revert.
305 * @return {number} Animation duration in ms.
307 Command.prototype.revertView = function(canvas, imageView) {
308 imageView.replace(canvas);
309 return 0;
313 * Creates canvas to render on.
315 * @param {!Document} document Document to create canvas in.
316 * @param {!HTMLCanvasElement} srcCanvas to copy optional dimensions from.
317 * @param {number=} opt_width new canvas width.
318 * @param {number=} opt_height new canvas height.
319 * @return {!HTMLCanvasElement} Newly created canvas.
320 * @private
322 Command.prototype.createCanvas_ = function(
323 document, srcCanvas, opt_width, opt_height) {
324 var result = assertInstanceof(document.createElement('canvas'),
325 HTMLCanvasElement);
326 result.width = opt_width || srcCanvas.width;
327 result.height = opt_height || srcCanvas.height;
328 return result;
333 * Rotate command
334 * @param {number} rotate90 Rotation angle in 90 degree increments (signed).
335 * @constructor
336 * @extends {Command}
337 * @struct
339 Command.Rotate = function(rotate90) {
340 Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)');
341 this.rotate90_ = rotate90;
344 Command.Rotate.prototype = { __proto__: Command.prototype };
346 /** @override */
347 Command.Rotate.prototype.execute = function(
348 document, srcCanvas, callback, uiContext) {
349 var result = this.createCanvas_(
350 document,
351 srcCanvas,
352 (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width,
353 (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height);
354 ImageUtil.drawImageTransformed(
355 result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2);
356 var delay;
357 if (uiContext.imageView) {
358 delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_);
360 setTimeout(callback, 0, result, delay);
363 /** @override */
364 Command.Rotate.prototype.revertView = function(canvas, imageView) {
365 return imageView.replaceAndAnimate(canvas, null, -this.rotate90_);
370 * Crop command.
372 * @param {!ImageRect} imageRect Crop rectangle in image coordinates.
373 * @constructor
374 * @extends {Command}
375 * @struct
377 Command.Crop = function(imageRect) {
378 Command.call(this, 'crop' + imageRect.toString());
379 this.imageRect_ = imageRect;
382 Command.Crop.prototype = { __proto__: Command.prototype };
384 /** @override */
385 Command.Crop.prototype.execute = function(
386 document, srcCanvas, callback, uiContext) {
387 var result = this.createCanvas_(
388 document, srcCanvas, this.imageRect_.width, this.imageRect_.height);
389 var ctx = assertInstanceof(result.getContext('2d'), CanvasRenderingContext2D);
390 ImageRect.drawImage(ctx, srcCanvas, null, this.imageRect_);
391 var delay;
392 if (uiContext.imageView) {
393 delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0);
395 setTimeout(callback, 0, result, delay);
398 /** @override */
399 Command.Crop.prototype.revertView = function(canvas, imageView) {
400 return imageView.animateAndReplace(canvas, this.imageRect_);
405 * Filter command.
407 * @param {string} name Command name.
408 * @param {function(ImageData,ImageData,number,number)} filter Filter function.
409 * @param {?string} message Message to display when done.
410 * @constructor
411 * @extends {Command}
412 * @struct
414 Command.Filter = function(name, filter, message) {
415 Command.call(this, name);
416 this.filter_ = filter;
417 this.message_ = message;
420 Command.Filter.prototype = { __proto__: Command.prototype };
422 /** @override */
423 Command.Filter.prototype.execute = function(
424 document, srcCanvas, callback, uiContext) {
425 var result = this.createCanvas_(document, srcCanvas);
426 var self = this;
427 var previousRow = 0;
429 function onProgressVisible(updatedRow, rowCount) {
430 if (updatedRow == rowCount) {
431 uiContext.imageView.replace(result);
432 if (self.message_)
433 uiContext.prompt.show(self.message_, 2000);
434 callback(result);
435 } else {
436 var viewport = uiContext.imageView.viewport_;
438 var imageStrip = ImageRect.createFromBounds(viewport.getImageBounds());
439 imageStrip.top = previousRow;
440 imageStrip.height = updatedRow - previousRow;
442 var screenStrip = ImageRect.createFromBounds(
443 viewport.getImageBoundsOnScreen());
444 screenStrip.top = Math.round(viewport.imageToScreenY(previousRow));
445 screenStrip.height =
446 Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top;
448 uiContext.imageView.paintDeviceRect(result, imageStrip);
449 previousRow = updatedRow;
453 function onProgressInvisible(updatedRow, rowCount) {
454 if (updatedRow == rowCount) {
455 callback(result);
459 filter.applyByStrips(result, srcCanvas, this.filter_,
460 uiContext.imageView ? onProgressVisible : onProgressInvisible);