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