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 */
21function 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 */
63ImageEditor.prototype.isLocked = function() {
64  return !this.commandQueue_ || this.commandQueue_.isBusy();
65};
66
67/**
68 * @return {boolean} True if the command queue is busy.
69 */
70ImageEditor.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 */
78ImageEditor.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 */
86ImageEditor.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 */
95ImageEditor.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 */
111ImageEditor.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 */
137ImageEditor.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 */
165ImageEditor.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 */
179ImageEditor.prototype.canUndo = function() {
180  return this.commandQueue_ && this.commandQueue_.canUndo();
181};
182
183/**
184 * Undo the recently executed command.
185 */
186ImageEditor.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 */
205ImageEditor.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 */
217ImageEditor.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 */
227ImageEditor.prototype.getCanvas = function() {
228  return this.getImageView().getCanvas();
229};
230
231/**
232 * @return {ImageBuffer} ImageBuffer instance.
233 */
234ImageEditor.prototype.getBuffer = function() { return this.buffer_; };
235
236/**
237 * @return {ImageView} ImageView instance.
238 */
239ImageEditor.prototype.getImageView = function() { return this.imageView_; };
240
241/**
242 * @return {Viewport} Viewport instance.
243 */
244ImageEditor.prototype.getViewport = function() { return this.viewport_; };
245
246/**
247 * @return {ImageEditor.Prompt} Prompt instance.
248 */
249ImageEditor.prototype.getPrompt = function() { return this.prompt_; };
250
251/**
252 * Handle the toolbar controls update.
253 * @param {Object} options A map of options.
254 */
255ImageEditor.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
273ImageEditor.Mode = function(name, title) {
274  this.name = name;
275  this.title = title;
276  this.message_ = 'GALLERY_ENTER_WHEN_DONE';
277};
278
279ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype };
280
281/**
282 * @return {Viewport} Viewport instance.
283 */
284ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_; };
285
286/**
287 * @return {ImageView} ImageView instance.
288 */
289ImageEditor.Mode.prototype.getImageView = function() {
290  return this.imageView_;
291};
292
293/**
294 * @return {string} The mode-specific message to be displayed when entering.
295 */
296ImageEditor.Mode.prototype.getMessage = function() { return this.message_; };
297
298/**
299 * @return {boolean} True if the mode is applicable in the current context.
300 */
301ImageEditor.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
310ImageEditor.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 */
321ImageEditor.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 */
330ImageEditor.Mode.prototype.createTools = function(toolbar) {};
331
332/**
333 * Called before exiting the mode.
334 */
335ImageEditor.Mode.prototype.cleanUpUI = function() {
336  this.editor_.getBuffer().removeOverlay(this);
337};
338
339/**
340 * Called after exiting the mode.
341 */
342ImageEditor.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 */
348ImageEditor.Mode.prototype.update = function(options) {
349  this.markUpdated();
350};
351
352/**
353 * Mark the editor mode as updated.
354 */
355ImageEditor.Mode.prototype.markUpdated = function() {
356  this.updated_ = true;
357};
358
359/**
360 * @return {boolean} True if the mode controls changed.
361 */
362ImageEditor.Mode.prototype.isUpdated = function() { return this.updated_; };
363
364/**
365 * Resets the mode to a clean state.
366 */
367ImageEditor.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 */
380ImageEditor.Mode.OneClick = function(name, title, command) {
381  ImageEditor.Mode.call(this, name, title);
382  this.instant = true;
383  this.command_ = command;
384};
385
386ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype};
387
388/**
389 * @return {Command} command.
390 */
391ImageEditor.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 */
400ImageEditor.prototype.registerAction_ = function(name) {
401  this.actionNames_.push(name);
402};
403
404/**
405 * Populate the toolbar.
406 */
407ImageEditor.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 */
440ImageEditor.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 */
447ImageEditor.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 */
471ImageEditor.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 */
495ImageEditor.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 */
523ImageEditor.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 */
535ImageEditor.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 */
552ImageEditor.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 */
605ImageEditor.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 */
622ImageEditor.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 */
649ImageEditor.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 */
677ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_ = 8;
678
679/**
680 * Maximum time for touch to be detected as a tap (in milliseconds).
681 * @private
682 */
683ImageEditor.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 */
690ImageEditor.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 */
696ImageEditor.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 */
705ImageEditor.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 */
719ImageEditor.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 */
730ImageEditor.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 */
749ImageEditor.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 */
780ImageEditor.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 */
786ImageEditor.MouseControl.prototype.setDoubleTapCallback = function(callback) {
787  this.doubleTapCallback_ = callback;
788};
789
790/**
791 * Touch cancel handler.
792 */
793ImageEditor.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 */
804ImageEditor.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 */
825ImageEditor.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 */
838ImageEditor.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 */
853ImageEditor.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 */
875ImageEditor.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 */
885ImageEditor.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 */
900ImageEditor.Toolbar = function(parent, displayStringFunction, updateCallback) {
901  this.wrapper_ = parent;
902  this.displayStringFunction_ = displayStringFunction;
903  this.updateCallback_ = updateCallback;
904  Object.seal(this);
905};
906
907ImageEditor.Toolbar.prototype = {
908  get element() {
909    return this.wrapper_;
910  }
911};
912
913/**
914 * Clear the toolbar.
915 */
916ImageEditor.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 */
926ImageEditor.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 */
935ImageEditor.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 */
945ImageEditor.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 */
960ImageEditor.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 */
987ImageEditor.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 */
1051ImageEditor.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 */
1063ImageEditor.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 */
1073ImageEditor.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 */
1086ImageEditor.Prompt = function(container, displayStringFunction) {
1087  this.container_ = container;
1088  this.displayStringFunction_ = displayStringFunction;
1089};
1090
1091/**
1092 * Reset the prompt.
1093 */
1094ImageEditor.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 */
1106ImageEditor.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 */
1118ImageEditor.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 */
1134ImageEditor.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 */
1148ImageEditor.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 */
1162ImageEditor.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 */
1199ImageEditor.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