Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / image_editor / image_editor.js
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.
4
5 /**
6  * ImageEditor is the top level object that holds together and connects
7  * everything needed for image editing.
8  *
9  * @param {Viewport} viewport The viewport.
10  * @param {ImageView} imageView The ImageView containing the images to edit.
11  * @param {ImageEditor.Prompt} prompt Prompt instance.
12  * @param {Object} DOMContainers Various DOM containers required for the editor.
13  * @param {Array.<ImageEditor.Mode>} modes Available editor modes.
14  * @param {function(string, ...[string])} displayStringFunction String
15  *     formatting function.
16  * @param {function()} onToolsVisibilityChanged Callback to be called, when
17  *     some of the UI elements have been dimmed or revealed.
18  * @constructor
19  */
20 function ImageEditor(
21     viewport, imageView, prompt, DOMContainers, modes, displayStringFunction,
22     onToolsVisibilityChanged) {
23   this.rootContainer_ = DOMContainers.root;
24   this.container_ = DOMContainers.image;
25   this.modes_ = modes;
26   this.displayStringFunction_ = displayStringFunction;
27   this.onToolsVisibilityChanged_ = onToolsVisibilityChanged;
28
29   ImageUtil.removeChildren(this.container_);
30
31   this.viewport_ = viewport;
32   this.viewport_.setScreenSize(
33       this.container_.clientWidth, this.container_.clientHeight);
34
35   this.imageView_ = imageView;
36   this.imageView_.addContentCallback(this.onContentUpdate_.bind(this));
37
38   this.buffer_ = new ImageBuffer();
39   this.buffer_.addOverlay(this.imageView_);
40
41   this.panControl_ = new ImageEditor.MouseControl(
42       this.rootContainer_, this.container_, this.getBuffer());
43   this.panControl_.setDoubleTapCallback(this.onDoubleTap_.bind(this));
44
45   this.mainToolbar_ = new ImageEditor.Toolbar(
46       DOMContainers.toolbar, displayStringFunction);
47
48   this.modeToolbar_ = new ImageEditor.Toolbar(
49       DOMContainers.mode, displayStringFunction,
50       this.onOptionsChange.bind(this));
51
52   this.prompt_ = prompt;
53
54   this.createToolButtons();
55
56   this.commandQueue_ = null;
57 }
58
59 /**
60  * @return {boolean} True if no user commands are to be accepted.
61  */
62 ImageEditor.prototype.isLocked = function() {
63   return !this.commandQueue_ || this.commandQueue_.isBusy();
64 };
65
66 /**
67  * @return {boolean} True if the command queue is busy.
68  */
69 ImageEditor.prototype.isBusy = function() {
70   return this.commandQueue_ && this.commandQueue_.isBusy();
71 };
72
73 /**
74  * Reflect the locked state of the editor in the UI.
75  * @param {boolean} on True if locked.
76  */
77 ImageEditor.prototype.lockUI = function(on) {
78   ImageUtil.setAttribute(this.rootContainer_, 'locked', on);
79 };
80
81 /**
82  * Report the tool use to the metrics subsystem.
83  * @param {string} name Action name.
84  */
85 ImageEditor.prototype.recordToolUse = function(name) {
86   ImageUtil.metrics.recordEnum(
87       ImageUtil.getMetricName('Tool'), name, this.actionNames_);
88 };
89
90 /**
91  * Content update handler.
92  * @private
93  */
94 ImageEditor.prototype.onContentUpdate_ = function() {
95   for (var i = 0; i != this.modes_.length; i++) {
96     var mode = this.modes_[i];
97     ImageUtil.setAttribute(mode.button_, 'disabled', !mode.isApplicable());
98   }
99 };
100
101 /**
102  * Open the editing session for a new image.
103  *
104  * @param {Gallery.Item} item Gallery item.
105  * @param {Object} effect Transition effect object.
106  * @param {function(function())} saveFunction Image save function.
107  * @param {function(number)} displayCallback Display callback.
108  * @param {function(number, number, *=)} loadCallback Load callback.
109  */
110 ImageEditor.prototype.openSession = function(
111     item, effect, saveFunction, displayCallback, loadCallback) {
112   if (this.commandQueue_)
113     throw new Error('Session not closed');
114
115   this.lockUI(true);
116
117   var self = this;
118   this.imageView_.load(
119       item, effect, displayCallback, function(loadType, delay, error) {
120         self.lockUI(false);
121         self.commandQueue_ = new CommandQueue(
122             self.container_.ownerDocument,
123             self.imageView_.getCanvas(),
124             saveFunction);
125         self.commandQueue_.attachUI(
126             self.getImageView(), self.getPrompt(), self.lockUI.bind(self));
127         self.updateUndoRedo();
128         loadCallback(loadType, delay, error);
129       });
130 };
131
132 /**
133  * Close the current image editing session.
134  * @param {function()} callback Callback.
135  */
136 ImageEditor.prototype.closeSession = function(callback) {
137   this.getPrompt().hide();
138   if (this.imageView_.isLoading()) {
139     if (this.commandQueue_) {
140       console.warn('Inconsistent image editor state');
141       this.commandQueue_ = null;
142     }
143     this.imageView_.cancelLoad();
144     this.lockUI(false);
145     callback();
146     return;
147   }
148   if (!this.commandQueue_) {
149     // Session is already closed.
150     callback();
151     return;
152   }
153
154   this.executeWhenReady(callback);
155   this.commandQueue_.close();
156   this.commandQueue_ = null;
157 };
158
159 /**
160  * Commit the current operation and execute the action.
161  *
162  * @param {function()} callback Callback.
163  */
164 ImageEditor.prototype.executeWhenReady = function(callback) {
165   if (this.commandQueue_) {
166     this.leaveModeGently();
167     this.commandQueue_.executeWhenReady(callback);
168   } else {
169     if (!this.imageView_.isLoading())
170       console.warn('Inconsistent image editor state');
171     callback();
172   }
173 };
174
175 /**
176  * @return {boolean} True if undo queue is not empty.
177  */
178 ImageEditor.prototype.canUndo = function() {
179   return this.commandQueue_ && this.commandQueue_.canUndo();
180 };
181
182 /**
183  * Undo the recently executed command.
184  */
185 ImageEditor.prototype.undo = function() {
186   if (this.isLocked()) return;
187   this.recordToolUse('undo');
188
189   // First undo click should dismiss the uncommitted modifications.
190   if (this.currentMode_ && this.currentMode_.isUpdated()) {
191     this.currentMode_.reset();
192     return;
193   }
194
195   this.getPrompt().hide();
196   this.leaveMode(false);
197   this.commandQueue_.undo();
198   this.updateUndoRedo();
199 };
200
201 /**
202  * Redo the recently un-done command.
203  */
204 ImageEditor.prototype.redo = function() {
205   if (this.isLocked()) return;
206   this.recordToolUse('redo');
207   this.getPrompt().hide();
208   this.leaveMode(false);
209   this.commandQueue_.redo();
210   this.updateUndoRedo();
211 };
212
213 /**
214  * Update Undo/Redo buttons state.
215  */
216 ImageEditor.prototype.updateUndoRedo = function() {
217   var canUndo = this.commandQueue_ && this.commandQueue_.canUndo();
218   var canRedo = this.commandQueue_ && this.commandQueue_.canRedo();
219   ImageUtil.setAttribute(this.undoButton_, 'disabled', !canUndo);
220   this.redoButton_.hidden = !canRedo;
221 };
222
223 /**
224  * @return {HTMLCanvasElement} The current image canvas.
225  */
226 ImageEditor.prototype.getCanvas = function() {
227   return this.getImageView().getCanvas();
228 };
229
230 /**
231  * @return {ImageBuffer} ImageBuffer instance.
232  */
233 ImageEditor.prototype.getBuffer = function() { return this.buffer_; };
234
235 /**
236  * @return {ImageView} ImageView instance.
237  */
238 ImageEditor.prototype.getImageView = function() { return this.imageView_; };
239
240 /**
241  * @return {Viewport} Viewport instance.
242  */
243 ImageEditor.prototype.getViewport = function() { return this.viewport_; };
244
245 /**
246  * @return {ImageEditor.Prompt} Prompt instance.
247  */
248 ImageEditor.prototype.getPrompt = function() { return this.prompt_; };
249
250 /**
251  * Handle the toolbar controls update.
252  * @param {Object} options A map of options.
253  */
254 ImageEditor.prototype.onOptionsChange = function(options) {
255   ImageUtil.trace.resetTimer('update');
256   if (this.currentMode_) {
257     this.currentMode_.update(options);
258   }
259   ImageUtil.trace.reportTimer('update');
260 };
261
262 /**
263  * ImageEditor.Mode represents a modal state dedicated to a specific operation.
264  * Inherits from ImageBuffer. Overlay to simplify the drawing of mode-specific
265  * tools.
266  *
267  * @param {string} name The mode name.
268  * @param {string} title The mode title.
269  * @constructor
270  */
271
272 ImageEditor.Mode = function(name, title) {
273   this.name = name;
274   this.title = title;
275   this.message_ = 'GALLERY_ENTER_WHEN_DONE';
276 };
277
278 ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype };
279
280 /**
281  * @return {Viewport} Viewport instance.
282  */
283 ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_; };
284
285 /**
286  * @return {ImageView} ImageView instance.
287  */
288 ImageEditor.Mode.prototype.getImageView = function() {
289   return this.imageView_;
290 };
291
292 /**
293  * @return {string} The mode-specific message to be displayed when entering.
294  */
295 ImageEditor.Mode.prototype.getMessage = function() { return this.message_; };
296
297 /**
298  * @return {boolean} True if the mode is applicable in the current context.
299  */
300 ImageEditor.Mode.prototype.isApplicable = function() { return true; };
301
302 /**
303  * Called once after creating the mode button.
304  *
305  * @param {ImageEditor} editor The editor instance.
306  * @param {HTMLElement} button The mode button.
307  */
308
309 ImageEditor.Mode.prototype.bind = function(editor, button) {
310   this.editor_ = editor;
311   this.editor_.registerAction_(this.name);
312   this.button_ = button;
313   this.viewport_ = editor.getViewport();
314   this.imageView_ = editor.getImageView();
315 };
316
317 /**
318  * Called before entering the mode.
319  */
320 ImageEditor.Mode.prototype.setUp = function() {
321   this.editor_.getBuffer().addOverlay(this);
322   this.updated_ = false;
323 };
324
325 /**
326  * Create mode-specific controls here.
327  * @param {ImageEditor.Toolbar} toolbar The toolbar to populate.
328  */
329 ImageEditor.Mode.prototype.createTools = function(toolbar) {};
330
331 /**
332  * Called before exiting the mode.
333  */
334 ImageEditor.Mode.prototype.cleanUpUI = function() {
335   this.editor_.getBuffer().removeOverlay(this);
336 };
337
338 /**
339  * Called after exiting the mode.
340  */
341 ImageEditor.Mode.prototype.cleanUpCaches = function() {};
342
343 /**
344  * Called when any of the controls changed its value.
345  * @param {Object} options A map of options.
346  */
347 ImageEditor.Mode.prototype.update = function(options) {
348   this.markUpdated();
349 };
350
351 /**
352  * Mark the editor mode as updated.
353  */
354 ImageEditor.Mode.prototype.markUpdated = function() {
355   this.updated_ = true;
356 };
357
358 /**
359  * @return {boolean} True if the mode controls changed.
360  */
361 ImageEditor.Mode.prototype.isUpdated = function() { return this.updated_; };
362
363 /**
364  * Resets the mode to a clean state.
365  */
366 ImageEditor.Mode.prototype.reset = function() {
367   this.editor_.modeToolbar_.reset();
368   this.updated_ = false;
369 };
370
371 /**
372  * One-click editor tool, requires no interaction, just executes the command.
373  *
374  * @param {string} name The mode name.
375  * @param {string} title The mode title.
376  * @param {Command} command The command to execute on click.
377  * @constructor
378  */
379 ImageEditor.Mode.OneClick = function(name, title, command) {
380   ImageEditor.Mode.call(this, name, title);
381   this.instant = true;
382   this.command_ = command;
383 };
384
385 ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype};
386
387 /**
388  * @return {Command} command.
389  */
390 ImageEditor.Mode.OneClick.prototype.getCommand = function() {
391   return this.command_;
392 };
393
394 /**
395  * Register the action name. Required for metrics reporting.
396  * @param {string} name Button name.
397  * @private
398  */
399 ImageEditor.prototype.registerAction_ = function(name) {
400   this.actionNames_.push(name);
401 };
402
403 /**
404  * Populate the toolbar.
405  */
406 ImageEditor.prototype.createToolButtons = function() {
407   this.mainToolbar_.clear();
408   this.actionNames_ = [];
409
410   var self = this;
411   function createButton(name, title, handler) {
412     return self.mainToolbar_.addButton(name,
413                                        title,
414                                        handler,
415                                        name /* opt_className */);
416   }
417
418   for (var i = 0; i != this.modes_.length; i++) {
419     var mode = this.modes_[i];
420     mode.bind(this, createButton(mode.name,
421                                  mode.title,
422                                  this.enterMode.bind(this, mode)));
423   }
424
425   this.undoButton_ = createButton('undo',
426                                   'GALLERY_UNDO',
427                                   this.undo.bind(this));
428   this.registerAction_('undo');
429
430   this.redoButton_ = createButton('redo',
431                                   'GALLERY_REDO',
432                                   this.redo.bind(this));
433   this.registerAction_('redo');
434 };
435
436 /**
437  * @return {ImageEditor.Mode} The current mode.
438  */
439 ImageEditor.prototype.getMode = function() { return this.currentMode_; };
440
441 /**
442  * The user clicked on the mode button.
443  *
444  * @param {ImageEditor.Mode} mode The new mode.
445  */
446 ImageEditor.prototype.enterMode = function(mode) {
447   if (this.isLocked()) return;
448
449   if (this.currentMode_ == mode) {
450     // Currently active editor tool clicked, commit if modified.
451     this.leaveMode(this.currentMode_.updated_);
452     return;
453   }
454
455   this.recordToolUse(mode.name);
456
457   this.leaveModeGently();
458   // The above call could have caused a commit which might have initiated
459   // an asynchronous command execution. Wait for it to complete, then proceed
460   // with the mode set up.
461   this.commandQueue_.executeWhenReady(this.setUpMode_.bind(this, mode));
462 };
463
464 /**
465  * Set up the new editing mode.
466  *
467  * @param {ImageEditor.Mode} mode The mode.
468  * @private
469  */
470 ImageEditor.prototype.setUpMode_ = function(mode) {
471   this.currentTool_ = mode.button_;
472
473   ImageUtil.setAttribute(this.currentTool_, 'pressed', true);
474
475   this.currentMode_ = mode;
476   this.currentMode_.setUp();
477
478   if (this.currentMode_.instant) {  // Instant tool.
479     this.leaveMode(true);
480     return;
481   }
482
483   this.getPrompt().show(this.currentMode_.getMessage());
484
485   this.modeToolbar_.clear();
486   this.currentMode_.createTools(this.modeToolbar_);
487   this.modeToolbar_.show(true);
488 };
489
490 /**
491  * The user clicked on 'OK' or 'Cancel' or on a different mode button.
492  * @param {boolean} commit True if commit is required.
493  */
494 ImageEditor.prototype.leaveMode = function(commit) {
495   if (!this.currentMode_) return;
496
497   if (!this.currentMode_.instant) {
498     this.getPrompt().hide();
499   }
500
501   this.modeToolbar_.show(false);
502
503   this.currentMode_.cleanUpUI();
504   if (commit) {
505     var self = this;
506     var command = this.currentMode_.getCommand();
507     if (command) {  // Could be null if the user did not do anything.
508       this.commandQueue_.execute(command);
509       this.updateUndoRedo();
510     }
511   }
512   this.currentMode_.cleanUpCaches();
513   this.currentMode_ = null;
514
515   ImageUtil.setAttribute(this.currentTool_, 'pressed', false);
516   this.currentTool_ = null;
517 };
518
519 /**
520  * Leave the mode, commit only if required by the current mode.
521  */
522 ImageEditor.prototype.leaveModeGently = function() {
523   this.leaveMode(this.currentMode_ &&
524                  this.currentMode_.updated_ &&
525                  this.currentMode_.implicitCommit);
526 };
527
528 /**
529  * Enter the editor mode with the given name.
530  *
531  * @param {string} name Mode name.
532  * @private
533  */
534 ImageEditor.prototype.enterModeByName_ = function(name) {
535   for (var i = 0; i != this.modes_.length; i++) {
536     var mode = this.modes_[i];
537     if (mode.name == name) {
538       if (!mode.button_.hasAttribute('disabled'))
539         this.enterMode(mode);
540       return;
541     }
542   }
543   console.error('Mode "' + name + '" not found.');
544 };
545
546 /**
547  * Key down handler.
548  * @param {Event} event The keydown event.
549  * @return {boolean} True if handled.
550  */
551 ImageEditor.prototype.onKeyDown = function(event) {
552   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
553     case 'U+001B': // Escape
554     case 'Enter':
555       if (this.getMode()) {
556         this.leaveMode(event.keyIdentifier == 'Enter');
557         return true;
558       }
559       break;
560
561     case 'Ctrl-U+005A':  // Ctrl+Z
562       if (this.commandQueue_.canUndo()) {
563         this.undo();
564         return true;
565       }
566       break;
567
568     case 'Ctrl-U+0059':  // Ctrl+Y
569       if (this.commandQueue_.canRedo()) {
570         this.redo();
571         return true;
572       }
573       break;
574
575     case 'U+0041':  // 'a'
576       this.enterModeByName_('autofix');
577       return true;
578
579     case 'U+0042':  // 'b'
580       this.enterModeByName_('exposure');
581       return true;
582
583     case 'U+0043':  // 'c'
584       this.enterModeByName_('crop');
585       return true;
586
587     case 'U+004C':  // 'l'
588       this.enterModeByName_('rotate_left');
589       return true;
590
591     case 'U+0052':  // 'r'
592       this.enterModeByName_('rotate_right');
593       return true;
594   }
595   return false;
596 };
597
598 /**
599  * Double tap handler.
600  * @param {number} x X coordinate of the event.
601  * @param {number} y Y coordinate of the event.
602  * @private
603  */
604 ImageEditor.prototype.onDoubleTap_ = function(x, y) {
605   if (this.getMode()) {
606     var action = this.buffer_.getDoubleTapAction(x, y);
607     if (action == ImageBuffer.DoubleTapAction.COMMIT)
608       this.leaveMode(true);
609     else if (action == ImageBuffer.DoubleTapAction.CANCEL)
610       this.leaveMode(false);
611   }
612 };
613
614 /**
615  * Hide the tools that overlap the given rectangular frame.
616  *
617  * @param {ImageRect} frame Hide the tool that overlaps this rect.
618  * @param {ImageRect} transparent But do not hide the tool that is completely
619  *     inside this rect.
620  */
621 ImageEditor.prototype.hideOverlappingTools = function(frame, transparent) {
622   var tools = this.rootContainer_.ownerDocument.querySelectorAll('.dimmable');
623   var changed = false;
624   for (var i = 0; i != tools.length; i++) {
625     var tool = tools[i];
626     var toolRect = tool.getBoundingClientRect();
627     var overlapping =
628         (frame && frame.intersects(toolRect)) &&
629         !(transparent && transparent.contains(toolRect));
630     if (overlapping && !tool.hasAttribute('dimmed') ||
631         !overlapping && tool.hasAttribute('dimmed')) {
632       ImageUtil.setAttribute(tool, 'dimmed', overlapping);
633       changed = true;
634     }
635   }
636   if (changed)
637     this.onToolsVisibilityChanged_();
638 };
639
640 /**
641  * A helper object for panning the ImageBuffer.
642  *
643  * @param {HTMLElement} rootContainer The top-level container.
644  * @param {HTMLElement} container The container for mouse events.
645  * @param {ImageBuffer} buffer Image buffer.
646  * @constructor
647  */
648 ImageEditor.MouseControl = function(rootContainer, container, buffer) {
649   this.rootContainer_ = rootContainer;
650   this.container_ = container;
651   this.buffer_ = buffer;
652
653   var handlers = {
654     'touchstart': this.onTouchStart,
655     'touchend': this.onTouchEnd,
656     'touchcancel': this.onTouchCancel,
657     'touchmove': this.onTouchMove,
658     'mousedown': this.onMouseDown,
659     'mouseup': this.onMouseUp
660   };
661
662   for (var eventName in handlers) {
663     container.addEventListener(
664         eventName, handlers[eventName].bind(this), false);
665   }
666
667   // Mouse move handler has to be attached to the window to receive events
668   // from outside of the window. See: http://crbug.com/155705
669   window.addEventListener('mousemove', this.onMouseMove.bind(this), false);
670 };
671
672 /**
673  * Maximum movement for touch to be detected as a tap (in pixels).
674  * @private
675  */
676 ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_ = 8;
677
678 /**
679  * Maximum time for touch to be detected as a tap (in milliseconds).
680  * @private
681  */
682 ImageEditor.MouseControl.MAX_TAP_DURATION_ = 500;
683
684 /**
685  * Maximum distance from the first tap to the second tap to be considered
686  * as a double tap.
687  * @private
688  */
689 ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_ = 32;
690
691 /**
692  * Maximum time for touch to be detected as a double tap (in milliseconds).
693  * @private
694  */
695 ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_ = 1000;
696
697 /**
698  * Returns an event's position.
699  *
700  * @param {MouseEvent|Touch} e Pointer position.
701  * @return {Object} A pair of x,y in page coordinates.
702  * @private
703  */
704 ImageEditor.MouseControl.getPosition_ = function(e) {
705   return {
706     x: e.pageX,
707     y: e.pageY
708   };
709 };
710
711 /**
712  * Returns touch position or null if there is more than one touch position.
713  *
714  * @param {TouchEvent} e Event.
715  * @return {Object?} A pair of x,y in page coordinates.
716  * @private
717  */
718 ImageEditor.MouseControl.prototype.getTouchPosition_ = function(e) {
719   if (e.targetTouches.length == 1)
720     return ImageEditor.MouseControl.getPosition_(e.targetTouches[0]);
721   else
722     return null;
723 };
724
725 /**
726  * Touch start handler.
727  * @param {TouchEvent} e Event.
728  */
729 ImageEditor.MouseControl.prototype.onTouchStart = function(e) {
730   var position = this.getTouchPosition_(e);
731   if (position) {
732     this.touchStartInfo_ = {
733       x: position.x,
734       y: position.y,
735       time: Date.now()
736     };
737     this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
738                                                     true /* touch */);
739     this.dragHappened_ = false;
740     e.preventDefault();
741   }
742 };
743
744 /**
745  * Touch end handler.
746  * @param {TouchEvent} e Event.
747  */
748 ImageEditor.MouseControl.prototype.onTouchEnd = function(e) {
749   if (!this.dragHappened_ &&
750       this.touchStartInfo_ &&
751       Date.now() - this.touchStartInfo_.time <=
752           ImageEditor.MouseControl.MAX_TAP_DURATION_) {
753     this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y);
754     if (this.previousTouchStartInfo_ &&
755         Date.now() - this.previousTouchStartInfo_.time <
756             ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_) {
757       var prevTouchCircle = new Circle(
758           this.previousTouchStartInfo_.x,
759           this.previousTouchStartInfo_.y,
760           ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_);
761       if (prevTouchCircle.inside(this.touchStartInfo_.x,
762                                  this.touchStartInfo_.y)) {
763         this.doubleTapCallback_(this.touchStartInfo_.x, this.touchStartInfo_.y);
764       }
765     }
766     this.previousTouchStartInfo_ = this.touchStartInfo_;
767   } else {
768     this.previousTouchStartInfo_ = null;
769   }
770   this.onTouchCancel(e);
771 };
772
773 /**
774  * Default double tap handler.
775  * @param {number} x X coordinate of the event.
776  * @param {number} y Y coordinate of the event.
777  * @private
778  */
779 ImageEditor.MouseControl.prototype.doubleTapCallback_ = function(x, y) {};
780
781 /**
782  * Sets callback to be called when double tap detected.
783  * @param {function(number, number)} callback New double tap callback.
784  */
785 ImageEditor.MouseControl.prototype.setDoubleTapCallback = function(callback) {
786   this.doubleTapCallback_ = callback;
787 };
788
789 /**
790  * Touch cancel handler.
791  */
792 ImageEditor.MouseControl.prototype.onTouchCancel = function() {
793   this.dragHandler_ = null;
794   this.dragHappened_ = false;
795   this.touchStartInfo_ = null;
796   this.lockMouse_(false);
797 };
798
799 /**
800  * Touch move handler.
801  * @param {TouchEvent} e Event.
802  */
803 ImageEditor.MouseControl.prototype.onTouchMove = function(e) {
804   var position = this.getTouchPosition_(e);
805   if (!position)
806     return;
807
808   if (this.touchStartInfo_ && !this.dragHappened_) {
809     var tapCircle = new Circle(
810         this.touchStartInfo_.x, this.touchStartInfo_.y,
811         ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_);
812     this.dragHappened_ = !tapCircle.inside(position.x, position.y);
813   }
814   if (this.dragHandler_ && this.dragHappened_) {
815     this.dragHandler_(position.x, position.y, e.shiftKey);
816     this.lockMouse_(true);
817   }
818 };
819
820 /**
821  * Mouse down handler.
822  * @param {MouseEvent} e Event.
823  */
824 ImageEditor.MouseControl.prototype.onMouseDown = function(e) {
825   var position = ImageEditor.MouseControl.getPosition_(e);
826
827   this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
828                                                   false /* mouse */);
829   this.dragHappened_ = false;
830   this.updateCursor_(position);
831 };
832
833 /**
834  * Mouse up handler.
835  * @param {MouseEvent} e Event.
836  */
837 ImageEditor.MouseControl.prototype.onMouseUp = function(e) {
838   var position = ImageEditor.MouseControl.getPosition_(e);
839
840   if (!this.dragHappened_) {
841     this.buffer_.onClick(position.x, position.y);
842   }
843   this.dragHandler_ = null;
844   this.dragHappened_ = false;
845   this.lockMouse_(false);
846 };
847
848 /**
849  * Mouse move handler.
850  * @param {MouseEvent} e Event.
851  */
852 ImageEditor.MouseControl.prototype.onMouseMove = function(e) {
853   var position = ImageEditor.MouseControl.getPosition_(e);
854
855   if (this.dragHandler_ && !e.which) {
856     // mouseup must have happened while the mouse was outside our window.
857     this.dragHandler_ = null;
858     this.lockMouse_(false);
859   }
860
861   this.updateCursor_(position);
862   if (this.dragHandler_) {
863     this.dragHandler_(position.x, position.y, e.shiftKey);
864     this.dragHappened_ = true;
865     this.lockMouse_(true);
866   }
867 };
868
869 /**
870  * Update the UI to reflect mouse drag state.
871  * @param {boolean} on True if dragging.
872  * @private
873  */
874 ImageEditor.MouseControl.prototype.lockMouse_ = function(on) {
875   ImageUtil.setAttribute(this.rootContainer_, 'mousedrag', on);
876 };
877
878 /**
879  * Update the cursor.
880  *
881  * @param {Object} position An object holding x and y properties.
882  * @private
883  */
884 ImageEditor.MouseControl.prototype.updateCursor_ = function(position) {
885   var oldCursor = this.container_.getAttribute('cursor');
886   var newCursor = this.buffer_.getCursorStyle(
887       position.x, position.y, !!this.dragHandler_);
888   if (newCursor != oldCursor)  // Avoid flicker.
889     this.container_.setAttribute('cursor', newCursor);
890 };
891
892 /**
893  * A toolbar for the ImageEditor.
894  * @param {HTMLElement} parent The parent element.
895  * @param {function(string)} displayStringFunction A string formatting function.
896  * @param {function(Object)} updateCallback The callback called when controls
897  *     change.
898  * @constructor
899  */
900 ImageEditor.Toolbar = function(parent, displayStringFunction, updateCallback) {
901   this.wrapper_ = parent;
902   this.displayStringFunction_ = displayStringFunction;
903   this.updateCallback_ = updateCallback;
904   Object.seal(this);
905 };
906
907 ImageEditor.Toolbar.prototype = {
908   get element() {
909     return this.wrapper_;
910   }
911 };
912
913 /**
914  * Clear the toolbar.
915  */
916 ImageEditor.Toolbar.prototype.clear = function() {
917   ImageUtil.removeChildren(this.wrapper_);
918 };
919
920 /**
921  * Create a control.
922  * @param {string} tagName The element tag name.
923  * @return {HTMLElement} The created control element.
924  * @private
925  */
926 ImageEditor.Toolbar.prototype.create_ = function(tagName) {
927   return this.wrapper_.ownerDocument.createElement(tagName);
928 };
929
930 /**
931  * Add a control.
932  * @param {HTMLElement} element The control to add.
933  * @return {HTMLElement} The added element.
934  */
935 ImageEditor.Toolbar.prototype.add = function(element) {
936   this.wrapper_.appendChild(element);
937   return element;
938 };
939
940 /**
941  * Add a text label.
942  * @param {string} name Label name.
943  * @return {HTMLElement} The added label.
944  */
945 ImageEditor.Toolbar.prototype.addLabel = function(name) {
946   var label = this.create_('span');
947   label.textContent = this.displayStringFunction_(name);
948   return this.add(label);
949 };
950
951 /**
952  * Add a button.
953  *
954  * @param {string} name Button name.
955  * @param {string} title Button title.
956  * @param {function(Event)} handler onClick handler.
957  * @param {string=} opt_class Extra class name.
958  * @return {HTMLElement} The added button.
959  */
960 ImageEditor.Toolbar.prototype.addButton = function(
961     name, title, handler, opt_class) {
962   var button = this.create_('button');
963   if (opt_class)
964     button.classList.add(opt_class);
965   var label = this.create_('span');
966   label.textContent = this.displayStringFunction_(title);
967   button.appendChild(label);
968   button.label = this.displayStringFunction_(title);
969   button.title = this.displayStringFunction_(title);
970   button.addEventListener('click', handler, false);
971   return this.add(button);
972 };
973
974 /**
975  * Add a range control (scalar value picker).
976  *
977  * @param {string} name An option name.
978  * @param {string} title An option title.
979  * @param {number} min Min value of the option.
980  * @param {number} value Default value of the option.
981  * @param {number} max Max value of the options.
982  * @param {number} scale A number to multiply by when setting
983  *                       min/value/max in DOM.
984  * @param {boolean=} opt_showNumeric True if numeric value should be displayed.
985  * @return {HTMLElement} Range element.
986  */
987 ImageEditor.Toolbar.prototype.addRange = function(
988     name, title, min, value, max, scale, opt_showNumeric) {
989   var self = this;
990
991   scale = scale || 1;
992
993   var range = this.create_('input');
994
995   range.className = 'range';
996   range.type = 'range';
997   range.name = name;
998   range.min = Math.ceil(min * scale);
999   range.max = Math.floor(max * scale);
1000
1001   var numeric = this.create_('div');
1002   numeric.className = 'numeric';
1003   function mirror() {
1004     numeric.textContent = Math.round(range.getValue() * scale) / scale;
1005   }
1006
1007   range.setValue = function(newValue) {
1008     range.value = Math.round(newValue * scale);
1009     mirror();
1010   };
1011
1012   range.getValue = function() {
1013     return Number(range.value) / scale;
1014   };
1015
1016   range.reset = function() {
1017     range.setValue(value);
1018   };
1019
1020   range.addEventListener('change',
1021       function() {
1022         mirror();
1023         self.updateCallback_(self.getOptions());
1024       },
1025       false);
1026
1027   range.setValue(value);
1028
1029   var label = this.create_('div');
1030   label.textContent = this.displayStringFunction_(title);
1031   label.className = 'label ' + name;
1032   this.add(label);
1033   this.add(range);
1034
1035   if (opt_showNumeric)
1036     this.add(numeric);
1037
1038   // Swallow the left and right keys, so they are not handled by other
1039   // listeners.
1040   range.addEventListener('keydown', function(e) {
1041     if (e.keyIdentifier === 'Left' || e.keyIdentifier === 'Right')
1042       e.stopPropagation();
1043   });
1044
1045   return range;
1046 };
1047
1048 /**
1049  * @return {Object} options A map of options.
1050  */
1051 ImageEditor.Toolbar.prototype.getOptions = function() {
1052   var values = {};
1053   for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
1054     if (child.name)
1055       values[child.name] = child.getValue();
1056   }
1057   return values;
1058 };
1059
1060 /**
1061  * Reset the toolbar.
1062  */
1063 ImageEditor.Toolbar.prototype.reset = function() {
1064   for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
1065     if (child.reset) child.reset();
1066   }
1067 };
1068
1069 /**
1070  * Show/hide the toolbar.
1071  * @param {boolean} on True if show.
1072  */
1073 ImageEditor.Toolbar.prototype.show = function(on) {
1074   if (!this.wrapper_.firstChild)
1075     return;  // Do not show empty toolbar;
1076
1077   this.wrapper_.hidden = !on;
1078 };
1079
1080 /** A prompt panel for the editor.
1081  *
1082  * @param {HTMLElement} container Container element.
1083  * @param {function(string, ...[string])} displayStringFunction A formatting
1084  *     function.
1085  * @constructor
1086  */
1087 ImageEditor.Prompt = function(container, displayStringFunction) {
1088   this.container_ = container;
1089   this.displayStringFunction_ = displayStringFunction;
1090 };
1091
1092 /**
1093  * Reset the prompt.
1094  */
1095 ImageEditor.Prompt.prototype.reset = function() {
1096   this.cancelTimer();
1097   if (this.wrapper_) {
1098     this.container_.removeChild(this.wrapper_);
1099     this.wrapper_ = null;
1100     this.prompt_ = null;
1101   }
1102 };
1103
1104 /**
1105  * Cancel the delayed action.
1106  */
1107 ImageEditor.Prompt.prototype.cancelTimer = function() {
1108   if (this.timer_) {
1109     clearTimeout(this.timer_);
1110     this.timer_ = null;
1111   }
1112 };
1113
1114 /**
1115  * Schedule the delayed action.
1116  * @param {function()} callback Callback.
1117  * @param {number} timeout Timeout.
1118  */
1119 ImageEditor.Prompt.prototype.setTimer = function(callback, timeout) {
1120   this.cancelTimer();
1121   var self = this;
1122   this.timer_ = setTimeout(function() {
1123     self.timer_ = null;
1124     callback();
1125   }, timeout);
1126 };
1127
1128 /**
1129  * Show the prompt.
1130  *
1131  * @param {string} text The prompt text.
1132  * @param {number} timeout Timeout in ms.
1133  * @param {...Object} var_args varArgs for the formatting function.
1134  */
1135 ImageEditor.Prompt.prototype.show = function(text, timeout, var_args) {
1136   var args = [text].concat(Array.prototype.slice.call(arguments, 2));
1137   var message = this.displayStringFunction_.apply(null, args);
1138   this.showStringAt('center', message, timeout);
1139 };
1140
1141 /**
1142  * Show the position at the specific position.
1143  *
1144  * @param {string} pos The 'pos' attribute value.
1145  * @param {string} text The prompt text.
1146  * @param {number} timeout Timeout in ms.
1147  * @param {...Object} var_args varArgs for the formatting function.
1148  */
1149 ImageEditor.Prompt.prototype.showAt = function(
1150     pos, text, timeout, var_args) {
1151   var args = [text].concat(Array.prototype.slice.call(arguments, 3));
1152   var message = this.displayStringFunction_.apply(null, args);
1153   this.showStringAt(pos, message, timeout);
1154 };
1155
1156 /**
1157  * Show the string in the prompt
1158  *
1159  * @param {string} pos The 'pos' attribute value.
1160  * @param {string} text The prompt text.
1161  * @param {number} timeout Timeout in ms.
1162  */
1163 ImageEditor.Prompt.prototype.showStringAt = function(pos, text, timeout) {
1164   this.reset();
1165   if (!text)
1166     return;
1167
1168   var document = this.container_.ownerDocument;
1169   this.wrapper_ = document.createElement('div');
1170   this.wrapper_.className = 'prompt-wrapper';
1171   this.wrapper_.setAttribute('pos', pos);
1172   this.container_.appendChild(this.wrapper_);
1173
1174   this.prompt_ = document.createElement('div');
1175   this.prompt_.className = 'prompt';
1176
1177   // Create an extra wrapper which opacity can be manipulated separately.
1178   var tool = document.createElement('div');
1179   tool.className = 'dimmable';
1180   this.wrapper_.appendChild(tool);
1181   tool.appendChild(this.prompt_);
1182
1183   this.prompt_.textContent = text;
1184
1185   var close = document.createElement('div');
1186   close.className = 'close';
1187   close.addEventListener('click', this.hide.bind(this));
1188   this.prompt_.appendChild(close);
1189
1190   setTimeout(
1191       this.prompt_.setAttribute.bind(this.prompt_, 'state', 'fadein'), 0);
1192
1193   if (timeout)
1194     this.setTimer(this.hide.bind(this), timeout);
1195 };
1196
1197 /**
1198  * Hide the prompt.
1199  */
1200 ImageEditor.Prompt.prototype.hide = function() {
1201   if (!this.prompt_) return;
1202   this.prompt_.setAttribute('state', 'fadeout');
1203   // Allow some time for the animation to play out.
1204   this.setTimer(this.reset.bind(this), 500);
1205 };