Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / image_editor / commands.js
1 // Copyright (c) 2012 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.
4
5 'use strict';
6
7 /**
8  * Command queue is the only way to modify images.
9  * Supports undo/redo.
10  * Command execution is asynchronous (callback-based).
11  *
12  * @param {Document} document Document to create canvases in.
13  * @param {HTMLCanvasElement} canvas The canvas with the original image.
14  * @param {function(callback)} saveFunction Function to save the image.
15  * @constructor
16  */
17 function CommandQueue(document, canvas, saveFunction) {
18   this.document_ = document;
19   this.undo_ = [];
20   this.redo_ = [];
21   this.subscribers_ = [];
22   this.currentImage_ = canvas;
23
24   // Current image may be null or not-null but with width = height = 0.
25   // Copying an image with zero dimensions causes js errors.
26   if (this.currentImage_) {
27     this.baselineImage_ = document.createElement('canvas');
28     this.baselineImage_.width = this.currentImage_.width;
29     this.baselineImage_.height = this.currentImage_.height;
30     if (this.currentImage_.width > 0 && this.currentImage_.height > 0) {
31       var context = this.baselineImage_.getContext('2d');
32       context.drawImage(this.currentImage_, 0, 0);
33     }
34   } else {
35     this.baselineImage_ = null;
36   }
37
38   this.previousImage_ = document.createElement('canvas');
39   this.previousImageAvailable_ = false;
40
41   this.saveFunction_ = saveFunction;
42   this.busy_ = false;
43   this.UIContext_ = {};
44 }
45
46 /**
47  * Attach the UI elements to the command queue.
48  * Once the UI is attached the results of image manipulations are displayed.
49  *
50  * @param {ImageView} imageView The ImageView object to display the results.
51  * @param {ImageEditor.Prompt} prompt Prompt to use with this CommandQueue.
52  * @param {function(boolean)} lock Function to enable/disable buttons etc.
53  */
54 CommandQueue.prototype.attachUI = function(imageView, prompt, lock) {
55   this.UIContext_ = {
56     imageView: imageView,
57     prompt: prompt,
58     lock: lock
59   };
60 };
61
62 /**
63  * Execute the action when the queue is not busy.
64  * @param {function} callback Callback.
65  */
66 CommandQueue.prototype.executeWhenReady = function(callback) {
67   if (this.isBusy())
68     this.subscribers_.push(callback);
69   else
70     setTimeout(callback, 0);
71 };
72
73 /**
74  * @return {boolean} True if the command queue is busy.
75  */
76 CommandQueue.prototype.isBusy = function() { return this.busy_ };
77
78 /**
79  * Set the queue state to busy. Lock the UI.
80  * @private
81  */
82 CommandQueue.prototype.setBusy_ = function() {
83   if (this.busy_)
84     throw new Error('CommandQueue already busy');
85
86   this.busy_ = true;
87
88   if (this.UIContext_.lock)
89     this.UIContext_.lock(true);
90
91   ImageUtil.trace.resetTimer('command-busy');
92 };
93
94 /**
95  * Set the queue state to not busy. Unlock the UI and execute pending actions.
96  * @private
97  */
98 CommandQueue.prototype.clearBusy_ = function() {
99   if (!this.busy_)
100     throw new Error('Inconsistent CommandQueue already not busy');
101
102   this.busy_ = false;
103
104   // Execute the actions requested while the queue was busy.
105   while (this.subscribers_.length)
106     this.subscribers_.shift()();
107
108   if (this.UIContext_.lock)
109     this.UIContext_.lock(false);
110
111   ImageUtil.trace.reportTimer('command-busy');
112 };
113
114 /**
115  * Commit the image change: save and unlock the UI.
116  * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation).
117  * @private
118  */
119 CommandQueue.prototype.commit_ = function(opt_delay) {
120   setTimeout(this.saveFunction_.bind(null, this.clearBusy_.bind(this)),
121       opt_delay || 0);
122 };
123
124 /**
125  * Internal function to execute the command in a given context.
126  *
127  * @param {Command} command The command to execute.
128  * @param {Object} uiContext The UI context.
129  * @param {function} callback Completion callback.
130  * @private
131  */
132 CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) {
133   if (!this.currentImage_)
134     throw new Error('Cannot operate on null image');
135
136   // Remember one previous image so that the first undo is as fast as possible.
137   this.previousImage_.width = this.currentImage_.width;
138   this.previousImage_.height = this.currentImage_.height;
139   this.previousImageAvailable_ = true;
140   var context = this.previousImage_.getContext('2d');
141   context.drawImage(this.currentImage_, 0, 0);
142
143   command.execute(
144       this.document_,
145       this.currentImage_,
146       function(result, opt_delay) {
147         this.currentImage_ = result;
148         callback(opt_delay);
149       }.bind(this),
150       uiContext);
151 };
152
153 /**
154  * Executes the command.
155  *
156  * @param {Command} command Command to execute.
157  * @param {boolean=} opt_keep_redo True if redo stack should not be cleared.
158  */
159 CommandQueue.prototype.execute = function(command, opt_keep_redo) {
160   this.setBusy_();
161
162   if (!opt_keep_redo)
163     this.redo_ = [];
164
165   this.undo_.push(command);
166
167   this.doExecute_(command, this.UIContext_, this.commit_.bind(this));
168 };
169
170 /**
171  * @return {boolean} True if Undo is applicable.
172  */
173 CommandQueue.prototype.canUndo = function() {
174   return this.undo_.length != 0;
175 };
176
177 /**
178  * Undo the most recent command.
179  */
180 CommandQueue.prototype.undo = function() {
181   if (!this.canUndo())
182     throw new Error('Cannot undo');
183
184   this.setBusy_();
185
186   var command = this.undo_.pop();
187   this.redo_.push(command);
188
189   var self = this;
190
191   function complete() {
192     var delay = command.revertView(
193         self.currentImage_, self.UIContext_.imageView);
194     self.commit_(delay);
195   }
196
197   if (this.previousImageAvailable_) {
198     // First undo after an execute call.
199     this.currentImage_.width = this.previousImage_.width;
200     this.currentImage_.height = this.previousImage_.height;
201     var context = this.currentImage_.getContext('2d');
202     context.drawImage(this.previousImage_, 0, 0);
203
204     // Free memory.
205     this.previousImage_.width = 0;
206     this.previousImage_.height = 0;
207     this.previousImageAvailable_ = false;
208
209     complete();
210     // TODO(kaznacheev) Consider recalculating previousImage_ right here
211     // by replaying the commands in the background.
212   } else {
213     this.currentImage_.width = this.baselineImage_.width;
214     this.currentImage_.height = this.baselineImage_.height;
215     var context = this.currentImage_.getContext('2d');
216     context.drawImage(this.baselineImage_, 0, 0);
217
218     var replay = function(index) {
219       if (index < self.undo_.length)
220         self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1));
221       else {
222         complete();
223       }
224     };
225
226     replay(0);
227   }
228 };
229
230 /**
231  * @return {boolean} True if Redo is applicable.
232  */
233 CommandQueue.prototype.canRedo = function() {
234   return this.redo_.length != 0;
235 };
236
237 /**
238  * Repeat the command that was recently un-done.
239  */
240 CommandQueue.prototype.redo = function() {
241   if (!this.canRedo())
242     throw new Error('Cannot redo');
243
244   this.execute(this.redo_.pop(), true);
245 };
246
247 /**
248  * Closes internal buffers. Call to ensure, that internal buffers are freed
249  * as soon as possible.
250  */
251 CommandQueue.prototype.close = function() {
252   // Free memory used by the undo buffer.
253   this.previousImage_.width = 0;
254   this.previousImage_.height = 0;
255   this.previousImageAvailable_ = false;
256
257   if (this.baselineImage_) {
258     this.baselineImage_.width = 0;
259     this.baselineImage_.height = 0;
260   }
261 };
262
263 /**
264  * Command object encapsulates an operation on an image and a way to visualize
265  * its result.
266  *
267  * @param {string} name Command name.
268  * @constructor
269  */
270 function Command(name) {
271   this.name_ = name;
272 }
273
274 /**
275  * @return {string} String representation of the command.
276  */
277 Command.prototype.toString = function() {
278   return 'Command ' + this.name_;
279 };
280
281 /**
282  * Execute the command and visualize its results.
283  *
284  * The two actions are combined into one method because sometimes it is nice
285  * to be able to show partial results for slower operations.
286  *
287  * @param {Document} document Document on which to execute command.
288  * @param {HTMLCanvasElement} srcCanvas Canvas to execute on.
289  * @param {function(HTMLCanvasElement, number)} callback Callback to call on
290  *   completion.
291  * @param {Object} uiContext Context to work in.
292  */
293 Command.prototype.execute = function(document, srcCanvas, callback, uiContext) {
294   console.error('Command.prototype.execute not implemented');
295 };
296
297 /**
298  * Visualize reversion of the operation.
299  *
300  * @param {HTMLCanvasElement} canvas Image data to use.
301  * @param {ImageView} imageView ImageView to revert.
302  * @return {number} Animation duration in ms.
303  */
304 Command.prototype.revertView = function(canvas, imageView) {
305   imageView.replace(canvas);
306   return 0;
307 };
308
309 /**
310  * Creates canvas to render on.
311  *
312  * @param {Document} document Document to create canvas in.
313  * @param {HTMLCanvasElement} srcCanvas to copy optional dimensions from.
314  * @param {number=} opt_width new canvas width.
315  * @param {number=} opt_height new canvas height.
316  * @return {HTMLCanvasElement} Newly created canvas.
317  * @private
318  */
319 Command.prototype.createCanvas_ = function(
320     document, srcCanvas, opt_width, opt_height) {
321   var result = document.createElement('canvas');
322   result.width = opt_width || srcCanvas.width;
323   result.height = opt_height || srcCanvas.height;
324   return result;
325 };
326
327
328 /**
329  * Rotate command
330  * @param {number} rotate90 Rotation angle in 90 degree increments (signed).
331  * @constructor
332  * @extends {Command}
333  */
334 Command.Rotate = function(rotate90) {
335   Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)');
336   this.rotate90_ = rotate90;
337 };
338
339 Command.Rotate.prototype = { __proto__: Command.prototype };
340
341 /** @override */
342 Command.Rotate.prototype.execute = function(
343     document, srcCanvas, callback, uiContext) {
344   var result = this.createCanvas_(
345       document,
346       srcCanvas,
347       (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width,
348       (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height);
349   ImageUtil.drawImageTransformed(
350       result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2);
351   var delay;
352   if (uiContext.imageView) {
353     delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_);
354   }
355   setTimeout(callback, 0, result, delay);
356 };
357
358 /** @override */
359 Command.Rotate.prototype.revertView = function(canvas, imageView) {
360   return imageView.replaceAndAnimate(canvas, null, -this.rotate90_);
361 };
362
363
364 /**
365  * Crop command.
366  *
367  * @param {Rect} imageRect Crop rectangle in image coordinates.
368  * @constructor
369  * @extends {Command}
370  */
371 Command.Crop = function(imageRect) {
372   Command.call(this, 'crop' + imageRect.toString());
373   this.imageRect_ = imageRect;
374 };
375
376 Command.Crop.prototype = { __proto__: Command.prototype };
377
378 /** @override */
379 Command.Crop.prototype.execute = function(
380     document, srcCanvas, callback, uiContext) {
381   var result = this.createCanvas_(
382       document, srcCanvas, this.imageRect_.width, this.imageRect_.height);
383   Rect.drawImage(result.getContext('2d'), srcCanvas, null, this.imageRect_);
384   var delay;
385   if (uiContext.imageView) {
386     delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0);
387   }
388   setTimeout(callback, 0, result, delay);
389 };
390
391 /** @override */
392 Command.Crop.prototype.revertView = function(canvas, imageView) {
393   return imageView.animateAndReplace(canvas, this.imageRect_);
394 };
395
396
397 /**
398  * Filter command.
399  *
400  * @param {string} name Command name.
401  * @param {function(ImageData,ImageData,number,number)} filter Filter function.
402  * @param {string} message Message to display when done.
403  * @constructor
404  * @extends {Command}
405  */
406 Command.Filter = function(name, filter, message) {
407   Command.call(this, name);
408   this.filter_ = filter;
409   this.message_ = message;
410 };
411
412 Command.Filter.prototype = { __proto__: Command.prototype };
413
414 /** @override */
415 Command.Filter.prototype.execute = function(
416     document, srcCanvas, callback, uiContext) {
417   var result = this.createCanvas_(document, srcCanvas);
418
419   var self = this;
420
421   var previousRow = 0;
422
423   function onProgressVisible(updatedRow, rowCount) {
424     if (updatedRow == rowCount) {
425       uiContext.imageView.replace(result);
426       if (self.message_)
427         uiContext.prompt.show(self.message_, 2000);
428       callback(result);
429     } else {
430       var viewport = uiContext.imageView.viewport_;
431
432       var imageStrip = new Rect(viewport.getImageBounds());
433       imageStrip.top = previousRow;
434       imageStrip.height = updatedRow - previousRow;
435
436       var screenStrip = new Rect(viewport.getImageBoundsOnScreen());
437       screenStrip.top = Math.round(viewport.imageToScreenY(previousRow));
438       screenStrip.height =
439           Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top;
440
441       uiContext.imageView.paintDeviceRect(
442           viewport.screenToDeviceRect(screenStrip), result, imageStrip);
443       previousRow = updatedRow;
444     }
445   }
446
447   function onProgressInvisible(updatedRow, rowCount) {
448     if (updatedRow == rowCount) {
449       callback(result);
450     }
451   }
452
453   filter.applyByStrips(result, srcCanvas, this.filter_,
454       uiContext.imageView ? onProgressVisible : onProgressInvisible);
455 };