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