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.
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 editor.
13 * @param {Array.<ImageEditor.Mode>} modes Available editor modes.
14 * @param {function(string, ...[string])} displayStringFunction String
15 * formatting function.
16 * @param {function()} onToolsVisibilityChanged Callback to be called, when
17 * some of the UI elements have been dimmed or revealed.
21 viewport, imageView, prompt, DOMContainers, modes, displayStringFunction,
22 onToolsVisibilityChanged) {
23 this.rootContainer_ = DOMContainers.root;
24 this.container_ = DOMContainers.image;
26 this.displayStringFunction_ = displayStringFunction;
27 this.onToolsVisibilityChanged_ = onToolsVisibilityChanged;
29 ImageUtil.removeChildren(this.container_);
31 this.viewport_ = viewport;
32 this.viewport_.setScreenSize(
33 this.container_.clientWidth, this.container_.clientHeight);
35 this.imageView_ = imageView;
36 this.imageView_.addContentCallback(this.onContentUpdate_.bind(this));
38 this.buffer_ = new ImageBuffer();
39 this.buffer_.addOverlay(this.imageView_);
41 this.panControl_ = new ImageEditor.MouseControl(
42 this.rootContainer_, this.container_, this.getBuffer());
43 this.panControl_.setDoubleTapCallback(this.onDoubleTap_.bind(this));
45 this.mainToolbar_ = new ImageEditor.Toolbar(
46 DOMContainers.toolbar, displayStringFunction);
48 this.modeToolbar_ = new ImageEditor.Toolbar(
49 DOMContainers.mode, displayStringFunction,
50 this.onOptionsChange.bind(this));
52 this.prompt_ = prompt;
54 this.createToolButtons();
56 this.commandQueue_ = null;
60 * @return {boolean} True if no user commands are to be accepted.
62 ImageEditor.prototype.isLocked = function() {
63 return !this.commandQueue_ || this.commandQueue_.isBusy();
67 * @return {boolean} True if the command queue is busy.
69 ImageEditor.prototype.isBusy = function() {
70 return this.commandQueue_ && this.commandQueue_.isBusy();
74 * Reflect the locked state of the editor in the UI.
75 * @param {boolean} on True if locked.
77 ImageEditor.prototype.lockUI = function(on) {
78 ImageUtil.setAttribute(this.rootContainer_, 'locked', on);
82 * Report the tool use to the metrics subsystem.
83 * @param {string} name Action name.
85 ImageEditor.prototype.recordToolUse = function(name) {
86 ImageUtil.metrics.recordEnum(
87 ImageUtil.getMetricName('Tool'), name, this.actionNames_);
91 * Content update handler.
94 ImageEditor.prototype.onContentUpdate_ = function() {
95 for (var i = 0; i != this.modes_.length; i++) {
96 var mode = this.modes_[i];
97 ImageUtil.setAttribute(mode.button_, 'disabled', !mode.isApplicable());
102 * Open the editing session for a new image.
104 * @param {Gallery.Item} item Gallery item.
105 * @param {Object} effect Transition effect object.
106 * @param {function(function())} saveFunction Image save function.
107 * @param {function(number)} displayCallback Display callback.
108 * @param {function(number, number, *=)} loadCallback Load callback.
110 ImageEditor.prototype.openSession = function(
111 item, effect, saveFunction, displayCallback, loadCallback) {
112 if (this.commandQueue_)
113 throw new Error('Session not closed');
118 this.imageView_.load(
119 item, effect, displayCallback, function(loadType, delay, error) {
121 self.commandQueue_ = new CommandQueue(
122 self.container_.ownerDocument,
123 self.imageView_.getCanvas(),
125 self.commandQueue_.attachUI(
126 self.getImageView(), self.getPrompt(), self.lockUI.bind(self));
127 self.updateUndoRedo();
128 loadCallback(loadType, delay, error);
133 * Close the current image editing session.
134 * @param {function()} callback Callback.
136 ImageEditor.prototype.closeSession = function(callback) {
137 this.getPrompt().hide();
138 if (this.imageView_.isLoading()) {
139 if (this.commandQueue_) {
140 console.warn('Inconsistent image editor state');
141 this.commandQueue_ = null;
143 this.imageView_.cancelLoad();
148 if (!this.commandQueue_) {
149 // Session is already closed.
154 this.executeWhenReady(callback);
155 this.commandQueue_.close();
156 this.commandQueue_ = null;
160 * Commit the current operation and execute the action.
162 * @param {function()} callback Callback.
164 ImageEditor.prototype.executeWhenReady = function(callback) {
165 if (this.commandQueue_) {
166 this.leaveModeGently();
167 this.commandQueue_.executeWhenReady(callback);
169 if (!this.imageView_.isLoading())
170 console.warn('Inconsistent image editor state');
176 * @return {boolean} True if undo queue is not empty.
178 ImageEditor.prototype.canUndo = function() {
179 return this.commandQueue_ && this.commandQueue_.canUndo();
183 * Undo the recently executed command.
185 ImageEditor.prototype.undo = function() {
186 if (this.isLocked()) return;
187 this.recordToolUse('undo');
189 // First undo click should dismiss the uncommitted modifications.
190 if (this.currentMode_ && this.currentMode_.isUpdated()) {
191 this.currentMode_.reset();
195 this.getPrompt().hide();
196 this.leaveMode(false);
197 this.commandQueue_.undo();
198 this.updateUndoRedo();
202 * Redo the recently un-done command.
204 ImageEditor.prototype.redo = function() {
205 if (this.isLocked()) return;
206 this.recordToolUse('redo');
207 this.getPrompt().hide();
208 this.leaveMode(false);
209 this.commandQueue_.redo();
210 this.updateUndoRedo();
214 * Update Undo/Redo buttons state.
216 ImageEditor.prototype.updateUndoRedo = function() {
217 var canUndo = this.commandQueue_ && this.commandQueue_.canUndo();
218 var canRedo = this.commandQueue_ && this.commandQueue_.canRedo();
219 ImageUtil.setAttribute(this.undoButton_, 'disabled', !canUndo);
220 this.redoButton_.hidden = !canRedo;
224 * @return {HTMLCanvasElement} The current image canvas.
226 ImageEditor.prototype.getCanvas = function() {
227 return this.getImageView().getCanvas();
231 * @return {ImageBuffer} ImageBuffer instance.
233 ImageEditor.prototype.getBuffer = function() { return this.buffer_; };
236 * @return {ImageView} ImageView instance.
238 ImageEditor.prototype.getImageView = function() { return this.imageView_; };
241 * @return {Viewport} Viewport instance.
243 ImageEditor.prototype.getViewport = function() { return this.viewport_; };
246 * @return {ImageEditor.Prompt} Prompt instance.
248 ImageEditor.prototype.getPrompt = function() { return this.prompt_; };
251 * Handle the toolbar controls update.
252 * @param {Object} options A map of options.
254 ImageEditor.prototype.onOptionsChange = function(options) {
255 ImageUtil.trace.resetTimer('update');
256 if (this.currentMode_) {
257 this.currentMode_.update(options);
259 ImageUtil.trace.reportTimer('update');
263 * ImageEditor.Mode represents a modal state dedicated to a specific operation.
264 * Inherits from ImageBuffer. Overlay to simplify the drawing of mode-specific
267 * @param {string} name The mode name.
268 * @param {string} title The mode title.
272 ImageEditor.Mode = function(name, title) {
275 this.message_ = 'GALLERY_ENTER_WHEN_DONE';
278 ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype };
281 * @return {Viewport} Viewport instance.
283 ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_; };
286 * @return {ImageView} ImageView instance.
288 ImageEditor.Mode.prototype.getImageView = function() {
289 return this.imageView_;
293 * @return {string} The mode-specific message to be displayed when entering.
295 ImageEditor.Mode.prototype.getMessage = function() { return this.message_; };
298 * @return {boolean} True if the mode is applicable in the current context.
300 ImageEditor.Mode.prototype.isApplicable = function() { return true; };
303 * Called once after creating the mode button.
305 * @param {ImageEditor} editor The editor instance.
306 * @param {HTMLElement} button The mode button.
309 ImageEditor.Mode.prototype.bind = function(editor, button) {
310 this.editor_ = editor;
311 this.editor_.registerAction_(this.name);
312 this.button_ = button;
313 this.viewport_ = editor.getViewport();
314 this.imageView_ = editor.getImageView();
318 * Called before entering the mode.
320 ImageEditor.Mode.prototype.setUp = function() {
321 this.editor_.getBuffer().addOverlay(this);
322 this.updated_ = false;
326 * Create mode-specific controls here.
327 * @param {ImageEditor.Toolbar} toolbar The toolbar to populate.
329 ImageEditor.Mode.prototype.createTools = function(toolbar) {};
332 * Called before exiting the mode.
334 ImageEditor.Mode.prototype.cleanUpUI = function() {
335 this.editor_.getBuffer().removeOverlay(this);
339 * Called after exiting the mode.
341 ImageEditor.Mode.prototype.cleanUpCaches = function() {};
344 * Called when any of the controls changed its value.
345 * @param {Object} options A map of options.
347 ImageEditor.Mode.prototype.update = function(options) {
352 * Mark the editor mode as updated.
354 ImageEditor.Mode.prototype.markUpdated = function() {
355 this.updated_ = true;
359 * @return {boolean} True if the mode controls changed.
361 ImageEditor.Mode.prototype.isUpdated = function() { return this.updated_; };
364 * Resets the mode to a clean state.
366 ImageEditor.Mode.prototype.reset = function() {
367 this.editor_.modeToolbar_.reset();
368 this.updated_ = false;
372 * One-click editor tool, requires no interaction, just executes the command.
374 * @param {string} name The mode name.
375 * @param {string} title The mode title.
376 * @param {Command} command The command to execute on click.
379 ImageEditor.Mode.OneClick = function(name, title, command) {
380 ImageEditor.Mode.call(this, name, title);
382 this.command_ = command;
385 ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype};
388 * @return {Command} command.
390 ImageEditor.Mode.OneClick.prototype.getCommand = function() {
391 return this.command_;
395 * Register the action name. Required for metrics reporting.
396 * @param {string} name Button name.
399 ImageEditor.prototype.registerAction_ = function(name) {
400 this.actionNames_.push(name);
404 * Populate the toolbar.
406 ImageEditor.prototype.createToolButtons = function() {
407 this.mainToolbar_.clear();
408 this.actionNames_ = [];
411 function createButton(name, title, handler) {
412 return self.mainToolbar_.addButton(name,
415 name /* opt_className */);
418 for (var i = 0; i != this.modes_.length; i++) {
419 var mode = this.modes_[i];
420 mode.bind(this, createButton(mode.name,
422 this.enterMode.bind(this, mode)));
425 this.undoButton_ = createButton('undo',
427 this.undo.bind(this));
428 this.registerAction_('undo');
430 this.redoButton_ = createButton('redo',
432 this.redo.bind(this));
433 this.registerAction_('redo');
437 * @return {ImageEditor.Mode} The current mode.
439 ImageEditor.prototype.getMode = function() { return this.currentMode_; };
442 * The user clicked on the mode button.
444 * @param {ImageEditor.Mode} mode The new mode.
446 ImageEditor.prototype.enterMode = function(mode) {
447 if (this.isLocked()) return;
449 if (this.currentMode_ == mode) {
450 // Currently active editor tool clicked, commit if modified.
451 this.leaveMode(this.currentMode_.updated_);
455 this.recordToolUse(mode.name);
457 this.leaveModeGently();
458 // The above call could have caused a commit which might have initiated
459 // an asynchronous command execution. Wait for it to complete, then proceed
460 // with the mode set up.
461 this.commandQueue_.executeWhenReady(this.setUpMode_.bind(this, mode));
465 * Set up the new editing mode.
467 * @param {ImageEditor.Mode} mode The mode.
470 ImageEditor.prototype.setUpMode_ = function(mode) {
471 this.currentTool_ = mode.button_;
473 ImageUtil.setAttribute(this.currentTool_, 'pressed', true);
475 this.currentMode_ = mode;
476 this.currentMode_.setUp();
478 if (this.currentMode_.instant) { // Instant tool.
479 this.leaveMode(true);
483 this.getPrompt().show(this.currentMode_.getMessage());
485 this.modeToolbar_.clear();
486 this.currentMode_.createTools(this.modeToolbar_);
487 this.modeToolbar_.show(true);
491 * The user clicked on 'OK' or 'Cancel' or on a different mode button.
492 * @param {boolean} commit True if commit is required.
494 ImageEditor.prototype.leaveMode = function(commit) {
495 if (!this.currentMode_) return;
497 if (!this.currentMode_.instant) {
498 this.getPrompt().hide();
501 this.modeToolbar_.show(false);
503 this.currentMode_.cleanUpUI();
506 var command = this.currentMode_.getCommand();
507 if (command) { // Could be null if the user did not do anything.
508 this.commandQueue_.execute(command);
509 this.updateUndoRedo();
512 this.currentMode_.cleanUpCaches();
513 this.currentMode_ = null;
515 ImageUtil.setAttribute(this.currentTool_, 'pressed', false);
516 this.currentTool_ = null;
520 * Leave the mode, commit only if required by the current mode.
522 ImageEditor.prototype.leaveModeGently = function() {
523 this.leaveMode(this.currentMode_ &&
524 this.currentMode_.updated_ &&
525 this.currentMode_.implicitCommit);
529 * Enter the editor mode with the given name.
531 * @param {string} name Mode name.
534 ImageEditor.prototype.enterModeByName_ = function(name) {
535 for (var i = 0; i != this.modes_.length; i++) {
536 var mode = this.modes_[i];
537 if (mode.name == name) {
538 if (!mode.button_.hasAttribute('disabled'))
539 this.enterMode(mode);
543 console.error('Mode "' + name + '" not found.');
548 * @param {Event} event The keydown event.
549 * @return {boolean} True if handled.
551 ImageEditor.prototype.onKeyDown = function(event) {
552 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
553 case 'U+001B': // Escape
555 if (this.getMode()) {
556 this.leaveMode(event.keyIdentifier == 'Enter');
561 case 'Ctrl-U+005A': // Ctrl+Z
562 if (this.commandQueue_.canUndo()) {
568 case 'Ctrl-U+0059': // Ctrl+Y
569 if (this.commandQueue_.canRedo()) {
575 case 'U+0041': // 'a'
576 this.enterModeByName_('autofix');
579 case 'U+0042': // 'b'
580 this.enterModeByName_('exposure');
583 case 'U+0043': // 'c'
584 this.enterModeByName_('crop');
587 case 'U+004C': // 'l'
588 this.enterModeByName_('rotate_left');
591 case 'U+0052': // 'r'
592 this.enterModeByName_('rotate_right');
599 * Double tap handler.
600 * @param {number} x X coordinate of the event.
601 * @param {number} y Y coordinate of the event.
604 ImageEditor.prototype.onDoubleTap_ = function(x, y) {
605 if (this.getMode()) {
606 var action = this.buffer_.getDoubleTapAction(x, y);
607 if (action == ImageBuffer.DoubleTapAction.COMMIT)
608 this.leaveMode(true);
609 else if (action == ImageBuffer.DoubleTapAction.CANCEL)
610 this.leaveMode(false);
615 * Hide the tools that overlap the given rectangular frame.
617 * @param {ImageRect} frame Hide the tool that overlaps this rect.
618 * @param {ImageRect} transparent But do not hide the tool that is completely
621 ImageEditor.prototype.hideOverlappingTools = function(frame, transparent) {
622 var tools = this.rootContainer_.ownerDocument.querySelectorAll('.dimmable');
624 for (var i = 0; i != tools.length; i++) {
626 var toolRect = tool.getBoundingClientRect();
628 (frame && frame.intersects(toolRect)) &&
629 !(transparent && transparent.contains(toolRect));
630 if (overlapping && !tool.hasAttribute('dimmed') ||
631 !overlapping && tool.hasAttribute('dimmed')) {
632 ImageUtil.setAttribute(tool, 'dimmed', overlapping);
637 this.onToolsVisibilityChanged_();
641 * A helper object for panning the ImageBuffer.
643 * @param {HTMLElement} rootContainer The top-level container.
644 * @param {HTMLElement} container The container for mouse events.
645 * @param {ImageBuffer} buffer Image buffer.
648 ImageEditor.MouseControl = function(rootContainer, container, buffer) {
649 this.rootContainer_ = rootContainer;
650 this.container_ = container;
651 this.buffer_ = buffer;
654 'touchstart': this.onTouchStart,
655 'touchend': this.onTouchEnd,
656 'touchcancel': this.onTouchCancel,
657 'touchmove': this.onTouchMove,
658 'mousedown': this.onMouseDown,
659 'mouseup': this.onMouseUp
662 for (var eventName in handlers) {
663 container.addEventListener(
664 eventName, handlers[eventName].bind(this), false);
667 // Mouse move handler has to be attached to the window to receive events
668 // from outside of the window. See: http://crbug.com/155705
669 window.addEventListener('mousemove', this.onMouseMove.bind(this), false);
673 * Maximum movement for touch to be detected as a tap (in pixels).
676 ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_ = 8;
679 * Maximum time for touch to be detected as a tap (in milliseconds).
682 ImageEditor.MouseControl.MAX_TAP_DURATION_ = 500;
685 * Maximum distance from the first tap to the second tap to be considered
689 ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_ = 32;
692 * Maximum time for touch to be detected as a double tap (in milliseconds).
695 ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_ = 1000;
698 * Returns an event's position.
700 * @param {MouseEvent|Touch} e Pointer position.
701 * @return {Object} A pair of x,y in page coordinates.
704 ImageEditor.MouseControl.getPosition_ = function(e) {
712 * Returns touch position or null if there is more than one touch position.
714 * @param {TouchEvent} e Event.
715 * @return {Object?} A pair of x,y in page coordinates.
718 ImageEditor.MouseControl.prototype.getTouchPosition_ = function(e) {
719 if (e.targetTouches.length == 1)
720 return ImageEditor.MouseControl.getPosition_(e.targetTouches[0]);
726 * Touch start handler.
727 * @param {TouchEvent} e Event.
729 ImageEditor.MouseControl.prototype.onTouchStart = function(e) {
730 var position = this.getTouchPosition_(e);
732 this.touchStartInfo_ = {
737 this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
739 this.dragHappened_ = false;
746 * @param {TouchEvent} e Event.
748 ImageEditor.MouseControl.prototype.onTouchEnd = function(e) {
749 if (!this.dragHappened_ &&
750 this.touchStartInfo_ &&
751 Date.now() - this.touchStartInfo_.time <=
752 ImageEditor.MouseControl.MAX_TAP_DURATION_) {
753 this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y);
754 if (this.previousTouchStartInfo_ &&
755 Date.now() - this.previousTouchStartInfo_.time <
756 ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_) {
757 var prevTouchCircle = new Circle(
758 this.previousTouchStartInfo_.x,
759 this.previousTouchStartInfo_.y,
760 ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_);
761 if (prevTouchCircle.inside(this.touchStartInfo_.x,
762 this.touchStartInfo_.y)) {
763 this.doubleTapCallback_(this.touchStartInfo_.x, this.touchStartInfo_.y);
766 this.previousTouchStartInfo_ = this.touchStartInfo_;
768 this.previousTouchStartInfo_ = null;
770 this.onTouchCancel(e);
774 * Default double tap handler.
775 * @param {number} x X coordinate of the event.
776 * @param {number} y Y coordinate of the event.
779 ImageEditor.MouseControl.prototype.doubleTapCallback_ = function(x, y) {};
782 * Sets callback to be called when double tap detected.
783 * @param {function(number, number)} callback New double tap callback.
785 ImageEditor.MouseControl.prototype.setDoubleTapCallback = function(callback) {
786 this.doubleTapCallback_ = callback;
790 * Touch cancel handler.
792 ImageEditor.MouseControl.prototype.onTouchCancel = function() {
793 this.dragHandler_ = null;
794 this.dragHappened_ = false;
795 this.touchStartInfo_ = null;
796 this.lockMouse_(false);
800 * Touch move handler.
801 * @param {TouchEvent} e Event.
803 ImageEditor.MouseControl.prototype.onTouchMove = function(e) {
804 var position = this.getTouchPosition_(e);
808 if (this.touchStartInfo_ && !this.dragHappened_) {
809 var tapCircle = new Circle(
810 this.touchStartInfo_.x, this.touchStartInfo_.y,
811 ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_);
812 this.dragHappened_ = !tapCircle.inside(position.x, position.y);
814 if (this.dragHandler_ && this.dragHappened_) {
815 this.dragHandler_(position.x, position.y, e.shiftKey);
816 this.lockMouse_(true);
821 * Mouse down handler.
822 * @param {MouseEvent} e Event.
824 ImageEditor.MouseControl.prototype.onMouseDown = function(e) {
825 var position = ImageEditor.MouseControl.getPosition_(e);
827 this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
829 this.dragHappened_ = false;
830 this.updateCursor_(position);
835 * @param {MouseEvent} e Event.
837 ImageEditor.MouseControl.prototype.onMouseUp = function(e) {
838 var position = ImageEditor.MouseControl.getPosition_(e);
840 if (!this.dragHappened_) {
841 this.buffer_.onClick(position.x, position.y);
843 this.dragHandler_ = null;
844 this.dragHappened_ = false;
845 this.lockMouse_(false);
849 * Mouse move handler.
850 * @param {MouseEvent} e Event.
852 ImageEditor.MouseControl.prototype.onMouseMove = function(e) {
853 var position = ImageEditor.MouseControl.getPosition_(e);
855 if (this.dragHandler_ && !e.which) {
856 // mouseup must have happened while the mouse was outside our window.
857 this.dragHandler_ = null;
858 this.lockMouse_(false);
861 this.updateCursor_(position);
862 if (this.dragHandler_) {
863 this.dragHandler_(position.x, position.y, e.shiftKey);
864 this.dragHappened_ = true;
865 this.lockMouse_(true);
870 * Update the UI to reflect mouse drag state.
871 * @param {boolean} on True if dragging.
874 ImageEditor.MouseControl.prototype.lockMouse_ = function(on) {
875 ImageUtil.setAttribute(this.rootContainer_, 'mousedrag', on);
881 * @param {Object} position An object holding x and y properties.
884 ImageEditor.MouseControl.prototype.updateCursor_ = function(position) {
885 var oldCursor = this.container_.getAttribute('cursor');
886 var newCursor = this.buffer_.getCursorStyle(
887 position.x, position.y, !!this.dragHandler_);
888 if (newCursor != oldCursor) // Avoid flicker.
889 this.container_.setAttribute('cursor', newCursor);
893 * A toolbar for the ImageEditor.
894 * @param {HTMLElement} parent The parent element.
895 * @param {function(string)} displayStringFunction A string formatting function.
896 * @param {function(Object)} updateCallback The callback called when controls
900 ImageEditor.Toolbar = function(parent, displayStringFunction, updateCallback) {
901 this.wrapper_ = parent;
902 this.displayStringFunction_ = displayStringFunction;
903 this.updateCallback_ = updateCallback;
907 ImageEditor.Toolbar.prototype = {
909 return this.wrapper_;
916 ImageEditor.Toolbar.prototype.clear = function() {
917 ImageUtil.removeChildren(this.wrapper_);
922 * @param {string} tagName The element tag name.
923 * @return {HTMLElement} The created control element.
926 ImageEditor.Toolbar.prototype.create_ = function(tagName) {
927 return this.wrapper_.ownerDocument.createElement(tagName);
932 * @param {HTMLElement} element The control to add.
933 * @return {HTMLElement} The added element.
935 ImageEditor.Toolbar.prototype.add = function(element) {
936 this.wrapper_.appendChild(element);
942 * @param {string} name Label name.
943 * @return {HTMLElement} The added label.
945 ImageEditor.Toolbar.prototype.addLabel = function(name) {
946 var label = this.create_('span');
947 label.textContent = this.displayStringFunction_(name);
948 return this.add(label);
954 * @param {string} name Button name.
955 * @param {string} title Button title.
956 * @param {function(Event)} handler onClick handler.
957 * @param {string=} opt_class Extra class name.
958 * @return {HTMLElement} The added button.
960 ImageEditor.Toolbar.prototype.addButton = function(
961 name, title, handler, opt_class) {
962 var button = this.create_('button');
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);
975 * Add a range control (scalar value picker).
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.
987 ImageEditor.Toolbar.prototype.addRange = function(
988 name, title, min, value, max, scale, opt_showNumeric) {
993 var range = this.create_('input');
995 range.className = 'range';
996 range.type = 'range';
998 range.min = Math.ceil(min * scale);
999 range.max = Math.floor(max * scale);
1001 var numeric = this.create_('div');
1002 numeric.className = 'numeric';
1004 numeric.textContent = Math.round(range.getValue() * scale) / scale;
1007 range.setValue = function(newValue) {
1008 range.value = Math.round(newValue * scale);
1012 range.getValue = function() {
1013 return Number(range.value) / scale;
1016 range.reset = function() {
1017 range.setValue(value);
1020 range.addEventListener('change',
1023 self.updateCallback_(self.getOptions());
1027 range.setValue(value);
1029 var label = this.create_('div');
1030 label.textContent = this.displayStringFunction_(title);
1031 label.className = 'label ' + name;
1035 if (opt_showNumeric)
1038 // Swallow the left and right keys, so they are not handled by other
1040 range.addEventListener('keydown', function(e) {
1041 if (e.keyIdentifier === 'Left' || e.keyIdentifier === 'Right')
1042 e.stopPropagation();
1049 * @return {Object} options A map of options.
1051 ImageEditor.Toolbar.prototype.getOptions = function() {
1053 for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
1055 values[child.name] = child.getValue();
1061 * Reset the toolbar.
1063 ImageEditor.Toolbar.prototype.reset = function() {
1064 for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
1065 if (child.reset) child.reset();
1070 * Show/hide the toolbar.
1071 * @param {boolean} on True if show.
1073 ImageEditor.Toolbar.prototype.show = function(on) {
1074 if (!this.wrapper_.firstChild)
1075 return; // Do not show empty toolbar;
1077 this.wrapper_.hidden = !on;
1080 /** A prompt panel for the editor.
1082 * @param {HTMLElement} container Container element.
1083 * @param {function(string, ...[string])} displayStringFunction A formatting
1087 ImageEditor.Prompt = function(container, displayStringFunction) {
1088 this.container_ = container;
1089 this.displayStringFunction_ = displayStringFunction;
1095 ImageEditor.Prompt.prototype.reset = function() {
1097 if (this.wrapper_) {
1098 this.container_.removeChild(this.wrapper_);
1099 this.wrapper_ = null;
1100 this.prompt_ = null;
1105 * Cancel the delayed action.
1107 ImageEditor.Prompt.prototype.cancelTimer = function() {
1109 clearTimeout(this.timer_);
1115 * Schedule the delayed action.
1116 * @param {function()} callback Callback.
1117 * @param {number} timeout Timeout.
1119 ImageEditor.Prompt.prototype.setTimer = function(callback, timeout) {
1122 this.timer_ = setTimeout(function() {
1131 * @param {string} text The prompt text.
1132 * @param {number} timeout Timeout in ms.
1133 * @param {...Object} var_args varArgs for the formatting function.
1135 ImageEditor.Prompt.prototype.show = function(text, timeout, var_args) {
1136 var args = [text].concat(Array.prototype.slice.call(arguments, 2));
1137 var message = this.displayStringFunction_.apply(null, args);
1138 this.showStringAt('center', message, timeout);
1142 * Show the position at the specific position.
1144 * @param {string} pos The 'pos' attribute value.
1145 * @param {string} text The prompt text.
1146 * @param {number} timeout Timeout in ms.
1147 * @param {...Object} var_args varArgs for the formatting function.
1149 ImageEditor.Prompt.prototype.showAt = function(
1150 pos, text, timeout, var_args) {
1151 var args = [text].concat(Array.prototype.slice.call(arguments, 3));
1152 var message = this.displayStringFunction_.apply(null, args);
1153 this.showStringAt(pos, message, timeout);
1157 * Show the string in the prompt
1159 * @param {string} pos The 'pos' attribute value.
1160 * @param {string} text The prompt text.
1161 * @param {number} timeout Timeout in ms.
1163 ImageEditor.Prompt.prototype.showStringAt = function(pos, text, timeout) {
1168 var document = this.container_.ownerDocument;
1169 this.wrapper_ = document.createElement('div');
1170 this.wrapper_.className = 'prompt-wrapper';
1171 this.wrapper_.setAttribute('pos', pos);
1172 this.container_.appendChild(this.wrapper_);
1174 this.prompt_ = document.createElement('div');
1175 this.prompt_.className = 'prompt';
1177 // Create an extra wrapper which opacity can be manipulated separately.
1178 var tool = document.createElement('div');
1179 tool.className = 'dimmable';
1180 this.wrapper_.appendChild(tool);
1181 tool.appendChild(this.prompt_);
1183 this.prompt_.textContent = text;
1185 var close = document.createElement('div');
1186 close.className = 'close';
1187 close.addEventListener('click', this.hide.bind(this));
1188 this.prompt_.appendChild(close);
1191 this.prompt_.setAttribute.bind(this.prompt_, 'state', 'fadein'), 0);
1194 this.setTimer(this.hide.bind(this), timeout);
1200 ImageEditor.Prompt.prototype.hide = function() {
1201 if (!this.prompt_) return;
1202 this.prompt_.setAttribute('state', 'fadeout');
1203 // Allow some time for the animation to play out.
1204 this.setTimer(this.reset.bind(this), 500);