Home | History | Annotate | Download | only in image_editor
      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 };
   1205