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