| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as Platform from '../../core/platform/platform.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Tooltip text that appears when hovering over largeicon pan button in Transform Controller of the Layers panel |
| */ |
| panModeX: 'Pan mode (X)', |
| /** |
| *@description Tooltip text that appears when hovering over largeicon rotate button in Transform Controller of the Layers panel |
| */ |
| rotateModeV: 'Rotate mode (V)', |
| /** |
| *@description Tooltip text that appears when hovering over the largeicon center button in the Transform Controller of the Layers panel |
| */ |
| resetTransform: 'Reset transform (0)', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('panels/layer_viewer/TransformController.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| export class TransformController extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { |
| private mode!: Modes; |
| private scaleInternal: number; |
| private offsetXInternal: number; |
| private offsetYInternal: number; |
| private rotateXInternal: number; |
| private rotateYInternal: number; |
| private oldRotateX: number; |
| private oldRotateY: number; |
| private originX: number; |
| private originY: number; |
| element: HTMLElement; |
| private minScale: number; |
| private maxScale: number; |
| private readonly controlPanelToolbar: UI.Toolbar.Toolbar; |
| private readonly modeButtons: {[x: string]: UI.Toolbar.ToolbarToggle}; |
| constructor(element: HTMLElement, disableRotate?: boolean) { |
| super(); |
| this.scaleInternal = 1; |
| this.offsetXInternal = 0; |
| this.offsetYInternal = 0; |
| this.rotateXInternal = 0; |
| this.rotateYInternal = 0; |
| this.oldRotateX = 0; |
| this.oldRotateY = 0; |
| this.originX = 0; |
| this.originY = 0; |
| this.element = element; |
| this.registerShortcuts(); |
| UI.UIUtils.installDragHandle( |
| element, this.onDragStart.bind(this), this.onDrag.bind(this), this.onDragEnd.bind(this), 'move', null); |
| element.addEventListener('wheel', this.onMouseWheel.bind(this), false); |
| this.minScale = 0; |
| this.maxScale = Infinity; |
| |
| this.controlPanelToolbar = new UI.Toolbar.Toolbar('transform-control-panel'); |
| |
| this.modeButtons = {}; |
| if (!disableRotate) { |
| const panModeButton = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.panModeX), 'largeicon-pan'); |
| panModeButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.setMode.bind(this, Modes.Pan)); |
| this.modeButtons[Modes.Pan] = panModeButton; |
| this.controlPanelToolbar.appendToolbarItem(panModeButton); |
| const rotateModeButton = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.rotateModeV), 'largeicon-rotate'); |
| rotateModeButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.setMode.bind(this, Modes.Rotate)); |
| this.modeButtons[Modes.Rotate] = rotateModeButton; |
| this.controlPanelToolbar.appendToolbarItem(rotateModeButton); |
| } |
| this.setMode(Modes.Pan); |
| |
| const resetButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.resetTransform), 'largeicon-center'); |
| resetButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.resetAndNotify.bind(this, undefined)); |
| this.controlPanelToolbar.appendToolbarItem(resetButton); |
| |
| this.reset(); |
| } |
| |
| toolbar(): UI.Toolbar.Toolbar { |
| return this.controlPanelToolbar; |
| } |
| |
| private registerShortcuts(): void { |
| const zoomFactor = 1.1; |
| UI.ShortcutRegistry.ShortcutRegistry.instance().addShortcutListener(this.element, { |
| 'layers.reset-view': async(): Promise<true> => { |
| this.resetAndNotify(); |
| return true; |
| }, |
| 'layers.pan-mode': async(): Promise<true> => { |
| this.setMode(Modes.Pan); |
| return true; |
| }, |
| 'layers.rotate-mode': async(): Promise<true> => { |
| this.setMode(Modes.Rotate); |
| return true; |
| }, |
| 'layers.zoom-in': this.onKeyboardZoom.bind(this, zoomFactor), |
| 'layers.zoom-out': this.onKeyboardZoom.bind(this, 1 / zoomFactor), |
| 'layers.up': this.onKeyboardPanOrRotate.bind(this, 0, -1), |
| 'layers.down': this.onKeyboardPanOrRotate.bind(this, 0, 1), |
| 'layers.left': this.onKeyboardPanOrRotate.bind(this, -1, 0), |
| 'layers.right': this.onKeyboardPanOrRotate.bind(this, 1, 0), |
| }); |
| } |
| |
| private postChangeEvent(): void { |
| this.dispatchEventToListeners(Events.TransformChanged); |
| } |
| |
| private reset(): void { |
| this.scaleInternal = 1; |
| this.offsetXInternal = 0; |
| this.offsetYInternal = 0; |
| this.rotateXInternal = 0; |
| this.rotateYInternal = 0; |
| } |
| |
| private setMode(mode: Modes): void { |
| if (this.mode === mode) { |
| return; |
| } |
| this.mode = mode; |
| this.updateModeButtons(); |
| } |
| |
| private updateModeButtons(): void { |
| for (const mode in this.modeButtons) { |
| this.modeButtons[mode].setToggled(mode === this.mode); |
| } |
| } |
| |
| resetAndNotify(event?: Event): void { |
| this.reset(); |
| this.postChangeEvent(); |
| if (event) { |
| event.preventDefault(); |
| } |
| this.element.focus(); |
| } |
| |
| setScaleConstraints(minScale: number, maxScale: number): void { |
| this.minScale = minScale; |
| this.maxScale = maxScale; |
| this.scaleInternal = Platform.NumberUtilities.clamp(this.scaleInternal, minScale, maxScale); |
| } |
| |
| clampOffsets(minX: number, maxX: number, minY: number, maxY: number): void { |
| this.offsetXInternal = Platform.NumberUtilities.clamp(this.offsetXInternal, minX, maxX); |
| this.offsetYInternal = Platform.NumberUtilities.clamp(this.offsetYInternal, minY, maxY); |
| } |
| |
| scale(): number { |
| return this.scaleInternal; |
| } |
| |
| offsetX(): number { |
| return this.offsetXInternal; |
| } |
| |
| offsetY(): number { |
| return this.offsetYInternal; |
| } |
| |
| rotateX(): number { |
| return this.rotateXInternal; |
| } |
| |
| rotateY(): number { |
| return this.rotateYInternal; |
| } |
| |
| private onScale(scaleFactor: number, x: number, y: number): void { |
| scaleFactor = Platform.NumberUtilities.clamp(this.scaleInternal * scaleFactor, this.minScale, this.maxScale) / |
| this.scaleInternal; |
| this.scaleInternal *= scaleFactor; |
| this.offsetXInternal -= (x - this.offsetXInternal) * (scaleFactor - 1); |
| this.offsetYInternal -= (y - this.offsetYInternal) * (scaleFactor - 1); |
| this.postChangeEvent(); |
| } |
| |
| private onPan(offsetX: number, offsetY: number): void { |
| this.offsetXInternal += offsetX; |
| this.offsetYInternal += offsetY; |
| this.postChangeEvent(); |
| } |
| |
| private onRotate(rotateX: number, rotateY: number): void { |
| this.rotateXInternal = rotateX; |
| this.rotateYInternal = rotateY; |
| this.postChangeEvent(); |
| } |
| |
| private async onKeyboardZoom(zoomFactor: number): Promise<boolean> { |
| this.onScale(zoomFactor, this.element.clientWidth / 2, this.element.clientHeight / 2); |
| return true; |
| } |
| |
| private async onKeyboardPanOrRotate(xMultiplier: number, yMultiplier: number): Promise<boolean> { |
| const panStepInPixels = 6; |
| const rotateStepInDegrees = 5; |
| |
| if (this.mode === Modes.Rotate) { |
| // Sic! onRotate treats X and Y as "rotate around X" and "rotate around Y", so swap X/Y multiplers. |
| this.onRotate( |
| this.rotateXInternal + yMultiplier * rotateStepInDegrees, |
| this.rotateYInternal + xMultiplier * rotateStepInDegrees); |
| } else { |
| this.onPan(xMultiplier * panStepInPixels, yMultiplier * panStepInPixels); |
| } |
| return true; |
| } |
| |
| private onMouseWheel(event: Event): void { |
| /** @const */ |
| const zoomFactor = 1.1; |
| /** @const */ |
| const wheelZoomSpeed = 1 / 53; |
| const mouseEvent = event as WheelEvent; |
| const scaleFactor = Math.pow(zoomFactor, -mouseEvent.deltaY * wheelZoomSpeed); |
| this.onScale( |
| scaleFactor, mouseEvent.clientX - this.element.totalOffsetLeft(), |
| mouseEvent.clientY - this.element.totalOffsetTop()); |
| } |
| |
| private onDrag(event: Event): void { |
| const {clientX, clientY} = event as MouseEvent; |
| if (this.mode === Modes.Rotate) { |
| this.onRotate( |
| this.oldRotateX + (this.originY - clientY) / this.element.clientHeight * 180, |
| this.oldRotateY - (this.originX - clientX) / this.element.clientWidth * 180); |
| } else { |
| this.onPan(clientX - this.originX, clientY - this.originY); |
| this.originX = clientX; |
| this.originY = clientY; |
| } |
| } |
| |
| private onDragStart(event: MouseEvent): boolean { |
| this.element.focus(); |
| this.originX = event.clientX; |
| this.originY = event.clientY; |
| this.oldRotateX = this.rotateXInternal; |
| this.oldRotateY = this.rotateYInternal; |
| return true; |
| } |
| |
| private onDragEnd(): void { |
| this.originX = 0; |
| this.originY = 0; |
| this.oldRotateX = 0; |
| this.oldRotateY = 0; |
| } |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum Events { |
| TransformChanged = 'TransformChanged', |
| } |
| |
| export type EventTypes = { |
| [Events.TransformChanged]: void, |
| }; |
| |
| export const enum Modes { |
| Pan = 'Pan', |
| Rotate = 'Rotate', |
| } |