Add ability for NetLogLogger to gather data from more than just NetLog
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / image_editor.js
blob4f3233750ca9ef6181b7c9177a70a89a45c57ecf
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 * @param {function()} onToolsVisibilityChanged Callback to be called, when
18 * some of the UI elements have been dimmed or revealed.
19 * @constructor
20 * @struct
22 function ImageEditor(
23 viewport, imageView, prompt, DOMContainers, modes, displayStringFunction,
24 onToolsVisibilityChanged) {
25 this.rootContainer_ = DOMContainers.root;
26 this.container_ = DOMContainers.image;
27 this.modes_ = modes;
28 this.displayStringFunction_ = displayStringFunction;
29 this.onToolsVisibilityChanged_ = onToolsVisibilityChanged;
31 /**
32 * @type {ImageEditor.Mode}
33 * @private
35 this.currentMode_ = null;
37 /**
38 * @type {HTMLElement}
39 * @private
41 this.currentTool_ = null;
43 ImageUtil.removeChildren(this.container_);
45 this.viewport_ = viewport;
46 this.viewport_.setScreenSize(
47 this.container_.clientWidth, this.container_.clientHeight);
49 this.imageView_ = imageView;
50 this.imageView_.addContentCallback(this.onContentUpdate_.bind(this));
52 this.buffer_ = new ImageBuffer();
53 this.buffer_.addOverlay(this.imageView_);
55 this.panControl_ = new ImageEditor.MouseControl(
56 this.rootContainer_, this.container_, this.getBuffer());
57 this.panControl_.setDoubleTapCallback(this.onDoubleTap_.bind(this));
59 this.mainToolbar_ = new ImageEditor.Toolbar(
60 DOMContainers.toolbar, displayStringFunction);
62 this.modeToolbar_ = new ImageEditor.Toolbar(
63 DOMContainers.mode, displayStringFunction,
64 this.onOptionsChange.bind(this));
66 this.prompt_ = prompt;
68 this.commandQueue_ = null;
70 // -----------------------------------------------------------------
71 // Populate the toolbar.
73 /**
74 * @type {!Array.<string>}
75 * @private
77 this.actionNames_ = [];
79 this.mainToolbar_.clear();
81 // Create action buttons.
82 for (var i = 0; i != this.modes_.length; i++) {
83 var mode = this.modes_[i];
84 mode.bind(this, this.createToolButton_(mode.name, mode.title,
85 this.enterMode.bind(this, mode)));
88 /**
89 * @type {!HTMLElement}
90 * @private
92 this.undoButton_ = this.createToolButton_('undo', 'GALLERY_UNDO',
93 this.undo.bind(this));
94 this.registerAction_('undo');
96 /**
97 * @type {!HTMLElement}
98 * @private
100 this.redoButton_ = this.createToolButton_('redo', 'GALLERY_REDO',
101 this.redo.bind(this));
102 this.registerAction_('redo');
106 * Creates a toolbar button.
107 * @param {string} name Button name.
108 * @param {string} title Button title.
109 * @param {function(Event)} handler onClick handler.
110 * @return {!HTMLElement} A created button.
112 ImageEditor.prototype.createToolButton_ = function(name, title, handler) {
113 return this.mainToolbar_.addButton(name, title, handler,
114 name /* opt_className */);
118 * @return {boolean} True if no user commands are to be accepted.
120 ImageEditor.prototype.isLocked = function() {
121 return !this.commandQueue_ || this.commandQueue_.isBusy();
125 * @return {boolean} True if the command queue is busy.
127 ImageEditor.prototype.isBusy = function() {
128 return this.commandQueue_ && this.commandQueue_.isBusy();
132 * Reflect the locked state of the editor in the UI.
133 * @param {boolean} on True if locked.
135 ImageEditor.prototype.lockUI = function(on) {
136 ImageUtil.setAttribute(this.rootContainer_, 'locked', on);
140 * Report the tool use to the metrics subsystem.
141 * @param {string} name Action name.
143 ImageEditor.prototype.recordToolUse = function(name) {
144 ImageUtil.metrics.recordEnum(
145 ImageUtil.getMetricName('Tool'), name, this.actionNames_);
149 * Content update handler.
150 * @private
152 ImageEditor.prototype.onContentUpdate_ = function() {
153 for (var i = 0; i != this.modes_.length; i++) {
154 var mode = this.modes_[i];
155 ImageUtil.setAttribute(assert(mode.button_), 'disabled',
156 !mode.isApplicable());
161 * Open the editing session for a new image.
163 * @param {!Gallery.Item} item Gallery item.
164 * @param {!ImageView.Effect} effect Transition effect object.
165 * @param {function(function())} saveFunction Image save function.
166 * @param {function()} displayCallback Display callback.
167 * @param {function(!ImageView.LoadType, number, *=)} loadCallback Load
168 * callback.
170 ImageEditor.prototype.openSession = function(
171 item, effect, saveFunction, displayCallback, loadCallback) {
172 if (this.commandQueue_)
173 throw new Error('Session not closed');
175 this.lockUI(true);
177 var self = this;
178 this.imageView_.load(
179 item, effect, displayCallback, function(loadType, delay, error) {
180 self.lockUI(false);
181 self.commandQueue_ = new CommandQueue(
182 self.container_.ownerDocument, assert(self.imageView_.getCanvas()),
183 saveFunction);
184 self.commandQueue_.attachUI(
185 self.getImageView(), self.getPrompt(), self.lockUI.bind(self));
186 self.updateUndoRedo();
187 loadCallback(loadType, delay, error);
192 * Close the current image editing session.
193 * @param {function()} callback Callback.
195 ImageEditor.prototype.closeSession = function(callback) {
196 this.getPrompt().hide();
197 if (this.imageView_.isLoading()) {
198 if (this.commandQueue_) {
199 console.warn('Inconsistent image editor state');
200 this.commandQueue_ = null;
202 this.imageView_.cancelLoad();
203 this.lockUI(false);
204 callback();
205 return;
207 if (!this.commandQueue_) {
208 // Session is already closed.
209 callback();
210 return;
213 this.executeWhenReady(callback);
214 this.commandQueue_.close();
215 this.commandQueue_ = null;
219 * Commit the current operation and execute the action.
221 * @param {function()} callback Callback.
223 ImageEditor.prototype.executeWhenReady = function(callback) {
224 if (this.commandQueue_) {
225 this.leaveModeGently();
226 this.commandQueue_.executeWhenReady(callback);
227 } else {
228 if (!this.imageView_.isLoading())
229 console.warn('Inconsistent image editor state');
230 callback();
235 * @return {boolean} True if undo queue is not empty.
237 ImageEditor.prototype.canUndo = function() {
238 return !!this.commandQueue_ && this.commandQueue_.canUndo();
242 * Undo the recently executed command.
244 ImageEditor.prototype.undo = function() {
245 if (this.isLocked()) return;
246 this.recordToolUse('undo');
248 // First undo click should dismiss the uncommitted modifications.
249 if (this.currentMode_ && this.currentMode_.isUpdated()) {
250 this.currentMode_.reset();
251 return;
254 this.getPrompt().hide();
255 this.leaveMode(false);
256 this.commandQueue_.undo();
257 this.updateUndoRedo();
261 * Redo the recently un-done command.
263 ImageEditor.prototype.redo = function() {
264 if (this.isLocked()) return;
265 this.recordToolUse('redo');
266 this.getPrompt().hide();
267 this.leaveMode(false);
268 this.commandQueue_.redo();
269 this.updateUndoRedo();
273 * Update Undo/Redo buttons state.
275 ImageEditor.prototype.updateUndoRedo = function() {
276 var canUndo = this.commandQueue_ && this.commandQueue_.canUndo();
277 var canRedo = this.commandQueue_ && this.commandQueue_.canRedo();
278 ImageUtil.setAttribute(this.undoButton_, 'disabled', !canUndo);
279 this.redoButton_.hidden = !canRedo;
283 * @return {HTMLCanvasElement} The current image canvas.
285 ImageEditor.prototype.getCanvas = function() {
286 return this.getImageView().getCanvas();
290 * @return {!ImageBuffer} ImageBuffer instance.
292 ImageEditor.prototype.getBuffer = function() { return this.buffer_; };
295 * @return {!ImageView} ImageView instance.
297 ImageEditor.prototype.getImageView = function() { return this.imageView_; };
300 * @return {!Viewport} Viewport instance.
302 ImageEditor.prototype.getViewport = function() { return this.viewport_; };
305 * @return {!ImageEditor.Prompt} Prompt instance.
307 ImageEditor.prototype.getPrompt = function() { return this.prompt_; };
310 * Handle the toolbar controls update.
311 * @param {Object} options A map of options.
313 ImageEditor.prototype.onOptionsChange = function(options) {
314 ImageUtil.trace.resetTimer('update');
315 if (this.currentMode_) {
316 this.currentMode_.update(options);
318 ImageUtil.trace.reportTimer('update');
322 * ImageEditor.Mode represents a modal state dedicated to a specific operation.
323 * Inherits from ImageBuffer. Overlay to simplify the drawing of mode-specific
324 * tools.
326 * @param {string} name The mode name.
327 * @param {string} title The mode title.
328 * @constructor
329 * @struct
330 * @extends {ImageBuffer.Overlay}
332 ImageEditor.Mode = function(name, title) {
333 this.name = name;
334 this.title = title;
335 this.message_ = 'GALLERY_ENTER_WHEN_DONE';
338 * @type {boolean}
340 this.implicitCommit = false;
343 * @type {boolean}
345 this.instant = false;
348 * @type {ImageEditor}
349 * @private
351 this.editor_ = null;
354 * @type {Viewport}
355 * @private
357 this.viewport_ = null;
360 * @type {HTMLElement}
361 * @private
363 this.button_ = null;
366 * @type {boolean}
367 * @private
369 this.updated_ = false;
372 * @type {ImageView}
373 * @private
375 this.imageView_ = null;
378 ImageEditor.Mode.prototype = { __proto__: ImageBuffer.Overlay.prototype };
381 * @return {Viewport} Viewport instance.
383 ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_; };
386 * @return {ImageView} ImageView instance.
388 ImageEditor.Mode.prototype.getImageView = function() {
389 return this.imageView_;
393 * @return {string} The mode-specific message to be displayed when entering.
395 ImageEditor.Mode.prototype.getMessage = function() { return this.message_; };
398 * @return {boolean} True if the mode is applicable in the current context.
400 ImageEditor.Mode.prototype.isApplicable = function() { return true; };
403 * Called once after creating the mode button.
405 * @param {!ImageEditor} editor The editor instance.
406 * @param {!HTMLElement} button The mode button.
409 ImageEditor.Mode.prototype.bind = function(editor, button) {
410 this.editor_ = editor;
411 this.editor_.registerAction_(this.name);
412 this.button_ = button;
413 this.viewport_ = editor.getViewport();
414 this.imageView_ = editor.getImageView();
418 * Called before entering the mode.
420 ImageEditor.Mode.prototype.setUp = function() {
421 this.editor_.getBuffer().addOverlay(this);
422 this.updated_ = false;
426 * Create mode-specific controls here.
427 * @param {!ImageEditor.Toolbar} toolbar The toolbar to populate.
429 ImageEditor.Mode.prototype.createTools = function(toolbar) {};
432 * Called before exiting the mode.
434 ImageEditor.Mode.prototype.cleanUpUI = function() {
435 this.editor_.getBuffer().removeOverlay(this);
439 * Called after exiting the mode.
441 ImageEditor.Mode.prototype.cleanUpCaches = function() {};
444 * Called when any of the controls changed its value.
445 * @param {Object} options A map of options.
447 ImageEditor.Mode.prototype.update = function(options) {
448 this.markUpdated();
452 * Mark the editor mode as updated.
454 ImageEditor.Mode.prototype.markUpdated = function() {
455 this.updated_ = true;
459 * @return {boolean} True if the mode controls changed.
461 ImageEditor.Mode.prototype.isUpdated = function() { return this.updated_; };
464 * Resets the mode to a clean state.
466 ImageEditor.Mode.prototype.reset = function() {
467 this.editor_.modeToolbar_.reset();
468 this.updated_ = false;
472 * @return {Command} Command.
474 ImageEditor.Mode.prototype.getCommand = function() {
475 return null;
479 * One-click editor tool, requires no interaction, just executes the command.
481 * @param {string} name The mode name.
482 * @param {string} title The mode title.
483 * @param {!Command} command The command to execute on click.
484 * @constructor
485 * @extends {ImageEditor.Mode}
486 * @struct
488 ImageEditor.Mode.OneClick = function(name, title, command) {
489 ImageEditor.Mode.call(this, name, title);
490 this.instant = true;
491 this.command_ = command;
494 ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype};
497 * @override
499 ImageEditor.Mode.OneClick.prototype.getCommand = function() {
500 return this.command_;
504 * Register the action name. Required for metrics reporting.
505 * @param {string} name Button name.
506 * @private
508 ImageEditor.prototype.registerAction_ = function(name) {
509 this.actionNames_.push(name);
513 * @return {ImageEditor.Mode} The current mode.
515 ImageEditor.prototype.getMode = function() { return this.currentMode_; };
518 * The user clicked on the mode button.
520 * @param {!ImageEditor.Mode} mode The new mode.
522 ImageEditor.prototype.enterMode = function(mode) {
523 if (this.isLocked()) return;
525 if (this.currentMode_ == mode) {
526 // Currently active editor tool clicked, commit if modified.
527 this.leaveMode(this.currentMode_.updated_);
528 return;
531 this.recordToolUse(mode.name);
533 this.leaveModeGently();
534 // The above call could have caused a commit which might have initiated
535 // an asynchronous command execution. Wait for it to complete, then proceed
536 // with the mode set up.
537 this.commandQueue_.executeWhenReady(this.setUpMode_.bind(this, mode));
541 * Set up the new editing mode.
543 * @param {!ImageEditor.Mode} mode The mode.
544 * @private
546 ImageEditor.prototype.setUpMode_ = function(mode) {
547 this.currentTool_ = mode.button_;
549 ImageUtil.setAttribute(assert(this.currentTool_), 'pressed', true);
551 this.currentMode_ = mode;
552 this.currentMode_.setUp();
554 if (this.currentMode_.instant) { // Instant tool.
555 this.leaveMode(true);
556 return;
559 this.getPrompt().show(this.currentMode_.getMessage());
561 this.modeToolbar_.clear();
562 this.currentMode_.createTools(this.modeToolbar_);
563 this.modeToolbar_.show(true);
567 * The user clicked on 'OK' or 'Cancel' or on a different mode button.
568 * @param {boolean} commit True if commit is required.
570 ImageEditor.prototype.leaveMode = function(commit) {
571 if (!this.currentMode_) return;
573 if (!this.currentMode_.instant) {
574 this.getPrompt().hide();
577 this.modeToolbar_.show(false);
579 this.currentMode_.cleanUpUI();
580 if (commit) {
581 var self = this;
582 var command = this.currentMode_.getCommand();
583 if (command) { // Could be null if the user did not do anything.
584 this.commandQueue_.execute(command);
585 this.updateUndoRedo();
588 this.currentMode_.cleanUpCaches();
589 this.currentMode_ = null;
591 ImageUtil.setAttribute(assert(this.currentTool_), 'pressed', false);
592 this.currentTool_ = null;
596 * Leave the mode, commit only if required by the current mode.
598 ImageEditor.prototype.leaveModeGently = function() {
599 this.leaveMode(!!this.currentMode_ &&
600 this.currentMode_.updated_ &&
601 this.currentMode_.implicitCommit);
605 * Enter the editor mode with the given name.
607 * @param {string} name Mode name.
608 * @private
610 ImageEditor.prototype.enterModeByName_ = function(name) {
611 for (var i = 0; i !== this.modes_.length; i++) {
612 var mode = this.modes_[i];
613 if (mode.name === name) {
614 if (!mode.button_.hasAttribute('disabled'))
615 this.enterMode(mode);
616 return;
619 console.error('Mode "' + name + '" not found.');
623 * Key down handler.
624 * @param {!Event} event The keydown event.
625 * @return {boolean} True if handled.
627 ImageEditor.prototype.onKeyDown = function(event) {
628 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
629 case 'U+001B': // Escape
630 case 'Enter':
631 if (this.getMode()) {
632 this.leaveMode(event.keyIdentifier == 'Enter');
633 return true;
635 break;
637 case 'Ctrl-U+005A': // Ctrl+Z
638 if (this.commandQueue_.canUndo()) {
639 this.undo();
640 return true;
642 break;
644 case 'Ctrl-U+0059': // Ctrl+Y
645 if (this.commandQueue_.canRedo()) {
646 this.redo();
647 return true;
649 break;
651 case 'U+0041': // 'a'
652 this.enterModeByName_('autofix');
653 return true;
655 case 'U+0042': // 'b'
656 this.enterModeByName_('exposure');
657 return true;
659 case 'U+0043': // 'c'
660 this.enterModeByName_('crop');
661 return true;
663 case 'U+004C': // 'l'
664 this.enterModeByName_('rotate_left');
665 return true;
667 case 'U+0052': // 'r'
668 this.enterModeByName_('rotate_right');
669 return true;
671 return false;
675 * Double tap handler.
676 * @param {number} x X coordinate of the event.
677 * @param {number} y Y coordinate of the event.
678 * @private
680 ImageEditor.prototype.onDoubleTap_ = function(x, y) {
681 if (this.getMode()) {
682 var action = this.buffer_.getDoubleTapAction(x, y);
683 if (action == ImageBuffer.DoubleTapAction.COMMIT)
684 this.leaveMode(true);
685 else if (action == ImageBuffer.DoubleTapAction.CANCEL)
686 this.leaveMode(false);
691 * Hide the tools that overlap the given rectangular frame.
693 * @param {ImageRect=} opt_frame Hide the tool that overlaps this rect.
694 * @param {ImageRect=} opt_transparent But do not hide the tool that is
695 * completely inside this rect.
697 ImageEditor.prototype.hideOverlappingTools = function(
698 opt_frame, opt_transparent) {
699 var frame = opt_frame || null;
700 var transparent = opt_transparent || null;
702 var tools = this.rootContainer_.ownerDocument.querySelectorAll('.dimmable');
703 var changed = false;
704 for (var i = 0; i != tools.length; i++) {
705 var tool = tools[i];
706 var toolRect = tool.getBoundingClientRect();
707 var overlapping =
708 (!!frame && frame.intersects(toolRect)) &&
709 !(!!transparent && transparent.contains(toolRect));
710 if (overlapping && !tool.hasAttribute('dimmed') ||
711 !overlapping && tool.hasAttribute('dimmed')) {
712 ImageUtil.setAttribute(tool, 'dimmed', overlapping);
713 changed = true;
716 if (changed)
717 this.onToolsVisibilityChanged_();
721 * A helper object for panning the ImageBuffer.
723 * @param {!HTMLElement} rootContainer The top-level container.
724 * @param {!HTMLElement} container The container for mouse events.
725 * @param {!ImageBuffer} buffer Image buffer.
726 * @constructor
727 * @struct
729 ImageEditor.MouseControl = function(rootContainer, container, buffer) {
730 this.rootContainer_ = rootContainer;
731 this.container_ = container;
732 this.buffer_ = buffer;
734 var handlers = {
735 'touchstart': this.onTouchStart,
736 'touchend': this.onTouchEnd,
737 'touchcancel': this.onTouchCancel,
738 'touchmove': this.onTouchMove,
739 'mousedown': this.onMouseDown,
740 'mouseup': this.onMouseUp
743 for (var eventName in handlers) {
744 container.addEventListener(
745 eventName, handlers[eventName].bind(this), false);
748 // Mouse move handler has to be attached to the window to receive events
749 // from outside of the window. See: http://crbug.com/155705
750 window.addEventListener('mousemove', this.onMouseMove.bind(this), false);
753 * @type {?ImageBuffer.DragHandler}
754 * @private
756 this.dragHandler_ = null;
759 * @type {boolean}
760 * @private
762 this.dragHappened_ = false;
765 * @type {?{x: number, y: number, time:number}}
766 * @private
768 this.touchStartInfo_ = null;
771 * @type {?{x: number, y: number, time:number}}
772 * @private
774 this.previousTouchStartInfo_ = null;
778 * Maximum movement for touch to be detected as a tap (in pixels).
779 * @private
780 * @const
782 ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_ = 8;
785 * Maximum time for touch to be detected as a tap (in milliseconds).
786 * @private
787 * @const
789 ImageEditor.MouseControl.MAX_TAP_DURATION_ = 500;
792 * Maximum distance from the first tap to the second tap to be considered
793 * as a double tap.
794 * @private
795 * @const
797 ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_ = 32;
800 * Maximum time for touch to be detected as a double tap (in milliseconds).
801 * @private
802 * @const
804 ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_ = 1000;
807 * Returns an event's position.
809 * @param {!(MouseEvent|Touch)} e Pointer position.
810 * @return {!Object} A pair of x,y in page coordinates.
811 * @private
813 ImageEditor.MouseControl.getPosition_ = function(e) {
814 return {
815 x: e.pageX,
816 y: e.pageY
821 * Returns touch position or null if there is more than one touch position.
823 * @param {!TouchEvent} e Event.
824 * @return {Object?} A pair of x,y in page coordinates.
825 * @private
827 ImageEditor.MouseControl.prototype.getTouchPosition_ = function(e) {
828 if (e.targetTouches.length == 1)
829 return ImageEditor.MouseControl.getPosition_(e.targetTouches[0]);
830 else
831 return null;
835 * Touch start handler.
836 * @param {!TouchEvent} e Event.
838 ImageEditor.MouseControl.prototype.onTouchStart = function(e) {
839 var position = this.getTouchPosition_(e);
840 if (position) {
841 this.touchStartInfo_ = {
842 x: position.x,
843 y: position.y,
844 time: Date.now()
846 this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
847 true /* touch */);
848 this.dragHappened_ = false;
849 e.preventDefault();
854 * Touch end handler.
855 * @param {!TouchEvent} e Event.
857 ImageEditor.MouseControl.prototype.onTouchEnd = function(e) {
858 if (!this.dragHappened_ &&
859 this.touchStartInfo_ &&
860 Date.now() - this.touchStartInfo_.time <=
861 ImageEditor.MouseControl.MAX_TAP_DURATION_) {
862 this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y);
863 if (this.previousTouchStartInfo_ &&
864 Date.now() - this.previousTouchStartInfo_.time <
865 ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_) {
866 var prevTouchCircle = new Circle(
867 this.previousTouchStartInfo_.x,
868 this.previousTouchStartInfo_.y,
869 ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_);
870 if (prevTouchCircle.inside(this.touchStartInfo_.x,
871 this.touchStartInfo_.y)) {
872 this.doubleTapCallback_(this.touchStartInfo_.x, this.touchStartInfo_.y);
875 this.previousTouchStartInfo_ = this.touchStartInfo_;
876 } else {
877 this.previousTouchStartInfo_ = null;
879 this.onTouchCancel();
883 * Default double tap handler.
884 * @param {number} x X coordinate of the event.
885 * @param {number} y Y coordinate of the event.
886 * @private
888 ImageEditor.MouseControl.prototype.doubleTapCallback_ = function(x, y) {};
891 * Sets callback to be called when double tap detected.
892 * @param {function(number, number)} callback New double tap callback.
894 ImageEditor.MouseControl.prototype.setDoubleTapCallback = function(callback) {
895 this.doubleTapCallback_ = callback;
899 * Touch cancel handler.
901 ImageEditor.MouseControl.prototype.onTouchCancel = function() {
902 this.dragHandler_ = null;
903 this.dragHappened_ = false;
904 this.touchStartInfo_ = null;
905 this.lockMouse_(false);
909 * Touch move handler.
910 * @param {!TouchEvent} e Event.
912 ImageEditor.MouseControl.prototype.onTouchMove = function(e) {
913 var position = this.getTouchPosition_(e);
914 if (!position)
915 return;
917 if (this.touchStartInfo_ && !this.dragHappened_) {
918 var tapCircle = new Circle(
919 this.touchStartInfo_.x, this.touchStartInfo_.y,
920 ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_);
921 this.dragHappened_ = !tapCircle.inside(position.x, position.y);
923 if (this.dragHandler_ && this.dragHappened_) {
924 this.dragHandler_(position.x, position.y, e.shiftKey);
925 this.lockMouse_(true);
930 * Mouse down handler.
931 * @param {!MouseEvent} e Event.
933 ImageEditor.MouseControl.prototype.onMouseDown = function(e) {
934 var position = ImageEditor.MouseControl.getPosition_(e);
936 this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
937 false /* mouse */);
938 this.dragHappened_ = false;
939 this.updateCursor_(position);
943 * Mouse up handler.
944 * @param {!MouseEvent} e Event.
946 ImageEditor.MouseControl.prototype.onMouseUp = function(e) {
947 var position = ImageEditor.MouseControl.getPosition_(e);
949 if (!this.dragHappened_) {
950 this.buffer_.onClick(position.x, position.y);
952 this.dragHandler_ = null;
953 this.dragHappened_ = false;
954 this.lockMouse_(false);
958 * Mouse move handler.
959 * @param {!Event} e Event.
961 ImageEditor.MouseControl.prototype.onMouseMove = function(e) {
962 e = assertInstanceof(e, MouseEvent);
963 var position = ImageEditor.MouseControl.getPosition_(e);
965 if (this.dragHandler_ && !e.which) {
966 // mouseup must have happened while the mouse was outside our window.
967 this.dragHandler_ = null;
968 this.lockMouse_(false);
971 this.updateCursor_(position);
972 if (this.dragHandler_) {
973 this.dragHandler_(position.x, position.y, e.shiftKey);
974 this.dragHappened_ = true;
975 this.lockMouse_(true);
980 * Update the UI to reflect mouse drag state.
981 * @param {boolean} on True if dragging.
982 * @private
984 ImageEditor.MouseControl.prototype.lockMouse_ = function(on) {
985 ImageUtil.setAttribute(this.rootContainer_, 'mousedrag', on);
989 * Update the cursor.
991 * @param {!Object} position An object holding x and y properties.
992 * @private
994 ImageEditor.MouseControl.prototype.updateCursor_ = function(position) {
995 var oldCursor = this.container_.getAttribute('cursor');
996 var newCursor = this.buffer_.getCursorStyle(
997 position.x, position.y, !!this.dragHandler_);
998 if (newCursor != oldCursor) // Avoid flicker.
999 this.container_.setAttribute('cursor', newCursor);
1003 * A toolbar for the ImageEditor.
1004 * @param {!HTMLElement} parent The parent element.
1005 * @param {function(string)} displayStringFunction A string formatting function.
1006 * @param {function(Object)=} opt_updateCallback The callback called when
1007 * controls change.
1008 * @constructor
1009 * @struct
1011 ImageEditor.Toolbar = function(
1012 parent, displayStringFunction, opt_updateCallback) {
1013 this.wrapper_ = parent;
1014 this.displayStringFunction_ = displayStringFunction;
1017 * @type {?function(Object)}
1018 * @private
1020 this.updateCallback_ = opt_updateCallback || null;
1024 * Returns the parent element.
1025 * @return {!HTMLElement}
1027 ImageEditor.Toolbar.prototype.getElement = function() {
1028 return this.wrapper_;
1032 * Clear the toolbar.
1034 ImageEditor.Toolbar.prototype.clear = function() {
1035 ImageUtil.removeChildren(this.wrapper_);
1039 * Create a control.
1040 * @param {string} tagName The element tag name.
1041 * @return {!HTMLElement} The created control element.
1042 * @private
1044 ImageEditor.Toolbar.prototype.create_ = function(tagName) {
1045 return assertInstanceof(this.wrapper_.ownerDocument.createElement(tagName),
1046 HTMLElement);
1050 * Add a control.
1051 * @param {!HTMLElement} element The control to add.
1052 * @return {!HTMLElement} The added element.
1054 ImageEditor.Toolbar.prototype.add = function(element) {
1055 this.wrapper_.appendChild(element);
1056 return element;
1060 * Add a text label.
1061 * @param {string} name Label name.
1062 * @return {!HTMLElement} The added label.
1064 ImageEditor.Toolbar.prototype.addLabel = function(name) {
1065 var label = this.create_('span');
1066 label.textContent = this.displayStringFunction_(name);
1067 return this.add(label);
1071 * Add a button.
1073 * @param {string} name Button name.
1074 * @param {string} title Button title.
1075 * @param {function(Event)} handler onClick handler.
1076 * @param {string=} opt_class Extra class name.
1077 * @return {!HTMLElement} The added button.
1079 ImageEditor.Toolbar.prototype.addButton = function(
1080 name, title, handler, opt_class) {
1081 var button = this.create_('button');
1082 if (opt_class)
1083 button.classList.add(opt_class);
1084 var label = this.create_('span');
1085 label.textContent = this.displayStringFunction_(title);
1086 button.appendChild(label);
1087 button.label = this.displayStringFunction_(title);
1088 button.title = this.displayStringFunction_(title);
1089 button.addEventListener('click', handler, false);
1090 return this.add(button);
1094 * Add a range control (scalar value picker).
1096 * @param {string} name An option name.
1097 * @param {string} title An option title.
1098 * @param {number} min Min value of the option.
1099 * @param {number} value Default value of the option.
1100 * @param {number} max Max value of the options.
1101 * @param {number=} opt_scale A number to multiply by when setting
1102 * min/value/max in DOM.
1103 * @param {boolean=} opt_showNumeric True if numeric value should be displayed.
1104 * @return {!HTMLElement} Range element.
1106 ImageEditor.Toolbar.prototype.addRange = function(
1107 name, title, min, value, max, opt_scale, opt_showNumeric) {
1108 var self = this;
1110 var scale = opt_scale || 1;
1112 var range = this.create_('input');
1114 range.className = 'range';
1115 range.type = 'range';
1116 range.name = name;
1117 range.min = Math.ceil(min * scale);
1118 range.max = Math.floor(max * scale);
1120 var numeric = this.create_('div');
1121 numeric.className = 'numeric';
1122 function mirror() {
1123 numeric.textContent = Math.round(range.getValue() * scale) / scale;
1126 range.setValue = function(newValue) {
1127 range.value = Math.round(newValue * scale);
1128 mirror();
1131 range.getValue = function() {
1132 return Number(range.value) / scale;
1135 range.reset = function() {
1136 range.setValue(value);
1139 range.addEventListener('change',
1140 function() {
1141 mirror();
1142 if (self.updateCallback_)
1143 self.updateCallback_(self.getOptions());
1145 false);
1147 range.setValue(value);
1149 var label = this.create_('div');
1150 label.textContent = this.displayStringFunction_(title);
1151 label.className = 'label ' + name;
1152 this.add(label);
1153 this.add(range);
1155 if (opt_showNumeric)
1156 this.add(numeric);
1158 // Swallow the left and right keys, so they are not handled by other
1159 // listeners.
1160 range.addEventListener('keydown', function(e) {
1161 if (e.keyIdentifier === 'Left' || e.keyIdentifier === 'Right')
1162 e.stopPropagation();
1165 return range;
1169 * @return {!Object} options A map of options.
1171 ImageEditor.Toolbar.prototype.getOptions = function() {
1172 var values = {};
1173 for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
1174 if (child.name)
1175 values[child.name] = child.getValue();
1177 return values;
1181 * Reset the toolbar.
1183 ImageEditor.Toolbar.prototype.reset = function() {
1184 for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
1185 if (child.reset) child.reset();
1190 * Show/hide the toolbar.
1191 * @param {boolean} on True if show.
1193 ImageEditor.Toolbar.prototype.show = function(on) {
1194 if (!this.wrapper_.firstChild)
1195 return; // Do not show empty toolbar;
1197 this.wrapper_.hidden = !on;
1200 /** A prompt panel for the editor.
1202 * @param {!HTMLElement} container Container element.
1203 * @param {function(string, ...string)} displayStringFunction A formatting
1204 * function.
1205 * @constructor
1206 * @struct
1208 ImageEditor.Prompt = function(container, displayStringFunction) {
1209 this.container_ = container;
1210 this.displayStringFunction_ = displayStringFunction;
1213 * @type {HTMLDivElement}
1214 * @private
1216 this.wrapper_ = null;
1219 * @type {HTMLDivElement}
1220 * @private
1222 this.prompt_ = null;
1225 * @type {number}
1226 * @private
1228 this.timer_ = 0;
1232 * Reset the prompt.
1234 ImageEditor.Prompt.prototype.reset = function() {
1235 this.cancelTimer();
1236 if (this.wrapper_) {
1237 this.container_.removeChild(this.wrapper_);
1238 this.wrapper_ = null;
1239 this.prompt_ = null;
1244 * Cancel the delayed action.
1246 ImageEditor.Prompt.prototype.cancelTimer = function() {
1247 if (this.timer_) {
1248 clearTimeout(this.timer_);
1249 this.timer_ = 0;
1254 * Schedule the delayed action.
1255 * @param {function()} callback Callback.
1256 * @param {number} timeout Timeout.
1258 ImageEditor.Prompt.prototype.setTimer = function(callback, timeout) {
1259 this.cancelTimer();
1260 var self = this;
1261 this.timer_ = setTimeout(function() {
1262 self.timer_ = 0;
1263 callback();
1264 }, timeout);
1268 * Show the prompt.
1270 * @param {string} text The prompt text.
1271 * @param {number=} opt_timeout Timeout in ms.
1272 * @param {...Object} var_args varArgs for the formatting function.
1274 ImageEditor.Prompt.prototype.show = function(text, opt_timeout, var_args) {
1275 var args = [text].concat(Array.prototype.slice.call(arguments, 2));
1276 var message = this.displayStringFunction_.apply(null, args);
1277 this.showStringAt('center', message, opt_timeout);
1281 * Show the position at the specific position.
1283 * @param {string} pos The 'pos' attribute value.
1284 * @param {string} text The prompt text.
1285 * @param {number} timeout Timeout in ms.
1286 * @param {...Object} var_args varArgs for the formatting function.
1288 ImageEditor.Prompt.prototype.showAt = function(
1289 pos, text, timeout, var_args) {
1290 var args = [text].concat(Array.prototype.slice.call(arguments, 3));
1291 var message = this.displayStringFunction_.apply(null, args);
1292 this.showStringAt(pos, message, timeout);
1296 * Show the string in the prompt
1298 * @param {string} pos The 'pos' attribute value.
1299 * @param {string} text The prompt text.
1300 * @param {number=} opt_timeout Timeout in ms.
1302 ImageEditor.Prompt.prototype.showStringAt = function(pos, text, opt_timeout) {
1303 this.reset();
1304 if (!text)
1305 return;
1307 var document = this.container_.ownerDocument;
1308 this.wrapper_ = assertInstanceof(document.createElement('div'),
1309 HTMLDivElement);
1310 this.wrapper_.className = 'prompt-wrapper';
1311 this.wrapper_.setAttribute('pos', pos);
1312 this.container_.appendChild(this.wrapper_);
1314 this.prompt_ = assertInstanceof(document.createElement('div'),
1315 HTMLDivElement);
1316 this.prompt_.className = 'prompt';
1318 // Create an extra wrapper which opacity can be manipulated separately.
1319 var tool = document.createElement('div');
1320 tool.className = 'dimmable';
1321 this.wrapper_.appendChild(tool);
1322 tool.appendChild(this.prompt_);
1324 this.prompt_.textContent = text;
1326 var close = document.createElement('div');
1327 close.className = 'close';
1328 close.addEventListener('click', this.hide.bind(this));
1329 this.prompt_.appendChild(close);
1331 setTimeout(
1332 this.prompt_.setAttribute.bind(this.prompt_, 'state', 'fadein'), 0);
1334 if (opt_timeout)
1335 this.setTimer(this.hide.bind(this), opt_timeout);
1339 * Hide the prompt.
1341 ImageEditor.Prompt.prototype.hide = function() {
1342 if (!this.prompt_) return;
1343 this.prompt_.setAttribute('state', 'fadeout');
1344 // Allow some time for the animation to play out.
1345 this.setTimer(this.reset.bind(this), 500);