Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / image_editor.js
blobec0c51d639a56d6c9ecd0a60ecf53ca35fc97fca
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.
5 /**
6 * ImageEditor is the top level object that holds together and connects
7 * everything needed for image editing.
9 * @param {!Viewport} viewport The viewport.
10 * @param {!ImageView} imageView The ImageView containing the images to edit.
11 * @param {!ImageEditor.Prompt} prompt Prompt instance.
12 * @param {!Object} DOMContainers Various DOM containers required for the
13 * editor.
14 * @param {!Array<!ImageEditor.Mode>} modes Available editor modes.
15 * @param {function(string, ...string)} displayStringFunction String
16 * formatting function.
17 * @constructor
18 * @extends {cr.EventTarget}
19 * @struct
21 * TODO(yawano): Remove displayStringFunction from arguments.
23 function ImageEditor(
24 viewport, imageView, prompt, DOMContainers, modes, displayStringFunction) {
25 cr.EventTarget.call(this);
27 this.rootContainer_ = DOMContainers.root;
28 this.container_ = DOMContainers.image;
29 this.modes_ = modes;
30 this.displayStringFunction_ = displayStringFunction;
32 /**
33 * @type {ImageEditor.Mode}
34 * @private
36 this.currentMode_ = null;
38 /**
39 * @type {HTMLElement}
40 * @private
42 this.currentTool_ = null;
44 ImageUtil.removeChildren(this.container_);
46 this.viewport_ = viewport;
48 this.imageView_ = imageView;
49 this.imageView_.addContentCallback(this.onContentUpdate_.bind(this));
51 this.buffer_ = new ImageBuffer();
52 this.buffer_.addOverlay(this.imageView_);
54 this.panControl_ = new ImageEditor.MouseControl(
55 this.rootContainer_, this.container_, this.getBuffer());
56 this.panControl_.setDoubleTapCallback(this.onDoubleTap_.bind(this));
58 this.mainToolbar_ = new ImageEditor.Toolbar(
59 DOMContainers.toolbar, displayStringFunction);
61 this.modeToolbar_ = new ImageEditor.Toolbar(
62 DOMContainers.mode, displayStringFunction,
63 this.onOptionsChange.bind(this), true /* done button */);
64 this.modeToolbar_.addEventListener(
65 'done-clicked', this.onDoneClicked_.bind(this));
66 this.modeToolbar_.addEventListener(
67 'cancel-clicked', this.onCancelClicked_.bind(this));
69 this.prompt_ = prompt;
71 this.commandQueue_ = null;
73 // -----------------------------------------------------------------
74 // Populate the toolbar.
76 /**
77 * @type {!Array<string>}
78 * @private
80 this.actionNames_ = [];
82 this.mainToolbar_.clear();
84 // Create action buttons.
85 for (var i = 0; i != this.modes_.length; i++) {
86 var mode = this.modes_[i];
87 mode.bind(this, this.createToolButton_(mode.name, mode.title,
88 this.enterMode.bind(this, mode)));
91 /**
92 * @type {!HTMLElement}
93 * @private
95 this.undoButton_ = this.createToolButton_('undo', 'GALLERY_UNDO',
96 this.undo.bind(this));
97 this.registerAction_('undo');
99 /**
100 * @type {!HTMLElement}
101 * @private
103 this.redoButton_ = this.createToolButton_('redo', 'GALLERY_REDO',
104 this.redo.bind(this));
105 this.registerAction_('redo');
108 * @private {!HTMLElement}
109 * @const
111 this.exitButton_ = /** @type {!HTMLElement} */
112 (queryRequiredElement('.edit-mode-toolbar paper-button.exit'));
113 this.exitButton_.addEventListener('click', this.onExitClicked_.bind(this));
116 * @private {!FilesToast}
118 this.filesToast_ = /** @type {!FilesToast}*/
119 (queryRequiredElement('files-toast'));
122 ImageEditor.prototype.__proto__ = cr.EventTarget.prototype;
125 * Handles click event of exit button.
126 * @private
128 ImageEditor.prototype.onExitClicked_ = function() {
129 var event = new Event('exit-clicked');
130 this.dispatchEvent(event);
134 * Creates a toolbar button.
135 * @param {string} name Button name.
136 * @param {string} title Button title.
137 * @param {function(Event)} handler onClick handler.
138 * @return {!HTMLElement} A created button.
140 ImageEditor.prototype.createToolButton_ = function(name, title, handler) {
141 var button = this.mainToolbar_.addButton(
142 title, ImageEditor.Toolbar.ButtonType.ICON, handler,
143 name /* opt_className */);
144 return button;
148 * @return {boolean} True if no user commands are to be accepted.
150 ImageEditor.prototype.isLocked = function() {
151 return !this.commandQueue_ || this.commandQueue_.isBusy();
155 * @return {boolean} True if the command queue is busy.
157 ImageEditor.prototype.isBusy = function() {
158 return this.commandQueue_ && this.commandQueue_.isBusy();
162 * Reflect the locked state of the editor in the UI.
163 * @param {boolean} on True if locked.
165 ImageEditor.prototype.lockUI = function(on) {
166 ImageUtil.setAttribute(this.rootContainer_, 'locked', on);
170 * Report the tool use to the metrics subsystem.
171 * @param {string} name Action name.
173 ImageEditor.prototype.recordToolUse = function(name) {
174 ImageUtil.metrics.recordEnum(
175 ImageUtil.getMetricName('Tool'), name, this.actionNames_);
179 * Content update handler.
180 * @private
182 ImageEditor.prototype.onContentUpdate_ = function() {
183 for (var i = 0; i != this.modes_.length; i++) {
184 var mode = this.modes_[i];
185 ImageUtil.setAttribute(assert(mode.button_), 'disabled',
186 !mode.isApplicable());
191 * Open the editing session for a new image.
193 * @param {!Gallery.Item} item Gallery item.
194 * @param {!ImageView.Effect} effect Transition effect object.
195 * @param {function(function())} saveFunction Image save function.
196 * @param {function()} displayCallback Display callback.
197 * @param {function(!ImageView.LoadType, number, *=)} loadCallback Load
198 * callback.
200 ImageEditor.prototype.openSession = function(
201 item, effect, saveFunction, displayCallback, loadCallback) {
202 if (this.commandQueue_)
203 throw new Error('Session not closed');
205 this.lockUI(true);
207 var self = this;
208 this.imageView_.load(
209 item, effect, displayCallback, function(loadType, delay, error) {
210 self.lockUI(false);
212 // Always handle an item as original for new session.
213 item.setAsOriginal();
215 self.commandQueue_ = new CommandQueue(
216 self.container_.ownerDocument, assert(self.imageView_.getCanvas()),
217 saveFunction);
218 self.commandQueue_.attachUI(
219 self.getImageView(), self.getPrompt(), self.filesToast_,
220 self.updateUndoRedo.bind(self), self.lockUI.bind(self));
221 self.updateUndoRedo();
222 loadCallback(loadType, delay, error);
227 * Close the current image editing session.
228 * @param {function()} callback Callback.
230 ImageEditor.prototype.closeSession = function(callback) {
231 this.getPrompt().hide();
232 if (this.imageView_.isLoading()) {
233 if (this.commandQueue_) {
234 console.warn('Inconsistent image editor state');
235 this.commandQueue_ = null;
237 this.imageView_.cancelLoad();
238 this.lockUI(false);
239 callback();
240 return;
242 if (!this.commandQueue_) {
243 // Session is already closed.
244 callback();
245 return;
248 this.executeWhenReady(callback);
249 this.commandQueue_.close();
250 this.commandQueue_ = null;
254 * Commit the current operation and execute the action.
256 * @param {function()} callback Callback.
258 ImageEditor.prototype.executeWhenReady = function(callback) {
259 if (this.commandQueue_) {
260 this.leaveModeGently();
261 this.commandQueue_.executeWhenReady(callback);
262 } else {
263 if (!this.imageView_.isLoading())
264 console.warn('Inconsistent image editor state');
265 callback();
270 * @return {boolean} True if undo queue is not empty.
272 ImageEditor.prototype.canUndo = function() {
273 return !!this.commandQueue_ && this.commandQueue_.canUndo();
277 * Undo the recently executed command.
279 ImageEditor.prototype.undo = function() {
280 if (this.isLocked()) return;
281 this.recordToolUse('undo');
283 // First undo click should dismiss the uncommitted modifications.
284 if (this.currentMode_ && this.currentMode_.isUpdated()) {
285 this.currentMode_.reset();
286 return;
289 this.getPrompt().hide();
290 this.leaveMode(false);
291 this.commandQueue_.undo();
292 this.updateUndoRedo();
296 * Redo the recently un-done command.
298 ImageEditor.prototype.redo = function() {
299 if (this.isLocked()) return;
300 this.recordToolUse('redo');
301 this.getPrompt().hide();
302 this.leaveMode(false);
303 this.commandQueue_.redo();
304 this.updateUndoRedo();
308 * Update Undo/Redo buttons state.
310 ImageEditor.prototype.updateUndoRedo = function() {
311 var canUndo = this.commandQueue_ && this.commandQueue_.canUndo();
312 var canRedo = this.commandQueue_ && this.commandQueue_.canRedo();
313 ImageUtil.setAttribute(this.undoButton_, 'disabled', !canUndo);
314 ImageUtil.setAttribute(this.redoButton_, 'disabled', !canRedo);
318 * @return {HTMLCanvasElement} The current image canvas.
320 ImageEditor.prototype.getCanvas = function() {
321 return this.getImageView().getCanvas();
325 * @return {!ImageBuffer} ImageBuffer instance.
327 ImageEditor.prototype.getBuffer = function() { return this.buffer_; };
330 * @return {!ImageView} ImageView instance.
332 ImageEditor.prototype.getImageView = function() { return this.imageView_; };
335 * @return {!Viewport} Viewport instance.
337 ImageEditor.prototype.getViewport = function() { return this.viewport_; };
340 * @return {!ImageEditor.Prompt} Prompt instance.
342 ImageEditor.prototype.getPrompt = function() { return this.prompt_; };
345 * Handle the toolbar controls update.
346 * @param {Object} options A map of options.
348 ImageEditor.prototype.onOptionsChange = function(options) {
349 ImageUtil.trace.resetTimer('update');
350 if (this.currentMode_) {
351 this.currentMode_.update(options);
353 ImageUtil.trace.reportTimer('update');
357 * ImageEditor.Mode represents a modal state dedicated to a specific operation.
358 * Inherits from ImageBuffer. Overlay to simplify the drawing of mode-specific
359 * tools.
361 * @param {string} name The mode name.
362 * @param {string} title The mode title.
363 * @constructor
364 * @struct
365 * @extends {ImageBuffer.Overlay}
367 ImageEditor.Mode = function(name, title) {
368 this.name = name;
369 this.title = title;
370 this.message_ = 'GALLERY_ENTER_WHEN_DONE';
373 * @type {boolean}
375 this.implicitCommit = false;
378 * @type {boolean}
380 this.instant = false;
383 * @type {ImageEditor}
384 * @private
386 this.editor_ = null;
389 * @type {Viewport}
390 * @private
392 this.viewport_ = null;
395 * @type {HTMLElement}
396 * @private
398 this.button_ = null;
401 * @type {boolean}
402 * @private
404 this.updated_ = false;
407 * @type {ImageView}
408 * @private
410 this.imageView_ = null;
413 ImageEditor.Mode.prototype = { __proto__: ImageBuffer.Overlay.prototype };
416 * @return {Viewport} Viewport instance.
418 ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_; };
421 * @return {ImageView} ImageView instance.
423 ImageEditor.Mode.prototype.getImageView = function() {
424 return this.imageView_;
428 * @return {string} The mode-specific message to be displayed when entering.
430 ImageEditor.Mode.prototype.getMessage = function() { return this.message_; };
433 * @return {boolean} True if the mode is applicable in the current context.
435 ImageEditor.Mode.prototype.isApplicable = function() { return true; };
438 * Called once after creating the mode button.
440 * @param {!ImageEditor} editor The editor instance.
441 * @param {!HTMLElement} button The mode button.
444 ImageEditor.Mode.prototype.bind = function(editor, button) {
445 this.editor_ = editor;
446 this.editor_.registerAction_(this.name);
447 this.button_ = button;
448 this.viewport_ = editor.getViewport();
449 this.imageView_ = editor.getImageView();
453 * Called before entering the mode.
455 ImageEditor.Mode.prototype.setUp = function() {
456 this.editor_.getBuffer().addOverlay(this);
457 this.updated_ = false;
461 * Create mode-specific controls here.
462 * @param {!ImageEditor.Toolbar} toolbar The toolbar to populate.
464 ImageEditor.Mode.prototype.createTools = function(toolbar) {};
467 * Called before exiting the mode.
469 ImageEditor.Mode.prototype.cleanUpUI = function() {
470 this.editor_.getBuffer().removeOverlay(this);
474 * Called after exiting the mode.
476 ImageEditor.Mode.prototype.cleanUpCaches = function() {};
479 * Called when any of the controls changed its value.
480 * @param {Object} options A map of options.
482 ImageEditor.Mode.prototype.update = function(options) {
483 this.markUpdated();
487 * Mark the editor mode as updated.
489 ImageEditor.Mode.prototype.markUpdated = function() {
490 this.updated_ = true;
494 * @return {boolean} True if the mode controls changed.
496 ImageEditor.Mode.prototype.isUpdated = function() { return this.updated_; };
499 * Resets the mode to a clean state.
501 ImageEditor.Mode.prototype.reset = function() {
502 this.editor_.modeToolbar_.reset();
503 this.updated_ = false;
507 * @return {Command} Command.
509 ImageEditor.Mode.prototype.getCommand = function() {
510 return null;
514 * One-click editor tool, requires no interaction, just executes the command.
516 * @param {string} name The mode name.
517 * @param {string} title The mode title.
518 * @param {!Command} command The command to execute on click.
519 * @constructor
520 * @extends {ImageEditor.Mode}
521 * @struct
523 ImageEditor.Mode.OneClick = function(name, title, command) {
524 ImageEditor.Mode.call(this, name, title);
525 this.instant = true;
526 this.command_ = command;
529 ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype};
532 * @override
534 ImageEditor.Mode.OneClick.prototype.getCommand = function() {
535 return this.command_;
539 * Register the action name. Required for metrics reporting.
540 * @param {string} name Button name.
541 * @private
543 ImageEditor.prototype.registerAction_ = function(name) {
544 this.actionNames_.push(name);
548 * @return {ImageEditor.Mode} The current mode.
550 ImageEditor.prototype.getMode = function() { return this.currentMode_; };
553 * The user clicked on the mode button.
555 * @param {!ImageEditor.Mode} mode The new mode.
557 ImageEditor.prototype.enterMode = function(mode) {
558 if (this.isLocked()) return;
560 if (this.currentMode_ == mode) {
561 // Currently active editor tool clicked, commit if modified.
562 this.leaveMode(this.currentMode_.updated_);
563 return;
566 this.recordToolUse(mode.name);
568 this.leaveModeGently();
569 // The above call could have caused a commit which might have initiated
570 // an asynchronous command execution. Wait for it to complete, then proceed
571 // with the mode set up.
572 this.commandQueue_.executeWhenReady(this.setUpMode_.bind(this, mode));
576 * Set up the new editing mode.
578 * @param {!ImageEditor.Mode} mode The mode.
579 * @private
581 ImageEditor.prototype.setUpMode_ = function(mode) {
582 this.currentTool_ = mode.button_;
584 var paperRipple = this.currentTool_.querySelector('paper-ripple');
585 if (mode.instant)
586 paperRipple.simulatedRipple();
587 else
588 paperRipple.downAction();
590 this.currentMode_ = mode;
591 this.currentMode_.setUp();
593 if (this.currentMode_.instant) { // Instant tool.
594 this.leaveMode(true);
595 return;
598 this.exitButton_.hidden = true;
600 this.modeToolbar_.clear();
601 this.currentMode_.createTools(this.modeToolbar_);
602 this.modeToolbar_.show(true);
606 * Handles click event of Done button.
607 * @param {!Event} event An event.
608 * @private
610 ImageEditor.prototype.onDoneClicked_ = function(event) {
611 this.leaveMode(true /* commit */);
615 * Handles click event of Cancel button.
616 * @param {!Event} event An event.
617 * @private
619 ImageEditor.prototype.onCancelClicked_ = function(event) {
620 this.leaveMode(false /* not commit */);
624 * The user clicked on 'OK' or 'Cancel' or on a different mode button.
625 * @param {boolean} commit True if commit is required.
627 ImageEditor.prototype.leaveMode = function(commit) {
628 if (!this.currentMode_) return;
630 this.modeToolbar_.show(false);
632 this.currentMode_.cleanUpUI();
633 if (commit) {
634 var self = this;
635 var command = this.currentMode_.getCommand();
636 if (command) { // Could be null if the user did not do anything.
637 this.commandQueue_.execute(command);
638 this.updateUndoRedo();
642 if (!this.currentMode_.instant) {
643 var paperRipple = this.currentTool_.querySelector('paper-ripple');
644 paperRipple.upAction();
647 this.exitButton_.hidden = false;
649 this.currentMode_.cleanUpCaches();
650 this.currentMode_ = null;
651 this.currentTool_ = null;
655 * Leave the mode, commit only if required by the current mode.
657 ImageEditor.prototype.leaveModeGently = function() {
658 this.leaveMode(!!this.currentMode_ &&
659 this.currentMode_.updated_ &&
660 this.currentMode_.implicitCommit);
664 * Enter the editor mode with the given name.
666 * @param {string} name Mode name.
667 * @private
669 ImageEditor.prototype.enterModeByName_ = function(name) {
670 for (var i = 0; i !== this.modes_.length; i++) {
671 var mode = this.modes_[i];
672 if (mode.name === name) {
673 if (!mode.button_.hasAttribute('disabled'))
674 this.enterMode(mode);
675 return;
678 console.error('Mode "' + name + '" not found.');
682 * Key down handler.
683 * @param {!Event} event The keydown event.
684 * @return {boolean} True if handled.
686 ImageEditor.prototype.onKeyDown = function(event) {
687 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
688 case 'U+001B': // Escape
689 case 'Enter':
690 if (this.getMode()) {
691 this.leaveMode(event.keyIdentifier === 'Enter');
692 return true;
694 break;
696 case 'Ctrl-U+005A': // Ctrl+Z
697 if (this.commandQueue_.canUndo()) {
698 this.undo();
699 return true;
701 break;
703 case 'Ctrl-U+0059': // Ctrl+Y
704 if (this.commandQueue_.canRedo()) {
705 this.redo();
706 return true;
708 break;
710 case 'U+0041': // 'a'
711 this.enterModeByName_('autofix');
712 return true;
714 case 'U+0042': // 'b'
715 this.enterModeByName_('exposure');
716 return true;
718 case 'U+0043': // 'c'
719 this.enterModeByName_('crop');
720 return true;
722 case 'U+004C': // 'l'
723 this.enterModeByName_('rotate_left');
724 return true;
726 case 'U+0052': // 'r'
727 this.enterModeByName_('rotate_right');
728 return true;
730 return false;
734 * Double tap handler.
735 * @param {number} x X coordinate of the event.
736 * @param {number} y Y coordinate of the event.
737 * @private
739 ImageEditor.prototype.onDoubleTap_ = function(x, y) {
740 if (this.getMode()) {
741 var action = this.buffer_.getDoubleTapAction(x, y);
742 if (action == ImageBuffer.DoubleTapAction.COMMIT)
743 this.leaveMode(true);
744 else if (action == ImageBuffer.DoubleTapAction.CANCEL)
745 this.leaveMode(false);
750 * A helper object for panning the ImageBuffer.
752 * @param {!HTMLElement} rootContainer The top-level container.
753 * @param {!HTMLElement} container The container for mouse events.
754 * @param {!ImageBuffer} buffer Image buffer.
755 * @constructor
756 * @struct
758 ImageEditor.MouseControl = function(rootContainer, container, buffer) {
759 this.rootContainer_ = rootContainer;
760 this.container_ = container;
761 this.buffer_ = buffer;
763 var handlers = {
764 'touchstart': this.onTouchStart,
765 'touchend': this.onTouchEnd,
766 'touchcancel': this.onTouchCancel,
767 'touchmove': this.onTouchMove,
768 'mousedown': this.onMouseDown,
769 'mouseup': this.onMouseUp
772 for (var eventName in handlers) {
773 container.addEventListener(
774 eventName, handlers[eventName].bind(this), false);
777 // Mouse move handler has to be attached to the window to receive events
778 // from outside of the window. See: http://crbug.com/155705
779 window.addEventListener('mousemove', this.onMouseMove.bind(this), false);
782 * @type {?ImageBuffer.DragHandler}
783 * @private
785 this.dragHandler_ = null;
788 * @type {boolean}
789 * @private
791 this.dragHappened_ = false;
794 * @type {?{x: number, y: number, time:number}}
795 * @private
797 this.touchStartInfo_ = null;
800 * @type {?{x: number, y: number, time:number}}
801 * @private
803 this.previousTouchStartInfo_ = null;
807 * Maximum movement for touch to be detected as a tap (in pixels).
808 * @private
809 * @const
811 ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_ = 8;
814 * Maximum time for touch to be detected as a tap (in milliseconds).
815 * @private
816 * @const
818 ImageEditor.MouseControl.MAX_TAP_DURATION_ = 500;
821 * Maximum distance from the first tap to the second tap to be considered
822 * as a double tap.
823 * @private
824 * @const
826 ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_ = 32;
829 * Maximum time for touch to be detected as a double tap (in milliseconds).
830 * @private
831 * @const
833 ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_ = 1000;
836 * Returns an event's position.
838 * @param {!(MouseEvent|Touch)} e Pointer position.
839 * @return {!Object} A pair of x,y in page coordinates.
840 * @private
842 ImageEditor.MouseControl.getPosition_ = function(e) {
843 return {
844 x: e.pageX,
845 y: e.pageY
850 * Returns touch position or null if there is more than one touch position.
852 * @param {!TouchEvent} e Event.
853 * @return {Object?} A pair of x,y in page coordinates.
854 * @private
856 ImageEditor.MouseControl.prototype.getTouchPosition_ = function(e) {
857 if (e.targetTouches.length == 1)
858 return ImageEditor.MouseControl.getPosition_(e.targetTouches[0]);
859 else
860 return null;
864 * Touch start handler.
865 * @param {!TouchEvent} e Event.
867 ImageEditor.MouseControl.prototype.onTouchStart = function(e) {
868 var position = this.getTouchPosition_(e);
869 if (position) {
870 this.touchStartInfo_ = {
871 x: position.x,
872 y: position.y,
873 time: Date.now()
875 this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
876 true /* touch */);
877 this.dragHappened_ = false;
878 e.preventDefault();
883 * Touch end handler.
884 * @param {!TouchEvent} e Event.
886 ImageEditor.MouseControl.prototype.onTouchEnd = function(e) {
887 if (!this.dragHappened_ &&
888 this.touchStartInfo_ &&
889 Date.now() - this.touchStartInfo_.time <=
890 ImageEditor.MouseControl.MAX_TAP_DURATION_) {
891 this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y);
892 if (this.previousTouchStartInfo_ &&
893 Date.now() - this.previousTouchStartInfo_.time <
894 ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_) {
895 var prevTouchCircle = new Circle(
896 this.previousTouchStartInfo_.x,
897 this.previousTouchStartInfo_.y,
898 ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_);
899 if (prevTouchCircle.inside(this.touchStartInfo_.x,
900 this.touchStartInfo_.y)) {
901 this.doubleTapCallback_(this.touchStartInfo_.x, this.touchStartInfo_.y);
904 this.previousTouchStartInfo_ = this.touchStartInfo_;
905 } else {
906 this.previousTouchStartInfo_ = null;
908 this.onTouchCancel();
912 * Default double tap handler.
913 * @param {number} x X coordinate of the event.
914 * @param {number} y Y coordinate of the event.
915 * @private
917 ImageEditor.MouseControl.prototype.doubleTapCallback_ = function(x, y) {};
920 * Sets callback to be called when double tap detected.
921 * @param {function(number, number)} callback New double tap callback.
923 ImageEditor.MouseControl.prototype.setDoubleTapCallback = function(callback) {
924 this.doubleTapCallback_ = callback;
928 * Touch cancel handler.
930 ImageEditor.MouseControl.prototype.onTouchCancel = function() {
931 this.dragHandler_ = null;
932 this.dragHappened_ = false;
933 this.touchStartInfo_ = null;
934 this.lockMouse_(false);
938 * Touch move handler.
939 * @param {!TouchEvent} e Event.
941 ImageEditor.MouseControl.prototype.onTouchMove = function(e) {
942 var position = this.getTouchPosition_(e);
943 if (!position)
944 return;
946 if (this.touchStartInfo_ && !this.dragHappened_) {
947 var tapCircle = new Circle(
948 this.touchStartInfo_.x, this.touchStartInfo_.y,
949 ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_);
950 this.dragHappened_ = !tapCircle.inside(position.x, position.y);
952 if (this.dragHandler_ && this.dragHappened_) {
953 this.dragHandler_(position.x, position.y, e.shiftKey);
954 this.lockMouse_(true);
959 * Mouse down handler.
960 * @param {!MouseEvent} e Event.
962 ImageEditor.MouseControl.prototype.onMouseDown = function(e) {
963 var position = ImageEditor.MouseControl.getPosition_(e);
965 this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
966 false /* mouse */);
967 this.dragHappened_ = false;
968 this.updateCursor_(position);
972 * Mouse up handler.
973 * @param {!MouseEvent} e Event.
975 ImageEditor.MouseControl.prototype.onMouseUp = function(e) {
976 var position = ImageEditor.MouseControl.getPosition_(e);
978 if (!this.dragHappened_) {
979 this.buffer_.onClick(position.x, position.y);
981 this.dragHandler_ = null;
982 this.dragHappened_ = false;
983 this.lockMouse_(false);
987 * Mouse move handler.
988 * @param {!Event} e Event.
990 ImageEditor.MouseControl.prototype.onMouseMove = function(e) {
991 e = assertInstanceof(e, MouseEvent);
992 var position = ImageEditor.MouseControl.getPosition_(e);
994 if (this.dragHandler_ && !e.which) {
995 // mouseup must have happened while the mouse was outside our window.
996 this.dragHandler_ = null;
997 this.lockMouse_(false);
1000 this.updateCursor_(position);
1001 if (this.dragHandler_) {
1002 this.dragHandler_(position.x, position.y, e.shiftKey);
1003 this.dragHappened_ = true;
1004 this.lockMouse_(true);
1009 * Update the UI to reflect mouse drag state.
1010 * @param {boolean} on True if dragging.
1011 * @private
1013 ImageEditor.MouseControl.prototype.lockMouse_ = function(on) {
1014 ImageUtil.setAttribute(this.rootContainer_, 'mousedrag', on);
1018 * Update the cursor.
1020 * @param {!Object} position An object holding x and y properties.
1021 * @private
1023 ImageEditor.MouseControl.prototype.updateCursor_ = function(position) {
1024 var oldCursor = this.container_.getAttribute('cursor');
1025 var newCursor = this.buffer_.getCursorStyle(
1026 position.x, position.y, !!this.dragHandler_);
1027 if (newCursor != oldCursor) // Avoid flicker.
1028 this.container_.setAttribute('cursor', newCursor);
1032 * A toolbar for the ImageEditor.
1033 * @param {!HTMLElement} parent The parent element.
1034 * @param {function(string)} displayStringFunction A string formatting function.
1035 * @param {function(Object)=} opt_updateCallback The callback called when
1036 * controls change.
1037 * @param {boolean=} opt_showActionButtons True to show action buttons.
1038 * @constructor
1039 * @extends {cr.EventTarget}
1040 * @struct
1042 ImageEditor.Toolbar = function(
1043 parent, displayStringFunction, opt_updateCallback, opt_showActionButtons) {
1044 this.wrapper_ = parent;
1045 this.displayStringFunction_ = displayStringFunction;
1048 * @type {?function(Object)}
1049 * @private
1051 this.updateCallback_ = opt_updateCallback || null;
1053 // Create action buttons.
1054 if (opt_showActionButtons) {
1055 var actionButtonsLayer = document.createElement('div');
1056 actionButtonsLayer.classList.add('action-buttons');
1058 this.cancelButton_ = ImageEditor.Toolbar.createButton_(
1059 'GALLERY_CANCEL_LABEL', ImageEditor.Toolbar.ButtonType.LABEL_UPPER_CASE,
1060 this.onCancelClicked_.bind(this), 'cancel');
1061 actionButtonsLayer.appendChild(this.cancelButton_);
1063 this.doneButton_ = ImageEditor.Toolbar.createButton_(
1064 'GALLERY_DONE', ImageEditor.Toolbar.ButtonType.LABEL_UPPER_CASE,
1065 this.onDoneClicked_.bind(this), 'done');
1066 actionButtonsLayer.appendChild(this.doneButton_);
1068 this.wrapper_.appendChild(actionButtonsLayer);
1072 * @private {!HTMLElement}
1074 this.container_ = /** @type {!HTMLElement} */ (document.createElement('div'));
1075 this.container_.classList.add('container');
1076 this.wrapper_.appendChild(this.container_);
1079 ImageEditor.Toolbar.prototype.__proto__ = cr.EventTarget.prototype;
1082 * Height of the toolbar.
1083 * @const {number}
1085 ImageEditor.Toolbar.HEIGHT = 48; // px
1088 * Handles click event of done button.
1089 * @private
1091 ImageEditor.Toolbar.prototype.onDoneClicked_ = function() {
1092 this.doneButton_.querySelector('paper-ripple').simulatedRipple();
1094 var event = new Event('done-clicked');
1095 this.dispatchEvent(event);
1099 * Handles click event of cancel button.
1100 * @private
1102 ImageEditor.Toolbar.prototype.onCancelClicked_ = function() {
1103 this.cancelButton_.querySelector('paper-ripple').simulatedRipple();
1105 var event = new Event('cancel-clicked');
1106 this.dispatchEvent(event);
1110 * Returns the parent element.
1111 * @return {!HTMLElement}
1113 ImageEditor.Toolbar.prototype.getElement = function() {
1114 return this.container_;
1118 * Clear the toolbar.
1120 ImageEditor.Toolbar.prototype.clear = function() {
1121 ImageUtil.removeChildren(this.container_);
1125 * Add a control.
1126 * @param {!HTMLElement} element The control to add.
1127 * @return {!HTMLElement} The added element.
1129 ImageEditor.Toolbar.prototype.add = function(element) {
1130 this.container_.appendChild(element);
1131 return element;
1135 * Button type.
1136 * @enum {string}
1138 ImageEditor.Toolbar.ButtonType = {
1139 ICON: 'icon',
1140 LABEL: 'label',
1141 LABEL_UPPER_CASE: 'label_upper_case'
1145 * Create a button.
1147 * @param {string} title String ID of button title.
1148 * @param {ImageEditor.Toolbar.ButtonType} type Button type.
1149 * @param {function(Event)} handler onClick handler.
1150 * @param {string=} opt_class Extra class name.
1151 * @return {!HTMLElement} The created button.
1152 * @private
1154 ImageEditor.Toolbar.createButton_ = function(
1155 title, type, handler, opt_class) {
1156 var button = /** @type {!HTMLElement} */ (document.createElement('button'));
1157 if (opt_class)
1158 button.classList.add(opt_class);
1159 button.classList.add('edit-toolbar');
1161 if (type === ImageEditor.Toolbar.ButtonType.ICON) {
1162 var icon = document.createElement('div');
1163 icon.classList.add('icon');
1164 button.appendChild(icon);
1165 } else if (type === ImageEditor.Toolbar.ButtonType.LABEL ||
1166 type === ImageEditor.Toolbar.ButtonType.LABEL_UPPER_CASE) {
1167 var label = document.createElement('span');
1168 label.classList.add('label');
1169 label.textContent =
1170 type === ImageEditor.Toolbar.ButtonType.LABEL_UPPER_CASE ?
1171 strf(title).toLocaleUpperCase() : strf(title);
1172 button.appendChild(label);
1173 } else {
1174 assertNotReached();
1177 var paperRipple = document.createElement('paper-ripple');
1178 button.appendChild(paperRipple);
1180 button.label = strf(title);
1181 button.title = strf(title);
1183 GalleryUtil.decorateMouseFocusHandling(button);
1185 button.addEventListener('click', handler, false);
1186 button.addEventListener('keydown', function(event) {
1187 // Stop propagation of Enter key event to prevent it from being captured by
1188 // image editor.
1189 if (event.keyIdentifier === 'Enter')
1190 event.stopPropagation();
1193 return button;
1197 * Add a button.
1199 * @param {string} title Button title.
1200 * @param {ImageEditor.Toolbar.ButtonType} type Button type.
1201 * @param {function(Event)} handler onClick handler.
1202 * @param {string=} opt_class Extra class name.
1203 * @return {!HTMLElement} The added button.
1205 ImageEditor.Toolbar.prototype.addButton = function(
1206 title, type, handler, opt_class) {
1207 var button = ImageEditor.Toolbar.createButton_(
1208 title, type, handler, opt_class);
1209 this.add(button);
1210 return button;
1214 * Add a range control (scalar value picker).
1216 * @param {string} name An option name.
1217 * @param {string} title An option title.
1218 * @param {number} min Min value of the option.
1219 * @param {number} value Default value of the option.
1220 * @param {number} max Max value of the options.
1221 * @param {number=} opt_scale A number to multiply by when setting
1222 * min/value/max in DOM.
1223 * @param {boolean=} opt_showNumeric True if numeric value should be displayed.
1224 * @return {!HTMLElement} Range element.
1226 ImageEditor.Toolbar.prototype.addRange = function(
1227 name, title, min, value, max, opt_scale, opt_showNumeric) {
1228 var range = /** @type {!HTMLElement} */ (document.createElement('div'));
1229 range.classList.add('range', name);
1231 var icon = document.createElement('icon');
1232 icon.classList.add('icon');
1233 range.appendChild(icon);
1235 var label = document.createElement('span');
1236 label.textContent = strf(title);
1237 label.classList.add('label');
1238 range.appendChild(label);
1240 var scale = opt_scale || 1;
1241 var slider = document.createElement('paper-slider');
1242 slider.min = Math.ceil(min * scale);
1243 slider.max = Math.floor(max * scale);
1244 slider.value = value * scale;
1245 slider.addEventListener('change', function(event) {
1246 if (this.updateCallback_)
1247 this.updateCallback_(this.getOptions());
1248 }.bind(this));
1249 range.appendChild(slider);
1251 range.name = name;
1252 range.getValue = function(slider, scale) {
1253 return slider.value / scale;
1254 }.bind(this, slider, scale);
1256 // Swallow the left and right keys, so they are not handled by other
1257 // listeners.
1258 range.addEventListener('keydown', function(e) {
1259 if (e.keyIdentifier === 'Left' || e.keyIdentifier === 'Right')
1260 e.stopPropagation();
1263 this.add(range);
1265 return range;
1269 * @return {!Object} options A map of options.
1271 ImageEditor.Toolbar.prototype.getOptions = function() {
1272 var values = {};
1274 for (var child = this.container_.firstChild;
1275 child;
1276 child = child.nextSibling) {
1277 if (child.name)
1278 values[child.name] = child.getValue();
1281 return values;
1285 * Reset the toolbar.
1287 ImageEditor.Toolbar.prototype.reset = function() {
1288 for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
1289 if (child.reset) child.reset();
1294 * Show/hide the toolbar.
1295 * @param {boolean} on True if show.
1297 ImageEditor.Toolbar.prototype.show = function(on) {
1298 if (!this.wrapper_.firstChild)
1299 return; // Do not show empty toolbar;
1301 this.wrapper_.hidden = !on;
1304 /** A prompt panel for the editor.
1306 * @param {!HTMLElement} container Container element.
1307 * @param {function(string, ...string)} displayStringFunction A formatting
1308 * function.
1309 * @constructor
1310 * @struct
1312 ImageEditor.Prompt = function(container, displayStringFunction) {
1313 this.container_ = container;
1314 this.displayStringFunction_ = displayStringFunction;
1317 * @type {HTMLDivElement}
1318 * @private
1320 this.wrapper_ = null;
1323 * @type {HTMLDivElement}
1324 * @private
1326 this.prompt_ = null;
1329 * @type {number}
1330 * @private
1332 this.timer_ = 0;
1336 * Reset the prompt.
1338 ImageEditor.Prompt.prototype.reset = function() {
1339 this.cancelTimer();
1340 if (this.wrapper_) {
1341 this.container_.removeChild(this.wrapper_);
1342 this.wrapper_ = null;
1343 this.prompt_ = null;
1348 * Cancel the delayed action.
1350 ImageEditor.Prompt.prototype.cancelTimer = function() {
1351 if (this.timer_) {
1352 clearTimeout(this.timer_);
1353 this.timer_ = 0;
1358 * Schedule the delayed action.
1359 * @param {function()} callback Callback.
1360 * @param {number} timeout Timeout.
1362 ImageEditor.Prompt.prototype.setTimer = function(callback, timeout) {
1363 this.cancelTimer();
1364 var self = this;
1365 this.timer_ = setTimeout(function() {
1366 self.timer_ = 0;
1367 callback();
1368 }, timeout);
1372 * Show the prompt.
1374 * @param {string} text The prompt text.
1375 * @param {number=} opt_timeout Timeout in ms.
1376 * @param {...Object} var_args varArgs for the formatting function.
1378 ImageEditor.Prompt.prototype.show = function(text, opt_timeout, var_args) {
1379 var args = [text].concat(Array.prototype.slice.call(arguments, 2));
1380 var message = this.displayStringFunction_.apply(null, args);
1381 this.showStringAt('center', message, opt_timeout);
1385 * Show the position at the specific position.
1387 * @param {string} pos The 'pos' attribute value.
1388 * @param {string} text The prompt text.
1389 * @param {number} timeout Timeout in ms.
1390 * @param {...Object} var_args varArgs for the formatting function.
1392 ImageEditor.Prompt.prototype.showAt = function(
1393 pos, text, timeout, var_args) {
1394 var args = [text].concat(Array.prototype.slice.call(arguments, 3));
1395 var message = this.displayStringFunction_.apply(null, args);
1396 this.showStringAt(pos, message, timeout);
1400 * Show the string in the prompt
1402 * @param {string} pos The 'pos' attribute value.
1403 * @param {string} text The prompt text.
1404 * @param {number=} opt_timeout Timeout in ms.
1406 ImageEditor.Prompt.prototype.showStringAt = function(pos, text, opt_timeout) {
1407 this.reset();
1408 if (!text)
1409 return;
1411 var document = this.container_.ownerDocument;
1412 this.wrapper_ = assertInstanceof(document.createElement('div'),
1413 HTMLDivElement);
1414 this.wrapper_.className = 'prompt-wrapper';
1415 this.wrapper_.setAttribute('pos', pos);
1416 this.container_.appendChild(this.wrapper_);
1418 this.prompt_ = assertInstanceof(document.createElement('div'),
1419 HTMLDivElement);
1420 this.prompt_.className = 'prompt';
1422 // Create an extra wrapper which opacity can be manipulated separately.
1423 var tool = document.createElement('div');
1424 tool.className = 'dimmable';
1425 this.wrapper_.appendChild(tool);
1426 tool.appendChild(this.prompt_);
1428 this.prompt_.textContent = text;
1430 var close = document.createElement('div');
1431 close.className = 'close';
1432 close.addEventListener('click', this.hide.bind(this));
1433 this.prompt_.appendChild(close);
1435 setTimeout(
1436 this.prompt_.setAttribute.bind(this.prompt_, 'state', 'fadein'), 0);
1438 if (opt_timeout)
1439 this.setTimer(this.hide.bind(this), opt_timeout);
1443 * Hide the prompt.
1445 ImageEditor.Prompt.prototype.hide = function() {
1446 if (!this.prompt_) return;
1447 this.prompt_.setAttribute('state', 'fadeout');
1448 // Allow some time for the animation to play out.
1449 this.setTimer(this.reset.bind(this), 500);