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
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.
23 viewport
, imageView
, prompt
, DOMContainers
, modes
, displayStringFunction
,
24 onToolsVisibilityChanged
) {
25 this.rootContainer_
= DOMContainers
.root
;
26 this.container_
= DOMContainers
.image
;
28 this.displayStringFunction_
= displayStringFunction
;
29 this.onToolsVisibilityChanged_
= onToolsVisibilityChanged
;
32 * @type {ImageEditor.Mode}
35 this.currentMode_
= null;
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.
74 * @type {!Array.<string>}
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
)));
89 * @type {!HTMLElement}
92 this.undoButton_
= this.createToolButton_('undo', 'GALLERY_UNDO',
93 this.undo
.bind(this));
94 this.registerAction_('undo');
97 * @type {!HTMLElement}
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.
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
170 ImageEditor
.prototype.openSession = function(
171 item
, effect
, saveFunction
, displayCallback
, loadCallback
) {
172 if (this.commandQueue_
)
173 throw new Error('Session not closed');
178 this.imageView_
.load(
179 item
, effect
, displayCallback
, function(loadType
, delay
, error
) {
181 self
.commandQueue_
= new CommandQueue(
182 self
.container_
.ownerDocument
, assert(self
.imageView_
.getCanvas()),
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();
207 if (!this.commandQueue_
) {
208 // Session is already closed.
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
);
228 if (!this.imageView_
.isLoading())
229 console
.warn('Inconsistent image editor state');
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();
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
326 * @param {string} name The mode name.
327 * @param {string} title The mode title.
330 * @extends {ImageBuffer.Overlay}
332 ImageEditor
.Mode = function(name
, title
) {
335 this.message_
= 'GALLERY_ENTER_WHEN_DONE';
340 this.implicitCommit
= false;
345 this.instant
= false;
348 * @type {ImageEditor}
357 this.viewport_
= null;
360 * @type {HTMLElement}
369 this.updated_
= false;
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
) {
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() {
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.
485 * @extends {ImageEditor.Mode}
488 ImageEditor
.Mode
.OneClick = function(name
, title
, command
) {
489 ImageEditor
.Mode
.call(this, name
, title
);
491 this.command_
= command
;
494 ImageEditor
.Mode
.OneClick
.prototype = {__proto__
: ImageEditor
.Mode
.prototype};
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.
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_
);
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.
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);
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();
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.
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
);
619 console
.error('Mode "' + name
+ '" not found.');
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
631 if (this.getMode()) {
632 this.leaveMode(event
.keyIdentifier
== 'Enter');
637 case 'Ctrl-U+005A': // Ctrl+Z
638 if (this.commandQueue_
.canUndo()) {
644 case 'Ctrl-U+0059': // Ctrl+Y
645 if (this.commandQueue_
.canRedo()) {
651 case 'U+0041': // 'a'
652 this.enterModeByName_('autofix');
655 case 'U+0042': // 'b'
656 this.enterModeByName_('exposure');
659 case 'U+0043': // 'c'
660 this.enterModeByName_('crop');
663 case 'U+004C': // 'l'
664 this.enterModeByName_('rotate_left');
667 case 'U+0052': // 'r'
668 this.enterModeByName_('rotate_right');
675 * Double tap handler.
676 * @param {number} x X coordinate of the event.
677 * @param {number} y Y coordinate of the event.
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');
704 for (var i
= 0; i
!= tools
.length
; i
++) {
706 var toolRect
= tool
.getBoundingClientRect();
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
);
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.
729 ImageEditor
.MouseControl = function(rootContainer
, container
, buffer
) {
730 this.rootContainer_
= rootContainer
;
731 this.container_
= container
;
732 this.buffer_
= buffer
;
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}
756 this.dragHandler_
= null;
762 this.dragHappened_
= false;
765 * @type {?{x: number, y: number, time:number}}
768 this.touchStartInfo_
= null;
771 * @type {?{x: number, y: number, time:number}}
774 this.previousTouchStartInfo_
= null;
778 * Maximum movement for touch to be detected as a tap (in pixels).
782 ImageEditor
.MouseControl
.MAX_MOVEMENT_FOR_TAP_
= 8;
785 * Maximum time for touch to be detected as a tap (in milliseconds).
789 ImageEditor
.MouseControl
.MAX_TAP_DURATION_
= 500;
792 * Maximum distance from the first tap to the second tap to be considered
797 ImageEditor
.MouseControl
.MAX_DISTANCE_FOR_DOUBLE_TAP_
= 32;
800 * Maximum time for touch to be detected as a double tap (in milliseconds).
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.
813 ImageEditor
.MouseControl
.getPosition_ = function(e
) {
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.
827 ImageEditor
.MouseControl
.prototype.getTouchPosition_ = function(e
) {
828 if (e
.targetTouches
.length
== 1)
829 return ImageEditor
.MouseControl
.getPosition_(e
.targetTouches
[0]);
835 * Touch start handler.
836 * @param {!TouchEvent} e Event.
838 ImageEditor
.MouseControl
.prototype.onTouchStart = function(e
) {
839 var position
= this.getTouchPosition_(e
);
841 this.touchStartInfo_
= {
846 this.dragHandler_
= this.buffer_
.getDragHandler(position
.x
, position
.y
,
848 this.dragHappened_
= false;
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_
;
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.
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
);
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
,
938 this.dragHappened_
= false;
939 this.updateCursor_(position
);
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.
984 ImageEditor
.MouseControl
.prototype.lockMouse_ = function(on
) {
985 ImageUtil
.setAttribute(this.rootContainer_
, 'mousedrag', on
);
991 * @param {!Object} position An object holding x and y properties.
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
1011 ImageEditor
.Toolbar = function(
1012 parent
, displayStringFunction
, opt_updateCallback
) {
1013 this.wrapper_
= parent
;
1014 this.displayStringFunction_
= displayStringFunction
;
1017 * @type {?function(Object)}
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_
);
1040 * @param {string} tagName The element tag name.
1041 * @return {!HTMLElement} The created control element.
1044 ImageEditor
.Toolbar
.prototype.create_ = function(tagName
) {
1045 return assertInstanceof(this.wrapper_
.ownerDocument
.createElement(tagName
),
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
);
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
);
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');
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
) {
1110 var scale
= opt_scale
|| 1;
1112 var range
= this.create_('input');
1114 range
.className
= 'range';
1115 range
.type
= 'range';
1117 range
.min
= Math
.ceil(min
* scale
);
1118 range
.max
= Math
.floor(max
* scale
);
1120 var numeric
= this.create_('div');
1121 numeric
.className
= 'numeric';
1123 numeric
.textContent
= Math
.round(range
.getValue() * scale
) / scale
;
1126 range
.setValue = function(newValue
) {
1127 range
.value
= Math
.round(newValue
* scale
);
1131 range
.getValue = function() {
1132 return Number(range
.value
) / scale
;
1135 range
.reset = function() {
1136 range
.setValue(value
);
1139 range
.addEventListener('change',
1142 if (self
.updateCallback_
)
1143 self
.updateCallback_(self
.getOptions());
1147 range
.setValue(value
);
1149 var label
= this.create_('div');
1150 label
.textContent
= this.displayStringFunction_(title
);
1151 label
.className
= 'label ' + name
;
1155 if (opt_showNumeric
)
1158 // Swallow the left and right keys, so they are not handled by other
1160 range
.addEventListener('keydown', function(e
) {
1161 if (e
.keyIdentifier
=== 'Left' || e
.keyIdentifier
=== 'Right')
1162 e
.stopPropagation();
1169 * @return {!Object} options A map of options.
1171 ImageEditor
.Toolbar
.prototype.getOptions = function() {
1173 for (var child
= this.wrapper_
.firstChild
; child
; child
= child
.nextSibling
) {
1175 values
[child
.name
] = child
.getValue();
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
1208 ImageEditor
.Prompt = function(container
, displayStringFunction
) {
1209 this.container_
= container
;
1210 this.displayStringFunction_
= displayStringFunction
;
1213 * @type {HTMLDivElement}
1216 this.wrapper_
= null;
1219 * @type {HTMLDivElement}
1222 this.prompt_
= null;
1234 ImageEditor
.Prompt
.prototype.reset = function() {
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() {
1248 clearTimeout(this.timer_
);
1254 * Schedule the delayed action.
1255 * @param {function()} callback Callback.
1256 * @param {number} timeout Timeout.
1258 ImageEditor
.Prompt
.prototype.setTimer = function(callback
, timeout
) {
1261 this.timer_
= setTimeout(function() {
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
) {
1307 var document
= this.container_
.ownerDocument
;
1308 this.wrapper_
= assertInstanceof(document
.createElement('div'),
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'),
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
);
1332 this.prompt_
.setAttribute
.bind(this.prompt_
, 'state', 'fadein'), 0);
1335 this.setTimer(this.hide
.bind(this), opt_timeout
);
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);