"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Tools
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.WheelEventProcessor = exports.ToolAdmin = exports.CurrentInputState = exports.SuspendedToolState = exports.ToolState = exports.ToolSettingsState = exports.ManipulatorToolEvent = exports.StartOrResume = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const AccuSnap_1 = require("../AccuSnap");
const FrontendLoggerCategory_1 = require("../common/FrontendLoggerCategory");
const IModelApp_1 = require("../IModelApp");
const LinePlaneIntersect_1 = require("../LinePlaneIntersect");
const NotificationManager_1 = require("../NotificationManager");
const Sprites_1 = require("../Sprites");
const ViewContext_1 = require("../ViewContext");
const Viewport_1 = require("../Viewport");
const ViewStatus_1 = require("../ViewStatus");
const PrimitiveTool_1 = require("./PrimitiveTool");
const Tool_1 = require("./Tool");
const ToolSettings_1 = require("./ToolSettings");
/**
 * @public
 * @extensions
 */
var StartOrResume;
(function (StartOrResume) {
    StartOrResume[StartOrResume["Start"] = 1] = "Start";
    StartOrResume[StartOrResume["Resume"] = 2] = "Resume";
})(StartOrResume || (exports.StartOrResume = StartOrResume = {}));
/**
 * @public
 * @extensions
 */
var ManipulatorToolEvent;
(function (ManipulatorToolEvent) {
    ManipulatorToolEvent[ManipulatorToolEvent["Start"] = 1] = "Start";
    ManipulatorToolEvent[ManipulatorToolEvent["Stop"] = 2] = "Stop";
    ManipulatorToolEvent[ManipulatorToolEvent["Suspend"] = 3] = "Suspend";
    ManipulatorToolEvent[ManipulatorToolEvent["Unsuspend"] = 4] = "Unsuspend";
})(ManipulatorToolEvent || (exports.ManipulatorToolEvent = ManipulatorToolEvent = {}));
/** Maintains the state of tool settings properties for the current session.
 * @see [[ToolAdmin.toolSettingsState]] to access the state for the current session.
 * @public
 */
class ToolSettingsState {
    /** Retrieve saved tool settings DialogItemValue by property name. */
    getInitialToolSettingValue(toolId, propertyName) {
        const key = `${toolId}:${propertyName}`;
        const savedValue = window.sessionStorage.getItem(key);
        if (null !== savedValue) {
            return JSON.parse(savedValue);
        }
        return undefined;
    }
    /** Retrieve an array of DialogPropertyItem with the values latest values that were used in the session. */
    getInitialToolSettingValues(toolId, propertyNames) {
        const initializedProperties = [];
        let propertyValue;
        propertyNames.forEach((propertyName) => {
            propertyValue = this.getInitialToolSettingValue(toolId, propertyName);
            if (propertyValue)
                initializedProperties.push({ value: propertyValue, propertyName });
        });
        return initializedProperties.length ? initializedProperties : undefined;
    }
    /** Save single tool settings value to session storage. */
    saveToolSettingProperty(toolId, item) {
        const key = `${toolId}:${item.propertyName}`;
        const objectAsString = JSON.stringify(item.value);
        window.sessionStorage.setItem(key, objectAsString);
    }
    /** Save an array of tool settings values to session storage */
    saveToolSettingProperties(toolId, tsProps) {
        tsProps.forEach((item) => this.saveToolSettingProperty(toolId, item));
    }
}
exports.ToolSettingsState = ToolSettingsState;
/** @internal */
class ToolState {
    coordLockOvr = Tool_1.CoordinateLockOverrides.None;
    locateCircleOn = false;
    setFrom(other) {
        this.coordLockOvr = other.coordLockOvr;
        this.locateCircleOn = other.locateCircleOn;
    }
    clone() {
        const val = new ToolState();
        val.setFrom(this);
        return val;
    }
}
exports.ToolState = ToolState;
/** @internal */
class SuspendedToolState {
    _toolState;
    _accuSnapState;
    _locateOptions;
    _viewCursor;
    _inDynamics;
    _shuttingDown = false;
    constructor() {
        const { toolAdmin, viewManager, accuSnap, locateManager } = IModelApp_1.IModelApp;
        toolAdmin.setIncompatibleViewportCursor(true); // Don't save this
        this._toolState = toolAdmin.toolState.clone();
        this._accuSnapState = accuSnap.toolState.clone();
        this._locateOptions = locateManager.options.clone();
        this._viewCursor = viewManager.cursor;
        this._inDynamics = viewManager.inDynamicsMode;
        if (this._inDynamics)
            viewManager.endDynamicsMode();
    }
    stop() {
        if (this._shuttingDown)
            return;
        const { toolAdmin, viewManager, accuSnap, locateManager } = IModelApp_1.IModelApp;
        toolAdmin.setIncompatibleViewportCursor(true); // Don't restore this
        toolAdmin.toolState.setFrom(this._toolState);
        accuSnap.toolState.setFrom(this._accuSnapState);
        locateManager.options.setFrom(this._locateOptions);
        viewManager.setViewCursor(this._viewCursor);
        if (this._inDynamics)
            viewManager.beginDynamicsMode();
        else
            viewManager.endDynamicsMode();
    }
}
exports.SuspendedToolState = SuspendedToolState;
/** @internal */
class CurrentInputState {
    _rawPoint = new core_geometry_1.Point3d();
    _point = new core_geometry_1.Point3d();
    _viewPoint = new core_geometry_1.Point3d();
    qualifiers = Tool_1.BeModifierKeys.None;
    viewport;
    button = [new Tool_1.BeButtonState(), new Tool_1.BeButtonState(), new Tool_1.BeButtonState()];
    lastButton = Tool_1.BeButton.Data;
    inputSource = Tool_1.InputSource.Unknown;
    lastMotion = new core_geometry_1.Point2d();
    lastMotionEvent;
    lastWheelEvent;
    lastTouchStart;
    touchTapTimer;
    touchTapCount;
    get rawPoint() { return this._rawPoint; }
    set rawPoint(pt) { this._rawPoint.setFrom(pt); }
    get point() { return this._point; }
    set point(pt) { this._point.setFrom(pt); }
    get viewPoint() { return this._viewPoint; }
    set viewPoint(pt) { this._viewPoint.setFrom(pt); }
    get isShiftDown() { return 0 !== (this.qualifiers & Tool_1.BeModifierKeys.Shift); }
    get isControlDown() { return 0 !== (this.qualifiers & Tool_1.BeModifierKeys.Control); }
    get isAltDown() { return 0 !== (this.qualifiers & Tool_1.BeModifierKeys.Alt); }
    isDragging(button) { return this.button[button].isDragging; }
    onStartDrag(button) { this.button[button].isDragging = true; }
    onInstallTool() {
        this.clearKeyQualifiers();
        this.lastWheelEvent = this.lastMotionEvent = undefined;
        this.lastTouchStart = this.touchTapTimer = this.touchTapCount = undefined;
    }
    clearKeyQualifiers() { this.qualifiers = Tool_1.BeModifierKeys.None; }
    clearViewport(vp) {
        if (vp === this.viewport)
            this.viewport = undefined;
    }
    isAnyDragging() { return this.button.some((button) => button.isDragging); }
    setKeyQualifier(qual, down) {
        this.qualifiers = down ? (this.qualifiers | qual) : (this.qualifiers & (~qual));
    }
    setKeyQualifiers(ev) {
        this.setKeyQualifier(Tool_1.BeModifierKeys.Shift, ev.shiftKey);
        this.setKeyQualifier(Tool_1.BeModifierKeys.Control, ev.ctrlKey);
        this.setKeyQualifier(Tool_1.BeModifierKeys.Alt, ev.altKey);
    }
    onMotion(pt2d) {
        this.lastMotion.x = pt2d.x;
        this.lastMotion.y = pt2d.y;
    }
    changeButtonToDownPoint(ev) {
        ev.point = this.button[ev.button].downUorPt;
        ev.rawPoint = this.button[ev.button].downRawPt;
        if (ev.viewport)
            ev.viewPoint = ev.viewport.worldToView(ev.rawPoint);
    }
    updateDownPoint(ev) { this.button[ev.button].downUorPt = ev.point; }
    onButtonDown(button) {
        let isDoubleClick = false;
        const now = Date.now();
        const vp = this.viewport;
        if (undefined !== vp) {
            const viewPt = vp.worldToView(this.button[button].downRawPt);
            const center = vp.npcToView(core_common_1.NpcCenter);
            viewPt.z = center.z;
            isDoubleClick = ((now - this.button[button].downTime) < ToolSettings_1.ToolSettings.doubleClickTimeout.milliseconds) && (viewPt.distance(this.viewPoint) < vp.pixelsFromInches(ToolSettings_1.ToolSettings.doubleClickToleranceInches));
        }
        this.button[button].init(this.point, this.rawPoint, now, true, isDoubleClick, false, this.inputSource);
        this.lastButton = button;
    }
    onButtonUp(button) {
        this.button[button].isDown = false;
        this.button[button].isDragging = false;
        this.lastButton = button;
    }
    toEvent(ev, useSnap) {
        let coordsFrom = Tool_1.CoordSource.User;
        const point = this.point.clone();
        let viewport = this.viewport;
        if (useSnap) {
            const snap = AccuSnap_1.TentativeOrAccuSnap.getCurrentSnap(false);
            if (snap) {
                coordsFrom = snap.isHot ? Tool_1.CoordSource.ElemSnap : Tool_1.CoordSource.User;
                point.setFrom(snap.isPointAdjusted ? snap.adjustedPoint : snap.getPoint()); // NOTE: adjustedPoint can be set by adjustSnapPoint even when not hot...
                viewport = snap.viewport;
            }
            else if (IModelApp_1.IModelApp.tentativePoint.isActive) {
                coordsFrom = Tool_1.CoordSource.TentativePoint;
                point.setFrom(IModelApp_1.IModelApp.tentativePoint.getPoint());
                viewport = IModelApp_1.IModelApp.tentativePoint.viewport;
            }
        }
        const buttonState = this.button[this.lastButton];
        ev.init({
            point, rawPoint: this.rawPoint, viewPoint: this.viewPoint, viewport, coordsFrom,
            keyModifiers: this.qualifiers, button: this.lastButton, isDown: buttonState.isDown,
            isDoubleClick: buttonState.isDoubleClick, isDragging: buttonState.isDragging,
            inputSource: this.inputSource,
        });
    }
    adjustLastDataPoint(ev) {
        const state = this.button[Tool_1.BeButton.Data];
        state.downUorPt = ev.point;
        state.downRawPt = ev.point;
        this.viewport = ev.viewport;
    }
    toEventFromLastDataPoint(ev) {
        const state = this.button[Tool_1.BeButton.Data];
        const point = state.downUorPt;
        const rawPoint = state.downRawPt;
        const viewPoint = this.viewport ? this.viewport.worldToView(rawPoint) : core_geometry_1.Point3d.create(); // BeButtonEvent is invalid when viewport is undefined
        ev.init({
            point, rawPoint, viewPoint, viewport: this.viewport, coordsFrom: Tool_1.CoordSource.User,
            keyModifiers: this.qualifiers, button: Tool_1.BeButton.Data, isDown: state.isDown,
            isDoubleClick: state.isDoubleClick, isDragging: state.isDragging, inputSource: state.inputSource,
        });
    }
    fromPoint(vp, pt, source) {
        this.viewport = vp;
        this._viewPoint.x = pt.x;
        this._viewPoint.y = pt.y;
        this._viewPoint.z = vp.npcToView(core_common_1.NpcCenter).z;
        vp.viewToWorld(this._viewPoint, this._rawPoint);
        this.point = this._rawPoint;
        this.inputSource = source;
    }
    fromButton(vp, pt, source, applyLocks) {
        this.fromPoint(vp, pt, source);
        // NOTE: Using the hit point on the element is preferable to ignoring a snap that is not "hot" completely
        if (AccuSnap_1.TentativeOrAccuSnap.getCurrentSnap(false)) {
            if (applyLocks)
                IModelApp_1.IModelApp.toolAdmin.adjustSnapPoint();
            return;
        }
        IModelApp_1.IModelApp.toolAdmin.adjustPoint(this._point, vp, true, applyLocks);
    }
    isStartDrag(button) {
        // First make sure we aren't already dragging any button
        if (this.isAnyDragging())
            return false;
        const state = this.button[button];
        if (!state.isDown)
            return false;
        if ((Date.now() - state.downTime) <= ToolSettings_1.ToolSettings.startDragDelay.milliseconds)
            return false;
        const vp = this.viewport;
        if (undefined === vp)
            return false;
        const viewPt = vp.worldToView(state.downRawPt);
        const deltaX = Math.abs(this._viewPoint.x - viewPt.x);
        const deltaY = Math.abs(this._viewPoint.y - viewPt.y);
        return ((deltaX + deltaY) > vp.pixelsFromInches(ToolSettings_1.ToolSettings.startDragDistanceInches));
    }
}
exports.CurrentInputState = CurrentInputState;
/** Controls the operation of [[Tool]]s, administering the current [[ViewTool]], [[PrimitiveTool]], and [[IdleTool]] and forwarding events to the appropriate tool.
 * @see [[IModelApp.toolAdmin]] to access the session's `ToolAdmin`.
 * @public
 * @extensions
 */
class ToolAdmin {
    markupView;
    /** @internal */
    currentInputState = new CurrentInputState();
    /** @internal */
    toolState = new ToolState();
    /** Maintains the state of tool settings properties for the current session. */
    toolSettingsState = new ToolSettingsState();
    _canvasDecoration;
    _suspendedByViewTool;
    _suspendedByInputCollector;
    _viewTool;
    _primitiveTool;
    _idleTool;
    _inputCollector;
    _saveCursor;
    _saveLocateCircle = false;
    _defaultToolId = "Select";
    _defaultToolArgs;
    _lastHandledMotionTime;
    _mouseMoveOverTimeout;
    _editCommandHandler;
    /** The name of the [[PrimitiveTool]] to use as the default tool.
     * Defaults to "Select", referring to [[SelectionTool]].
     * @note An empty string signifies no default tool allowing more events to be handled by [[idleTool]].
     * @see [[startDefaultTool]] to activate the default tool.
     * @see [[defaultToolArgs]] to supply arguments when starting the tool.
     */
    get defaultToolId() {
        return this._defaultToolId;
    }
    set defaultToolId(toolId) {
        this._defaultToolId = toolId;
    }
    /** The arguments supplied to the default [[Tool]]'s [[Tool.run]] method from [[startDefaultTool]].
     * @see [[defaultToolId]] to configure the default tool.
     */
    get defaultToolArgs() {
        return this._defaultToolArgs;
    }
    set defaultToolArgs(args) {
        this._defaultToolArgs = args;
    }
    /** Apply operations such as transform, copy or delete to all members of an assembly. */
    assemblyLock = false;
    /** If Grid Lock is on, project data points to grid. */
    gridLock = false;
    /** If ACS Snap Lock is on, project snap points to the ACS plane. */
    acsPlaneSnapLock = false;
    /** If ACS Plane Lock is on, standard view rotations are relative to the ACS instead of global. */
    acsContextLock = false;
    /** Options for how uncaught exceptions should be handled by [[ToolAdmin.exceptionHandler]]. */
    static exceptionOptions = {
        /** Log exception to Logger. */
        log: true,
        /** Show an alert box explaining that a problem happened. */
        alertBox: true,
        /** Include the "gory details" (e.g. stack trace) in the alert box. */
        details: true,
        /** break into debugger (only works if debugger is already opened) */
        launchDebugger: true,
    };
    /** A function that catches otherwise-uncaught exceptions occurring inside ToolAdmin.eventLoop.
     * To customize the behavior of this function, modify [[ToolAdmin.exceptionOptions]].
     * To replace it within your own handler, simply assign to it, e.g.:
     * ```ts
     * ToolAdmin.exceptionHandler = (exception: any): Promise<any> => {
     *  ... your implementation here
     * }
     * ```
     */
    static async exceptionHandler(exception) {
        const opts = ToolAdmin.exceptionOptions;
        const msg = undefined !== exception.stack ? exception.stack : exception.toString();
        if (opts.log)
            core_bentley_1.Logger.logError(`${FrontendLoggerCategory_1.FrontendLoggerCategory.Package}.unhandledException`, msg);
        if (opts.launchDebugger) // this does nothing if the debugger window is not already opened
            debugger; // eslint-disable-line no-debugger
        if (!opts.alertBox)
            return;
        let out = `<h2>${IModelApp_1.IModelApp.localization.getLocalizedString("iModelJs:Errors.ReloadPage")}</h2>`;
        if (opts.details) {
            out += `<h3>${IModelApp_1.IModelApp.localization.getLocalizedString("iModelJs:Errors.Details")}</h3><h4>`;
            msg.split("\n").forEach((line) => out += `${line}<br>`);
            out += "</h4>";
        }
        const div = document.createElement("div");
        div.innerHTML = out;
        return IModelApp_1.IModelApp.notifications.openMessageBox(NotificationManager_1.MessageBoxType.MediumAlert, div, NotificationManager_1.MessageBoxIconType.Critical);
    }
    static _removals = [];
    /** The registered handler method that will update the UI with any property value changes.
     *  @internal
     */
    _toolSettingsChangeHandler = undefined;
    /** Returns the handler registered by the UI layer that allows it to display property changes made by the active Tool. */
    get toolSettingsChangeHandler() { return this._toolSettingsChangeHandler; }
    set toolSettingsChangeHandler(handler) {
        this._toolSettingsChangeHandler = handler;
    }
    /** The registered handler method that will inform the UI to reload tool setting with properties from active tool.
   *  @internal
   */
    _reloadToolSettingsHandler = undefined;
    /** Returns the handler registered by the UI layer that allows it to display property changes made by the active Tool. */
    get reloadToolSettingsHandler() { return this._reloadToolSettingsHandler; }
    set reloadToolSettingsHandler(handler) {
        this._reloadToolSettingsHandler = handler;
    }
    /** The registered handler method that will trigger UI Sync processing.
     *  @internal
     */
    _toolSyncUiEventDispatcher = undefined;
    /** Returns the handler registered by the UI layer that will trigger UiSyncEvent processing that informs UI component to refresh their state. */
    get toolSyncUiEventDispatcher() { return this._toolSyncUiEventDispatcher; }
    set toolSyncUiEventDispatcher(handler) {
        this._toolSyncUiEventDispatcher = handler;
    }
    /** Handler for keyboard events. */
    static _keyEventHandler = (ev) => {
        if (!ev.repeat) // we don't want repeated keyboard events. If we keep them they interfere with replacing mouse motion events, since they come as a stream.
            ToolAdmin.addEvent(ev);
    };
    /** Handler for modifier key transitions captured before focused UI elements can stop propagation. */
    static _keyEventCaptureHandler = async (event) => {
        const ev = event;
        if (!ev.repeat) {
            try {
                await IModelApp_1.IModelApp.toolAdmin.onKeyTransitionCaptured(ev);
            }
            catch (err) {
                await ToolAdmin.exceptionHandler(err);
            }
        }
    };
    /** @internal */
    onInitialized() {
        if (typeof document === "undefined")
            return; // if document isn't defined, we're probably running in a test environment. At any rate, we can't have interactive tools.
        this._idleTool = IModelApp_1.IModelApp.tools.create("Idle");
        ["keydown", "keyup"].forEach((type) => {
            document.addEventListener(type, ToolAdmin._keyEventCaptureHandler, true);
            ToolAdmin._removals.push(() => document.removeEventListener(type, ToolAdmin._keyEventCaptureHandler, true));
            document.addEventListener(type, ToolAdmin._keyEventHandler, false);
            ToolAdmin._removals.push(() => document.removeEventListener(type, ToolAdmin._keyEventHandler, false));
        });
        ToolAdmin._removals.push(() => window.onfocus = null);
    }
    /** @internal */
    onShutDown() {
        this.clearMotionPromises();
        this._idleTool = undefined;
        Sprites_1.IconSprites.emptyAll(); // clear cache of icon sprites
        ToolAdmin._removals.forEach((remove) => remove());
        ToolAdmin._removals.length = 0;
    }
    /** Get the ScreenViewport where the cursor is currently, if any. */
    get cursorView() { return this.currentInputState.viewport; }
    /** Called from ViewManager.dropViewport to prevent tools from continuing to operate on the dropped viewport.
     * @internal
     */
    forgetViewport(vp) {
        // Ignore pending motion promises on fulfillment.
        this.clearMotionPromises();
        // make sure tools don't think the cursor is still in this viewport.
        this.onMouseLeave(vp);
        // Invalidate last motion if for this viewport...
        if (this.currentInputState.lastMotionEvent?.viewport === vp)
            this.currentInputState.lastMotionEvent = undefined;
        // Remove any events associated with this viewport.
        ToolAdmin._toolEvents = ToolAdmin._toolEvents.filter((ev) => ev.vp !== vp);
    }
    getMousePosition(event) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return event.vp.mousePosFromEvent(event.ev);
    }
    getMouseMovement(event) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return event.vp.mouseMovementFromEvent(event.ev);
    }
    getMouseButton(button) {
        switch (button) {
            case 1 /* MouseButton.Middle */: return Tool_1.BeButton.Middle;
            case 2 /* MouseButton.Right */: return Tool_1.BeButton.Reset;
            default: return Tool_1.BeButton.Data;
        }
    }
    async onMouseButton(event, isDown) {
        const ev = event.ev;
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const vp = event.vp;
        const pos = this.getMousePosition(event);
        const button = this.getMouseButton(ev.button);
        this.currentInputState.setKeyQualifiers(ev);
        return isDown ? this.onButtonDown(vp, pos, button, Tool_1.InputSource.Mouse) : this.onButtonUp(vp, pos, button, Tool_1.InputSource.Mouse);
    }
    async onWheel(event) {
        const ev = event.ev;
        const vp = event.vp;
        if (undefined === vp || this.filterViewport(vp))
            return Tool_1.EventHandled.Yes;
        const current = this.currentInputState;
        current.setKeyQualifiers(ev);
        if (ev.deltaY === 0)
            return Tool_1.EventHandled.No;
        let delta;
        switch (ev.deltaMode) {
            case ev.DOM_DELTA_LINE:
                delta = -ev.deltaY * ToolSettings_1.ToolSettings.wheelLineFactor; // 40
                break;
            case ev.DOM_DELTA_PAGE:
                delta = -ev.deltaY * ToolSettings_1.ToolSettings.wheelPageFactor; // 120;
                break;
            default: // DOM_DELTA_PIXEL:
                delta = -ev.deltaY;
                break;
        }
        const pt2d = this.getMousePosition(event);
        vp.setAnimator();
        current.fromButton(vp, pt2d, Tool_1.InputSource.Mouse, true);
        const wheelEvent = new Tool_1.BeWheelEvent();
        wheelEvent.wheelDelta = delta;
        current.toEvent(wheelEvent, true);
        const overlayHit = this.pickCanvasDecoration(wheelEvent);
        if (undefined !== overlayHit && undefined !== overlayHit.onWheel && overlayHit.onWheel(wheelEvent))
            return Tool_1.EventHandled.Yes;
        const tool = this.activeTool;
        if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onMouseWheel(wheelEvent) && vp !== this.markupView)
            return this.idleTool.onMouseWheel(wheelEvent);
        return Tool_1.EventHandled.Yes;
    }
    async sendTapEvent(touchEv) {
        const vp = touchEv.viewport;
        if (undefined !== vp)
            vp.setAnimator();
        const overlayHit = this.pickCanvasDecoration(touchEv);
        if (undefined !== overlayHit && undefined !== overlayHit.onMouseButton && overlayHit.onMouseButton(touchEv))
            return Tool_1.EventHandled.Yes;
        if (await IModelApp_1.IModelApp.accuSnap.onTouchTap(touchEv))
            return Tool_1.EventHandled.Yes;
        const tool = this.activeTool;
        if (undefined !== tool && Tool_1.EventHandled.Yes === await tool.onTouchTap(touchEv))
            return Tool_1.EventHandled.Yes;
        return this.idleTool.onTouchTap(touchEv);
    }
    async doubleTapTimeout() {
        const current = this.currentInputState;
        if (undefined === current.touchTapTimer)
            return;
        const touchEv = current.lastTouchStart;
        const numTouches = (undefined !== current.lastTouchStart ? current.lastTouchStart.touchCount : 0);
        const numTaps = (undefined !== current.touchTapCount ? current.touchTapCount : 0);
        current.touchTapTimer = current.touchTapCount = current.lastTouchStart = undefined;
        if (undefined === touchEv || 0 > numTouches || 0 > numTaps)
            return;
        touchEv.tapCount = numTaps;
        await this.sendTapEvent(touchEv);
    }
    async onTouch(event) {
        const touchEvent = event.ev;
        const vp = event.vp;
        if (undefined === vp || this.filterViewport(vp))
            return;
        const ev = new Tool_1.BeTouchEvent({ touchEvent });
        const current = this.currentInputState;
        const pos = Tool_1.BeTouchEvent.getTouchListCentroid(0 !== touchEvent.targetTouches.length ? touchEvent.targetTouches : touchEvent.changedTouches, vp);
        switch (touchEvent.type) {
            case "touchstart":
                if (touchEvent.changedTouches.length === touchEvent.targetTouches.length)
                    vp.setAnimator(); // Clear viewport animator on start of new touch input (first contact point added)...
                current.setKeyQualifiers(touchEvent);
                break;
            case "touchend":
                current.setKeyQualifiers(touchEvent);
                break;
        }
        current.fromButton(vp, undefined !== pos ? pos : core_geometry_1.Point2d.createZero(), Tool_1.InputSource.Touch, true);
        current.toEvent(ev, false);
        const tool = this.activeTool;
        switch (touchEvent.type) {
            case "touchstart": {
                current.lastTouchStart = ev;
                IModelApp_1.IModelApp.accuSnap.onTouchStart(ev);
                if (undefined !== tool)
                    await tool.onTouchStart(ev);
                return;
            }
            case "touchend": {
                IModelApp_1.IModelApp.accuSnap.onTouchEnd(ev);
                if (undefined !== tool) {
                    await tool.onTouchEnd(ev);
                    if (0 === ev.touchCount)
                        await tool.onTouchComplete(ev);
                }
                if (undefined === current.lastTouchStart)
                    return;
                if (ev.touchEvent.timeStamp - current.lastTouchStart.touchEvent.timeStamp > (2.0 * ToolSettings_1.ToolSettings.doubleTapTimeout.milliseconds))
                    return; // Too much time has passed from touchstart to be considered a tap...
                // eslint-disable-next-line @typescript-eslint/prefer-for-of
                for (let i = 0; i < ev.touchEvent.changedTouches.length; i++) {
                    const currTouch = ev.touchEvent.changedTouches[i];
                    const startTouch = Tool_1.BeTouchEvent.findTouchById(current.lastTouchStart.touchEvent.targetTouches, currTouch.identifier);
                    if (undefined !== startTouch) {
                        const currPt = Tool_1.BeTouchEvent.getTouchPosition(currTouch, vp);
                        const startPt = Tool_1.BeTouchEvent.getTouchPosition(startTouch, vp);
                        if (currPt.distance(startPt) < vp.pixelsFromInches(ToolSettings_1.ToolSettings.touchMoveDistanceInches))
                            continue; // Hasn't moved appreciably....
                    }
                    current.lastTouchStart = undefined; // Not a tap...
                    return;
                }
                if (0 !== ev.touchCount || undefined === current.lastTouchStart)
                    return;
                // All fingers off, defer processing tap until we've waited long enough to detect double tap...
                if (undefined === current.touchTapTimer) {
                    current.touchTapTimer = Date.now();
                    current.touchTapCount = 1;
                    // NOTE: We cannot await the executeAfter call below, because that prevents any other
                    // taps from being processed, which makes it impossible for double tap to happen.
                    // eslint-disable-next-line @typescript-eslint/unbound-method
                    void ToolSettings_1.ToolSettings.doubleTapTimeout.executeAfter(this.doubleTapTimeout, this);
                }
                else if (undefined !== current.touchTapCount) {
                    current.touchTapCount++;
                }
                return;
            }
            case "touchcancel": {
                current.lastTouchStart = undefined;
                IModelApp_1.IModelApp.accuSnap.onTouchCancel(ev);
                if (undefined !== tool)
                    await tool.onTouchCancel(ev);
                return;
            }
            case "touchmove": {
                if (!IModelApp_1.IModelApp.accuSnap.onTouchMove(ev) && undefined !== tool)
                    await tool.onTouchMove(ev);
                if (undefined === current.lastTouchStart)
                    return;
                if (ev.touchEvent.timeStamp - current.lastTouchStart.touchEvent.timeStamp < ToolSettings_1.ToolSettings.touchMoveDelay.milliseconds)
                    return;
                // eslint-disable-next-line @typescript-eslint/prefer-for-of
                for (let i = 0; i < ev.touchEvent.changedTouches.length; ++i) {
                    const currTouch = ev.touchEvent.changedTouches[i];
                    const startTouch = Tool_1.BeTouchEvent.findTouchById(current.lastTouchStart.touchEvent.targetTouches, currTouch.identifier);
                    if (undefined === startTouch)
                        continue;
                    const currPt = Tool_1.BeTouchEvent.getTouchPosition(currTouch, vp);
                    const startPt = Tool_1.BeTouchEvent.getTouchPosition(startTouch, vp);
                    if (currPt.distance(startPt) < vp.pixelsFromInches(ToolSettings_1.ToolSettings.touchMoveDistanceInches))
                        continue; // Hasn't moved appreciably....
                    const touchStart = current.lastTouchStart;
                    current.lastTouchStart = undefined;
                    if (IModelApp_1.IModelApp.accuSnap.onTouchMoveStart(ev, touchStart))
                        return;
                    if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onTouchMoveStart(ev, touchStart))
                        await this.idleTool.onTouchMoveStart(ev, touchStart);
                    return;
                }
                return;
            }
        }
    }
    /** A first-in-first-out queue of ToolEvents. */
    static _toolEvents = [];
    static tryReplace(ev, vp) {
        if (ToolAdmin._toolEvents.length < 1)
            return false;
        const last = ToolAdmin._toolEvents[ToolAdmin._toolEvents.length - 1];
        const lastType = last.ev.type;
        if (lastType !== ev.type || (lastType !== "mousemove" && lastType !== "touchmove"))
            return false; // only mousemove and touchmove can replace previous
        last.ev = ev; // sequential moves are not important. Replace the previous one with this one.
        last.vp = vp;
        return true;
    }
    /** @internal */
    static getNextEvent() {
        if (ToolAdmin._toolEvents.length > 1) // if there is more than one event, we're going to need another animation frame to process it.
            IModelApp_1.IModelApp.requestNextAnimation();
        return ToolAdmin._toolEvents.shift(); // pull first event from the queue
    }
    /** Called from HTML event listeners. Events are processed in the order they're received in ToolAdmin.eventLoop
     * @internal
     */
    static addEvent(ev, vp) {
        // Don't add events to queue if event loop hasn't been started to process them...
        if (!IModelApp_1.IModelApp.isEventLoopStarted)
            return;
        if (!ToolAdmin.tryReplace(ev, vp)) // see if this event replaces the last event in the queue
            this._toolEvents.push({ ev, vp }); // otherwise put it at the end of the queue.
        IModelApp_1.IModelApp.requestNextAnimation(); // wake up event loop, if
    }
    /** Process the next event in the event queue, if any. */
    async processNextEvent() {
        const event = ToolAdmin.getNextEvent(); // pull first event from the queue
        if (undefined === event)
            return; // nothing in queue
        switch (event.ev.type) {
            case "mousedown": return this.onMouseButton(event, true);
            case "mouseup": return this.onMouseButton(event, false);
            case "mousemove": return this.onMouseMove(event);
            case "mouseover": return this.onMouseEnter(event);
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            case "mouseout": return this.onMouseLeave(event.vp);
            case "wheel": return this.onWheel(event);
            case "keydown": return this.onKeyTransition(event, true);
            case "keyup": return this.onKeyTransition(event, false);
            case "touchstart": return this.onTouch(event);
            case "touchend": return this.onTouch(event);
            case "touchcancel": return this.onTouch(event);
            case "touchmove": return this.onTouch(event);
        }
    }
    _processingEvent = false;
    /**
     * Process a single event, plus timer events. Don't start work on new events if the previous one has not finished.
     * @internal
     */
    async processEvent() {
        if (this._processingEvent)
            return; // we're still working on the previous event.
        try {
            this._processingEvent = true; // we can't allow any further event processing until the current event completes.
            await this.processNextEvent();
        }
        catch (exception) {
            await ToolAdmin.exceptionHandler(exception); // we don't attempt to exit here
        }
        finally {
            this._processingEvent = false; // this event is now finished. Allow processing next time through.
        }
    }
    /** The idleTool handles events that are not otherwise processed. */
    get idleTool() {
        (0, core_bentley_1.assert)(undefined !== this._idleTool);
        return this._idleTool;
    }
    set idleTool(idleTool) {
        this._idleTool = idleTool;
    }
    /** Return true to filter (ignore) events to the given viewport */
    filterViewport(vp) {
        if (undefined === vp || vp.isDisposed)
            return true;
        const tool = this.activeTool;
        return (undefined !== tool ? !tool.isCompatibleViewport(vp, false) : false);
    }
    /** @internal */
    async onInstallTool(tool) {
        this.currentInputState.onInstallTool();
        return tool.onInstall();
    }
    /** @internal */
    async onPostInstallTool(tool) { return tool.onPostInstall(); }
    get viewTool() { return this._viewTool; }
    get primitiveTool() { return this._primitiveTool; }
    /** The currently active InteractiveTool. May be ViewTool, InputCollector, PrimitiveTool, undefined - in that priority order. */
    get activeTool() {
        return this._viewTool ? this._viewTool : (this._inputCollector ? this._inputCollector : this._primitiveTool); // NOTE: Viewing tools suspend input collectors as well as primitives
    }
    /** The current tool. May be ViewTool, InputCollector, PrimitiveTool, or IdleTool - in that priority order. */
    get currentTool() { return this.activeTool ? this.activeTool : this.idleTool; }
    /** Allow applications to inhibit specific tooltips, such as for maps. */
    wantToolTip(_hit) { return true; }
    /** Ask the current tool to provide tooltip contents for the supplied HitDetail. */
    async getToolTip(hit) { return this.currentTool.getToolTip(hit); }
    /**
     * Event raised whenever the active tool changes. This includes PrimitiveTool, ViewTool, and InputCollector.
     * @param newTool The newly activated tool
     */
    activeToolChanged = new core_bentley_1.BeEvent();
    /**
     * Event raised by tools that support edit manipulators like the SelectTool.
     * @param tool The current tool
     */
    manipulatorToolEvent = new core_bentley_1.BeEvent();
    async onMouseEnter(event) {
        const vp = event.vp;
        const current = this.currentInputState;
        current.viewport = vp;
        // Detect if drag was active and button was released outside the view...
        const tool = this.activeTool;
        if (undefined === tool)
            return;
        const buttonMask = event.ev.buttons;
        let cancelDrag = false;
        current.button.forEach((button, buttonNum) => {
            if (button.isDragging && !(buttonMask & (1 << buttonNum))) {
                button.isDragging = button.isDown = false;
                cancelDrag = true;
            }
        });
        if (cancelDrag)
            await tool.onReinitialize();
    }
    /** @internal */
    onMouseLeave(vp) {
        if (this._mouseMoveOverTimeout !== undefined)
            clearTimeout(this._mouseMoveOverTimeout);
        IModelApp_1.IModelApp.accuSnap.clear();
        this.currentInputState.clearViewport(vp);
        this.setCanvasDecoration(vp);
        vp.invalidateDecorations(); // stop drawing locate circle...
    }
    /** @internal */
    updateDynamics(ev, useLastData, adjustPoint) {
        if (undefined === this.activeTool)
            return;
        if (undefined === ev) {
            ev = new Tool_1.BeButtonEvent();
            if (useLastData)
                this.fillEventFromLastDataButton(ev);
            else
                this.fillEventFromCursorLocation(ev);
            if (adjustPoint && undefined !== ev.viewport) {
                // Use ev.rawPoint for cursor location when not snapped as ev.point gets adjusted in fromButton...
                const snap = AccuSnap_1.TentativeOrAccuSnap.getCurrentSnap(false);
                if (undefined !== snap) {
                    // Account for changes to locks, reset and re-adjust snap point...
                    snap.adjustedPoint.setFrom(snap.getPoint());
                    this.adjustSnapPoint();
                    ev.point.setFrom(snap.isPointAdjusted ? snap.adjustedPoint : snap.getPoint());
                }
                else {
                    ev.point.setFrom(IModelApp_1.IModelApp.tentativePoint.isActive ? IModelApp_1.IModelApp.tentativePoint.getPoint() : ev.rawPoint);
                    this.adjustPoint(ev.point, ev.viewport);
                }
                IModelApp_1.IModelApp.accuDraw.onMotion(ev);
            }
        }
        if (undefined === ev.viewport)
            return;
        // Support tools requesting async information in onMouseMotion for use in decorate or onDynamicFrame...
        const toolPromise = this._toolMotionPromise = this.activeTool.onMouseMotion(ev);
        const tool = this.activeTool;
        const vp = ev.viewport;
        const motion = ev;
        toolPromise.then(() => {
            if (undefined === this._toolMotionPromise)
                return; // Only early return if canceled, result from a previous motion is preferable to showing nothing...
            this.currentInputState.lastMotionEvent = motion; // Save to use for simulation motion...
            // Update decorations when dynamics are inactive...
            if (!IModelApp_1.IModelApp.viewManager.inDynamicsMode) {
                vp.invalidateDecorations();
                return;
            }
            // Update dynamics and decorations only after motion...
            const context = new ViewContext_1.DynamicsContext(vp);
            tool.onDynamicFrame(motion, context);
            context.changeDynamics();
        }).catch((_) => { });
    }
    async sendEndDragEvent(ev) {
        let tool = this.activeTool;
        if (undefined !== tool) {
            if (!tool.isValidLocation(ev, true))
                tool = undefined;
            else if (tool.receivedDownEvent)
                tool.receivedDownEvent = false;
            else
                tool = undefined;
        }
        // Don't send tool end drag event if it didn't get the start drag event
        if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onMouseEndDrag(ev))
            return this.idleTool.onMouseEndDrag(ev);
    }
    setCanvasDecoration(vp, dec, ev) {
        if (dec === this._canvasDecoration)
            return;
        if (this._canvasDecoration && this._canvasDecoration.onMouseLeave)
            this._canvasDecoration.onMouseLeave();
        this._canvasDecoration = dec;
        if (ev && dec && dec.onMouseEnter)
            dec.onMouseEnter(ev);
        vp.canvas.style.cursor = dec ? (dec.decorationCursor ? dec.decorationCursor : "pointer") : IModelApp_1.IModelApp.viewManager.cursor;
        vp.invalidateDecorations();
    }
    pickCanvasDecoration(ev) {
        const vp = ev.viewport;
        if (undefined === vp)
            return undefined;
        const decoration = (undefined === this.viewTool) ? vp.pickCanvasDecoration(ev.viewPoint) : undefined;
        this.setCanvasDecoration(vp, decoration, ev);
        return decoration;
    }
    /** Current request for locate/snap */
    _snapMotionPromise;
    /** Current request for active tool motion event */
    _toolMotionPromise;
    clearMotionPromises() {
        this._snapMotionPromise = this._toolMotionPromise = undefined;
    }
    async forceOnMotionSnap(ev) {
        // Make sure that we fire the motion snap event correctly
        this._lastHandledMotionTime = undefined;
        return this.onMotionSnap(ev);
    }
    async onMotionSnap(ev) {
        try {
            await this.onMotionSnapOrSkip(ev);
            return true;
        }
        catch (error) {
            if (error instanceof core_bentley_1.AbandonedError)
                return false; // expected, not a problem. Just ignore this motion and return.
            throw error; // unknown error
        }
    }
    // Call accuSnap.onMotion
    async onMotionSnapOrSkip(ev) {
        if (this.shouldSkipOnMotionSnap())
            return;
        await IModelApp_1.IModelApp.accuSnap.onMotion(ev);
        this._lastHandledMotionTime = core_bentley_1.BeTimePoint.now();
    }
    // Should the current onMotionSnap event be skipped to avoid unnecessary ReadPixel calls?
    shouldSkipOnMotionSnap() {
        if (this._lastHandledMotionTime === undefined)
            return false;
        const now = core_bentley_1.BeTimePoint.now();
        const msSinceLastCall = now.milliseconds - this._lastHandledMotionTime.milliseconds;
        const delay = 1000 / ToolSettings_1.ToolSettings.maxOnMotionSnapCallPerSecond;
        return msSinceLastCall < delay;
    }
    async onStartDrag(ev, tool) {
        if (undefined !== tool && Tool_1.EventHandled.Yes === await tool.onMouseStartDrag(ev))
            return Tool_1.EventHandled.Yes;
        // Pass start drag event to idle tool if active tool doesn't explicitly handle it
        return this.idleTool.onMouseStartDrag(ev);
    }
    async onMotion(vp, pt2d, inputSource, forceStartDrag = false, movement) {
        const current = this.currentInputState;
        current.onMotion(pt2d);
        if (this.filterViewport(vp)) {
            this.setIncompatibleViewportCursor(false);
            return;
        }
        // Detect when the motion stops by setting a timeout
        if (this._mouseMoveOverTimeout !== undefined)
            clearTimeout(this._mouseMoveOverTimeout); // If a previous timeout was up, it is cancelled: the movement is not over yet
        const ev = new Tool_1.BeButtonEvent();
        current.fromPoint(vp, pt2d, inputSource);
        current.toEvent(ev, false);
        const overlayHit = this.pickCanvasDecoration(ev);
        if (undefined !== overlayHit) {
            if (overlayHit.onMouseMove)
                overlayHit.onMouseMove(ev);
            if (undefined === overlayHit.propagateMouseMove || !overlayHit.propagateMouseMove(ev))
                return; // we're inside a pickable decoration that doesn't want event sent to tool
        }
        this._mouseMoveOverTimeout = setTimeout(async () => {
            await this.onMotionEnd(vp, pt2d, inputSource);
            await processMotion();
        }, 100);
        const processMotion = async () => {
            // Update event to account for AccuSnap adjustments...
            current.fromButton(vp, pt2d, inputSource, true);
            current.toEvent(ev, true);
            ev.movement = movement;
            IModelApp_1.IModelApp.accuDraw.onMotion(ev);
            let tool = this.activeTool;
            const isValidLocation = (undefined !== tool ? tool.isValidLocation(ev, false) : true);
            this.setIncompatibleViewportCursor(isValidLocation);
            if (forceStartDrag || current.isStartDrag(ev.button)) {
                current.onStartDrag(ev.button);
                current.changeButtonToDownPoint(ev);
                ev.isDragging = true;
                if (undefined !== tool) {
                    if (!isValidLocation)
                        tool = undefined;
                    else if (forceStartDrag)
                        tool.receivedDownEvent = true;
                    else if (!tool.receivedDownEvent)
                        tool = undefined;
                }
                await this.onStartDrag(ev, tool);
                return;
            }
            this.updateDynamics(ev);
        };
        const snapPromise = this._snapMotionPromise = this.onMotionSnap(ev);
        /** When forceStartDrag is true, make sure we don't return a fulfilled promise until we've processed the motion so callers can await it.
         * The .then below happens AFTER this method returns its (fulfilled) promise so we can't use that.
         */
        if (forceStartDrag) {
            await snapPromise;
            return processMotion();
        }
        if (this.isLocateCircleOn)
            vp.invalidateDecorations();
        snapPromise.then(async (snapOk) => {
            if (!snapOk || snapPromise !== this._snapMotionPromise)
                return;
            return processMotion();
        }).catch((_) => { });
    }
    // Called when we detect that the motion stopped
    async onMotionEnd(vp, pos, inputSource) {
        const current = this.currentInputState;
        const ev = new Tool_1.BeButtonEvent();
        current.fromPoint(vp, pos, inputSource);
        current.toEvent(ev, false);
        await this.forceOnMotionSnap(ev);
    }
    async onMouseMove(event) {
        const vp = event.vp;
        if (undefined === vp)
            return;
        const pos = this.getMousePosition(event);
        const mov = this.getMouseMovement(event);
        // Sometimes the mouse goes down in a view, but we lose focus while its down so we never receive the up event.
        // That makes it look like the motion is a drag. Fix that by clearing the "isDown" based on the buttons member of the MouseEvent.
        const buttonMask = event.ev.buttons;
        if (!(buttonMask & 1))
            this.currentInputState.button[Tool_1.BeButton.Data].isDown = false;
        return this.onMotion(vp, pos, Tool_1.InputSource.Mouse, false, mov);
    }
    adjustPointToACS(pointActive, vp, perpendicular) {
        // The "I don't want ACS lock" flag can be set by tools to override the default behavior
        if (0 !== (this.toolState.coordLockOvr & Tool_1.CoordinateLockOverrides.ACS))
            return;
        let viewZRoot;
        // Lock to the construction plane
        if (vp.view.is3d() && vp.view.isCameraOn)
            viewZRoot = vp.view.camera.eye.vectorTo(pointActive);
        else
            viewZRoot = vp.rotation.getRow(2);
        const auxOriginRoot = vp.getAuxCoordOrigin();
        const auxRMatrixRoot = vp.getAuxCoordRotation();
        let auxNormalRoot = auxRMatrixRoot.getRow(2);
        // If ACS xy plane is perpendicular to view and not snapping, project to closest xz or yz plane instead
        if (auxNormalRoot.isPerpendicularTo(viewZRoot) && !AccuSnap_1.TentativeOrAccuSnap.isHot) {
            const auxXRoot = auxRMatrixRoot.getRow(0);
            const auxYRoot = auxRMatrixRoot.getRow(1);
            auxNormalRoot = (Math.abs(auxXRoot.dotProduct(viewZRoot)) > Math.abs(auxYRoot.dotProduct(viewZRoot))) ? auxXRoot : auxYRoot;
        }
        (0, LinePlaneIntersect_1.linePlaneIntersect)(pointActive, pointActive, viewZRoot, auxOriginRoot, auxNormalRoot, perpendicular);
    }
    adjustPointToGrid(pointActive, vp) {
        // The "I don't want grid lock" flag can be set by tools to override the default behavior
        if (!this.gridLock || 0 !== (this.toolState.coordLockOvr & Tool_1.CoordinateLockOverrides.Grid))
            return;
        vp.pointToGrid(pointActive);
    }
    adjustPoint(pointActive, vp, projectToACS = true, applyLocks = true) {
        if (Math.abs(pointActive.z) < 1.0e-7)
            pointActive.z = 0.0; // remove Z fuzz introduced by active depth when near 0
        let handled = false;
        if (applyLocks && !(IModelApp_1.IModelApp.tentativePoint.isActive || IModelApp_1.IModelApp.accuSnap.isHot))
            handled = IModelApp_1.IModelApp.accuDraw.adjustPoint(pointActive, vp, false);
        // NOTE: We don't need to support axis lock, it is worthless if you have AccuDraw
        if (!handled && vp.isPointAdjustmentRequired) {
            if (applyLocks)
                this.adjustPointToGrid(pointActive, vp);
            if (projectToACS)
                this.adjustPointToACS(pointActive, vp, false);
        }
        else if (applyLocks) {
            const savePoint = pointActive.clone();
            this.adjustPointToGrid(pointActive, vp);
            // if grid lock changes point, resend point to accudraw
            if (handled && !pointActive.isExactEqual(savePoint))
                IModelApp_1.IModelApp.accuDraw.adjustPoint(pointActive, vp, false);
        }
        if (Math.abs(pointActive.z) < 1.0e-7)
            pointActive.z = 0.0;
    }
    adjustSnapPoint(perpendicular = true) {
        const snap = AccuSnap_1.TentativeOrAccuSnap.getCurrentSnap(false);
        if (!snap)
            return;
        const vp = snap.viewport;
        const isHot = snap.isHot;
        const point = snap.getPoint().clone();
        const savePt = point.clone();
        if (!isHot) // Want point adjusted to grid for a hit that isn't hot
            this.adjustPointToGrid(point, vp);
        if (!IModelApp_1.IModelApp.accuDraw.adjustPoint(point, vp, isHot)) {
            if (vp.isSnapAdjustmentRequired)
                this.adjustPointToACS(point, vp, perpendicular || IModelApp_1.IModelApp.accuDraw.isActive);
        }
        if (!point.isExactEqual(savePt))
            snap.adjustedPoint.setFrom(point);
    }
    /** Application sub-classes can override this method to intercept button events before they are sent to the active tool.
     * An example use for this event would be to implement a shift + right-click or right-press menu.
     * @return true if event was handled and should not propagate to the active tool.
     */
    onPreButtonEvent(_ev) {
        return false;
    }
    /** @internal */
    async sendButtonEvent(ev) {
        const overlayHit = this.pickCanvasDecoration(ev);
        if (undefined !== overlayHit && undefined !== overlayHit.onMouseButton && overlayHit.onMouseButton(ev))
            return;
        if (this.onPreButtonEvent(ev))
            return;
        if (IModelApp_1.IModelApp.accuSnap.onPreButtonEvent(ev))
            return;
        const activeTool = this.activeTool;
        let tool = activeTool;
        if (undefined !== tool) {
            if (!tool.isValidLocation(ev, true))
                tool = undefined;
            else if (ev.isDown)
                tool.receivedDownEvent = true;
            else if (tool.receivedDownEvent)
                tool.receivedDownEvent = false;
            else
                tool = undefined;
        }
        if (IModelApp_1.IModelApp.accuDraw.onPreButtonEvent(ev))
            return;
        let updateDynamics = false;
        switch (ev.button) {
            case Tool_1.BeButton.Data: {
                if (undefined === tool) {
                    if (undefined !== activeTool)
                        break;
                    tool = this.idleTool; // Pass data button event to idle tool when no active tool present
                }
                if (ev.isDown) {
                    await tool.onDataButtonDown(ev);
                }
                else {
                    await tool.onDataButtonUp(ev);
                    break;
                }
                // Lock tool to target model of this view on first data button
                if (tool instanceof PrimitiveTool_1.PrimitiveTool)
                    tool.autoLockTarget();
                updateDynamics = true; // AccuDraw.onPostButtonEvent needs to process the active tool's pending hints from onDataButtonDown before calling updateDynamics...
                break;
            }
            case Tool_1.BeButton.Reset: {
                if (undefined === tool) {
                    if (undefined !== activeTool)
                        break;
                    tool = this.idleTool; // Pass reset button event to idle tool when no active tool present
                }
                if (ev.isDown)
                    await tool.onResetButtonDown(ev);
                else
                    await tool.onResetButtonUp(ev);
                break;
            }
            case Tool_1.BeButton.Middle: {
                // Pass middle button event to idle tool when active tool doesn't explicitly handle it
                if (ev.isDown) {
                    if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onMiddleButtonDown(ev))
                        await this.idleTool.onMiddleButtonDown(ev);
                }
                else {
                    if (undefined === tool || Tool_1.EventHandled.Yes !== await tool.onMiddleButtonUp(ev))
                        await this.idleTool.onMiddleButtonUp(ev);
                }
                break;
            }
        }
        IModelApp_1.IModelApp.tentativePoint.onButtonEvent(ev);
        IModelApp_1.IModelApp.accuDraw.onPostButtonEvent(ev);
        if (!updateDynamics)
            return;
        // Update tool dynamics for current cursor location to not require a motion event.
        this.updateDynamics(undefined, undefined, true);
    }
    async onButtonDown(vp, pt2d, button, inputSource) {
        const filtered = this.filterViewport(vp);
        if (undefined === this._viewTool && button === Tool_1.BeButton.Data)
            await IModelApp_1.IModelApp.viewManager.setSelectedView(vp);
        if (filtered)
            return;
        vp.setAnimator();
        const ev = new Tool_1.BeButtonEvent();
        const current = this.currentInputState;
        current.fromButton(vp, pt2d, inputSource, true);
        current.onButtonDown(button);
        current.toEvent(ev, true);
        current.updateDownPoint(ev);
        return this.sendButtonEvent(ev);
    }
    async onButtonUp(vp, pt2d, button, inputSource) {
        if (this.filterViewport(vp))
            return;
        const ev = new Tool_1.BeButtonEvent();
        const current = this.currentInputState;
        const wasDragging = current.isDragging(button);
        current.fromButton(vp, pt2d, inputSource, true);
        current.onButtonUp(button);
        current.toEvent(ev, true);
        if (wasDragging)
            return this.sendEndDragEvent(ev);
        current.changeButtonToDownPoint(ev);
        return this.sendButtonEvent(ev);
    }
    /** Called when any *modifier* (Shift, Alt, or Control) key is pressed or released. */
    async onModifierKeyTransition(wentDown, modifier, event) {
        const activeTool = this.activeTool;
        const changed = activeTool ? await activeTool.onModifierKeyTransition(wentDown, modifier, event) : Tool_1.EventHandled.No;
        if (changed === Tool_1.EventHandled.Yes) {
            IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
            this.updateDynamics(undefined, undefined, true); // Don't wait for motion to update dynamics...
        }
    }
    get _isFocusHome() {
        return (document.body === document.activeElement);
    }
    _setFocusHome() {
        const element = document.activeElement;
        if (element && element !== document.body)
            element.blur();
        document.body.focus();
    }
    static getModifierKey(event) {
        switch (event.key) {
            case "Alt": return Tool_1.BeModifierKeys.Alt;
            case "Shift": return Tool_1.BeModifierKeys.Shift;
            case "Control": return Tool_1.BeModifierKeys.Control;
        }
        return Tool_1.BeModifierKeys.None;
    }
    /** Process key down events while the Ctrl key is pressed */
    async onCtrlKeyPressed(keyEvent) {
        let handled = false;
        let result = false;
        switch (keyEvent.key) {
            case "z":
            case "Z":
                result = await this.doUndoOperation();
                handled = true;
                break;
            case "y":
            case "Y":
                result = await this.doRedoOperation();
                handled = true;
                break;
            case "F2":
                result = IModelApp_1.IModelApp.uiAdmin.showKeyinPalette();
                handled = true;
                break;
        }
        return { handled, result };
    }
    /** Sub-classes should override to process shortcut key events.
     * @return true if handled and no further processing of event should occur.
     */
    async processShortcutKey(_keyEvent, _wentDown) {
        return false;
    }
    /** Event for every key down and up transition.
     * @note Called before processShortcutKey.
     * @return true if handled and no further processing of event should occur.
     */
    processKeyboardEvent(keyEvent, _wentDown) {
        if (this._isFocusHome || undefined !== IModelApp_1.IModelApp.accuDraw.getFocusItem())
            return false; // Focus is Home or AccuDraw, allow shortcuts...
        // NOTE: Provide a convenient way for the user to move focus to Home to use shortcuts.
        // Escape is the only practical choice with its default behavior that can cancel/close/blur.
        // Intentionally not checking wentDown as some ui elements stop propagation of down but not up (moving to capture is not a good option).
        // Apps that want to use Escape to start the default tool still can with Home focus in processShortcutKey.
        if (keyEvent.key === "Escape" && ToolSettings_1.ToolSettings.escapeMovesFocusToHome && !keyEvent.defaultPrevented && !keyEvent.isComposing)
            this._setFocusHome();
        return true;
    }
    /** Need to check for modifier changes on capture phase as a ui element that calls stopPropagation
     * prevents onKeyTransition from being called when bubbling.
     */
    async onKeyTransitionCaptured(keyEvent) {
        this.currentInputState.setKeyQualifiers(keyEvent);
        const modifierKey = ToolAdmin.getModifierKey(keyEvent);
        if (Tool_1.BeModifierKeys.None !== modifierKey)
            return this.onModifierKeyTransition("keydown" === keyEvent.type, modifierKey, keyEvent);
    }
    /** Event for every key down and up transition. */
    async onKeyTransition(event, wentDown) {
        const keyEvent = event.ev;
        if (this.processKeyboardEvent(keyEvent, wentDown))
            return Tool_1.EventHandled.Yes;
        if (wentDown && keyEvent.ctrlKey) {
            const { handled, result } = await this.onCtrlKeyPressed(keyEvent);
            if (handled)
                return result;
        }
        const activeTool = this.activeTool;
        if (activeTool) {
            if (Tool_1.EventHandled.Yes === await activeTool.onKeyTransition(wentDown, keyEvent))
                return Tool_1.EventHandled.Yes;
        }
        if (await this.processShortcutKey(keyEvent, wentDown))
            return Tool_1.EventHandled.Yes;
        return Tool_1.EventHandled.No;
    }
    /** Called to undo previous data button for primitive tools or undo last write operation. */
    async doUndoOperation() {
        const activeTool = this.activeTool;
        if (activeTool instanceof PrimitiveTool_1.PrimitiveTool) {
            // ### TODO Add method so UI can be showing string to inform user that undo of last data point is available...
            if (await activeTool.undoPreviousStep())
                return true;
        }
        const imodel = IModelApp_1.IModelApp.viewManager.selectedView?.view.iModel;
        if (undefined === imodel || imodel.isReadonly || !imodel.isBriefcaseConnection())
            return false;
        return imodel.txns.reverseSingleTxnAsync().then(() => true).catch(() => false);
    }
    /** Called to redo previous data button for primitive tools or undo last write operation. */
    async doRedoOperation() {
        const activeTool = this.activeTool;
        if (activeTool instanceof PrimitiveTool_1.PrimitiveTool) {
            // ### TODO Add method so UI can be showing string to inform user that undo of last data point is available...
            if (await activeTool.redoPreviousStep())
                return true;
        }
        const imodel = IModelApp_1.IModelApp.viewManager.selectedView?.view.iModel;
        if (undefined === imodel || imodel.isReadonly || !imodel.isBriefcaseConnection())
            return false;
        return imodel.txns.reinstateTxnAsync().then(() => true).catch(() => false);
    }
    onActiveToolChanged(tool, start) {
        this.clearMotionPromises();
        this.activeToolChanged.raiseEvent(tool, start);
    }
    async onUnsuspendTool() {
        const tool = this.activeTool;
        if (tool === undefined)
            return;
        await tool.onUnsuspend();
        this.onActiveToolChanged(tool, StartOrResume.Resume);
    }
    /** @internal */
    async setInputCollector(newTool) {
        if (undefined !== this._inputCollector) {
            await this._inputCollector.onCleanup();
            this._inputCollector = undefined;
        }
        this._inputCollector = newTool;
    }
    /** @internal */
    async exitInputCollector() {
        if (undefined === this._inputCollector)
            return;
        let unsuspend = false;
        if (this._suspendedByInputCollector) {
            this._suspendedByInputCollector.stop();
            this._suspendedByInputCollector = undefined;
            unsuspend = true;
        }
        IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
        await this.setInputCollector(undefined);
        if (unsuspend)
            await this.onUnsuspendTool();
        IModelApp_1.IModelApp.accuDraw.onInputCollectorExit();
        this.updateDynamics(undefined, undefined, true);
    }
    /** @internal */
    async startInputCollector(newTool) {
        IModelApp_1.IModelApp.notifications.outputPrompt("");
        IModelApp_1.IModelApp.accuDraw.onInputCollectorInstall();
        if (undefined !== this._inputCollector) {
            await this.setInputCollector(undefined);
        }
        else {
            const tool = this.activeTool;
            if (tool)
                await tool.onSuspend();
            this._suspendedByInputCollector = new SuspendedToolState();
        }
        IModelApp_1.IModelApp.viewManager.endDynamicsMode();
        IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
        await this.setInputCollector(newTool);
        // it is important to raise event after setInputCollector is called
        this.onActiveToolChanged(newTool, StartOrResume.Start);
    }
    /** @internal */
    async setViewTool(newTool) {
        if (undefined !== this._viewTool) {
            await this._viewTool.onCleanup();
            this._viewTool = undefined;
        }
        this._viewTool = newTool;
    }
    /** @internal */
    async exitViewTool() {
        if (undefined === this._viewTool)
            return;
        let unsuspend = false;
        if (undefined !== this._suspendedByViewTool) {
            this._suspendedByViewTool.stop(); // Restore state of suspended tool
            this._suspendedByViewTool = undefined;
            unsuspend = true;
        }
        IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
        await this.setViewTool(undefined);
        if (unsuspend)
            await this.onUnsuspendTool();
        IModelApp_1.IModelApp.accuDraw.onViewToolExit();
        this.updateDynamics(undefined, undefined, true);
    }
    /** @internal */
    async startViewTool(newTool) {
        IModelApp_1.IModelApp.notifications.outputPrompt("");
        IModelApp_1.IModelApp.accuDraw.onViewToolInstall();
        if (undefined !== this._viewTool) {
            await this.setViewTool(undefined);
        }
        else {
            const tool = this.activeTool;
            if (tool)
                await tool.onSuspend();
            this._suspendedByViewTool = new SuspendedToolState();
        }
        IModelApp_1.IModelApp.viewManager.endDynamicsMode();
        IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
        this.toolState.coordLockOvr = Tool_1.CoordinateLockOverrides.All;
        this.toolState.locateCircleOn = false;
        IModelApp_1.IModelApp.accuSnap.onStartTool();
        this.setCursor(IModelApp_1.IModelApp.viewManager.crossHairCursor);
        await this.setViewTool(newTool);
        // it is important to raise event after setViewTool is called
        this.onActiveToolChanged(newTool, StartOrResume.Start);
    }
    /** @internal */
    setEditCommandHandler(handler) {
        this._editCommandHandler = handler;
    }
    /** @internal */
    async setPrimitiveTool(newTool) {
        if (undefined !== this._primitiveTool) {
            await this._primitiveTool.onCleanup();
            if (undefined !== this._editCommandHandler)
                await this._editCommandHandler.finishCommand();
            this._primitiveTool = undefined;
        }
        this._primitiveTool = newTool;
    }
    // serialize concurrent starts to avoid two tools installing simultaneously.
    _primitiveToolStarting;
    /** @internal */
    async startPrimitiveTool(newTool) {
        // If another start is in progress wait for it to finish before proceeding.
        while (undefined !== this._primitiveToolStarting)
            await this._primitiveToolStarting;
        let resolveStarting;
        this._primitiveToolStarting = new Promise((r) => { resolveStarting = r; });
        try {
            IModelApp_1.IModelApp.notifications.outputPrompt("");
            await this.exitViewTool();
            if (undefined !== this._primitiveTool)
                await this.setPrimitiveTool(undefined);
            // clear the primitive tool first so following call does not trigger the refreshing of the ToolSetting for the previous primitive tool
            await this.exitInputCollector();
            IModelApp_1.IModelApp.viewManager.endDynamicsMode();
            this.setIncompatibleViewportCursor(true); // Don't restore this
            IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
            this.toolState.coordLockOvr = Tool_1.CoordinateLockOverrides.None;
            this.toolState.locateCircleOn = false;
            IModelApp_1.IModelApp.accuDraw.onPrimitiveToolInstall();
            IModelApp_1.IModelApp.accuSnap.onStartTool();
            if (undefined !== newTool) {
                this.setCursor(IModelApp_1.IModelApp.viewManager.crossHairCursor);
                await this.setPrimitiveTool(newTool);
            }
            // it is important to raise event after setPrimitiveTool is called
            const activeTool = newTool ?? this._idleTool;
            // _idleTool is cleared during onShutDown(); skip the event if shutdown has already run
            // to avoid emitting activeToolChanged with an undefined tool.
            if (undefined === activeTool)
                return;
            this.onActiveToolChanged(activeTool, StartOrResume.Start);
        }
        finally {
            if (resolveStarting)
                resolveStarting();
            this._primitiveToolStarting = undefined;
        }
    }
    /** Method used by interactive tools to send updated values to UI components, typically showing tool settings.
     * @beta
     */
    syncToolSettingsProperties(toolId, syncProperties) {
        if (this.toolSettingsChangeHandler)
            this.toolSettingsChangeHandler(toolId, syncProperties);
    }
    /** Method used by interactive tools to send request to reload UI from properties returned via method supplyToolSettingsProperties.
     * @beta
     */
    reloadToolSettingsProperties() {
        if (this.reloadToolSettingsHandler)
            this.reloadToolSettingsHandler();
    }
    /** Method used to "bump" the value of a tool setting for the current tool.
     * To "bump" a setting means to toggle a boolean value or cycle through enum values.
     * If no `settingIndex` param is specified, the first setting is bumped.
     * Returns true if the setting was successfully bumped.
     */
    async bumpToolSetting(settingIndex) {
        return this.currentTool.bumpToolSetting(settingIndex);
    }
    /** Method used by interactive tools to inform one or more UI components to refresh. This is typically used to update labels or icons associated with a specific tool.
     * This method should be used when the caller wants the UI layer to process the sync event immediately. Use dispatchUiSyncEvent when the event may be triggered while other
     * more important user interaction processing is required.
     * @param specificSyncEventId Optional sync event id. If not specified then "tool-admin-refresh-ui" is used.
     * @param toolId Optional, will be used if specificSyncEventId is not specified. If used, the resulting sync event Id will be created using `tool-admin-refresh-ui-${toolId}`.toLowerCase()
     */
    dispatchImmediateUiSyncEvent(specificSyncEventId, toolId) {
        const defaultRefreshEventId = "tool-admin-refresh-ui";
        if (this.toolSyncUiEventDispatcher) {
            if (specificSyncEventId)
                this.toolSyncUiEventDispatcher(specificSyncEventId.toLowerCase(), true);
            else if (toolId)
                this.toolSyncUiEventDispatcher(`${defaultRefreshEventId}-${toolId}`.toLowerCase(), true);
            else
                this.toolSyncUiEventDispatcher(defaultRefreshEventId, true);
        }
    }
    /** Method used by interactive tools to inform one or more UI components to refresh. This is typically used to update labels or icons associated with a specific tool.
     * This method should be used when the caller wants the UI layer to process the sync event on a timer, waiting a few 100 ms, allowing other events that may require a UI refresh
     * to be processed together.
     * @param specificSyncEventId Optional sync event id. If not specified then "tool-admin-refresh-ui" is used.
     * @param toolId Optional, will be used if specificSyncEventId is not specified. If used, the resulting sync event Id will be created using `tool-admin-refresh-ui-${toolId}`.toLowerCase()
     */
    dispatchUiSyncEvent(specificSyncEventId, toolId) {
        const defaultRefreshEventId = "tool-admin-refresh-ui";
        if (this.toolSyncUiEventDispatcher) {
            if (specificSyncEventId)
                this.toolSyncUiEventDispatcher(specificSyncEventId.toLowerCase());
            else if (toolId)
                this.toolSyncUiEventDispatcher(`${defaultRefreshEventId}-${toolId}`.toLowerCase());
            else
                this.toolSyncUiEventDispatcher(defaultRefreshEventId);
        }
    }
    /**
     * Starts the default [[PrimitiveTool]], if any. Generally invoked automatically when other tools exit, so shouldn't be called directly.
     * @note The default tool, when specified, must be a subclass of [[PrimitiveTool]]. A call to startDefaultTool is required to terminate
     * an active [[ViewTool]] or [[InputCollector]] and replace or clear the current [[PrimitiveTool]]. The default tool can not be
     * a subclass of [[ViewTool]] as view tools replace each other and aren't suspended. This means [[ViewTool.exitTool]] would
     * result in the active tool being undefined instead of making the default tool active.
     * The tool's [[Tool.run]] method is invoked with arguments specified by [[defaultToolArgs]].
     * @see [[defaultToolId]] to configure the default tool.
     */
    async startDefaultTool() {
        const tool = IModelApp_1.IModelApp.tools.create(this.defaultToolId, this.defaultToolArgs);
        if (tool instanceof PrimitiveTool_1.PrimitiveTool) {
            if (!await tool.run(this.defaultToolArgs))
                return this.startPrimitiveTool(undefined);
        }
        else {
            await this.startPrimitiveTool(undefined); // Ensure active primitive tool is terminated...
            if (undefined !== tool)
                throw new Error("Default tool must be a subclass of PrimitiveTool");
        }
    }
    /**
     * Call from external events or immediate tools that may have invalidated the current primitive tool's state.
     * Examples are undo, which may invalidate any references to elements, or an immediate tool that uses an edit command to write to the iModel,
     * since immediate tools do not replace the active tool.
     * The current primitive tool is expected to call installTool with a new instance, or exitTool to start the default tool.
     * @note Should be called even if the primitive tool is currently suspended by a view tool or input collector.
     */
    async restartPrimitiveTool() {
        if (undefined !== this._primitiveTool)
            await this._primitiveTool.onRestartTool();
    }
    setCursor(cursor) {
        if (undefined === this._saveCursor)
            IModelApp_1.IModelApp.viewManager.setViewCursor(cursor);
        else
            this._saveCursor = cursor;
    }
    /** @internal */
    testDecorationHit(id) { return this.currentTool.testDecorationHit(id); }
    /** @internal */
    getDecorationGeometry(hit) { return this.currentTool.getDecorationGeometry(hit); }
    /** @internal */
    decorate(context) {
        const tool = this.activeTool;
        if (undefined !== tool) {
            tool.decorate(context);
            if (undefined !== this._inputCollector && tool !== this._inputCollector)
                this._inputCollector.decorateSuspended(context);
            if (undefined !== this._primitiveTool && tool !== this._primitiveTool)
                this._primitiveTool.decorateSuspended(context);
        }
        const viewport = this.currentInputState.viewport;
        if (viewport !== context.viewport)
            return;
        const ev = new Tool_1.BeButtonEvent();
        this.fillEventFromCursorLocation(ev);
        const hit = IModelApp_1.IModelApp.accuDraw.isActive ? undefined : IModelApp_1.IModelApp.accuSnap.currHit; // NOTE: Show surface normal until AccuDraw becomes active
        viewport.drawLocateCursor(context, ev.viewPoint, viewport.pixelsFromInches(IModelApp_1.IModelApp.locateManager.apertureInches), this.isLocateCircleOn, hit);
    }
    get isLocateCircleOn() {
        if (!this.toolState.locateCircleOn || undefined !== this._canvasDecoration)
            return false;
        if (Tool_1.InputSource.Mouse === this.currentInputState.inputSource)
            return true;
        return (Tool_1.InputSource.Touch === this.currentInputState.inputSource && undefined !== IModelApp_1.IModelApp.accuSnap.touchCursor);
    }
    /** @internal */
    beginDynamics() {
        IModelApp_1.IModelApp.accuDraw.onBeginDynamics();
        IModelApp_1.IModelApp.viewManager.beginDynamicsMode();
        this.setCursor(IModelApp_1.IModelApp.viewManager.dynamicsCursor);
    }
    /** @internal */
    endDynamics() {
        IModelApp_1.IModelApp.accuDraw.onEndDynamics();
        IModelApp_1.IModelApp.viewManager.endDynamicsMode();
        this.setCursor(IModelApp_1.IModelApp.viewManager.crossHairCursor);
    }
    /** Fill the supplied button event from the current cursor location. */
    fillEventFromCursorLocation(ev, useSnap = true) { this.currentInputState.toEvent(ev, useSnap); }
    /** Fill the supplied button event from the last data button location. */
    fillEventFromLastDataButton(ev) { this.currentInputState.toEventFromLastDataPoint(ev); }
    /** @internal */
    setAdjustedDataPoint(ev) { this.currentInputState.adjustLastDataPoint(ev); }
    /** Can be called by tools that wish to emulate mouse button down/up events for onTouchTap. */
    async convertTouchTapToButtonDownAndUp(ev, button = Tool_1.BeButton.Data) {
        const vp = ev.viewport;
        if (undefined === vp)
            return;
        const pt2d = ev.viewPoint;
        await this.onButtonDown(vp, pt2d, button, Tool_1.InputSource.Touch);
        return this.onButtonUp(vp, pt2d, button, Tool_1.InputSource.Touch);
    }
    /** Can be called by tools that wish to emulate moving the mouse with a button depressed for onTouchMoveStart.
     * @note Calls the tool's onMouseStartDrag method from onMotion.
     */
    async convertTouchMoveStartToButtonDownAndMotion(startEv, ev, button = Tool_1.BeButton.Data) {
        const startVp = startEv.viewport;
        if (undefined === startVp)
            return;
        const vp = ev.viewport;
        if (undefined === vp)
            return;
        await this.onButtonDown(startVp, startEv.viewPoint, button, Tool_1.InputSource.Touch);
        return this.onMotion(vp, ev.viewPoint, Tool_1.InputSource.Touch, true);
    }
    /** Can be called by tools that wish to emulate pressing the mouse button for onTouchStart or onTouchMoveStart. */
    async convertTouchStartToButtonDown(ev, button = Tool_1.BeButton.Data) {
        const vp = ev.viewport;
        if (undefined === vp)
            return;
        return this.onButtonDown(vp, ev.viewPoint, button, Tool_1.InputSource.Touch);
    }
    /** Can be called by tools that wish to emulate releasing the mouse button for onTouchEnd or onTouchComplete.
     * @note Calls the tool's onMouseEndDrag method if convertTouchMoveStartToButtonDownAndMotion was called for onTouchMoveStart.
     */
    async convertTouchEndToButtonUp(ev, button = Tool_1.BeButton.Data) {
        const vp = ev.viewport;
        if (undefined === vp)
            return;
        return this.onButtonUp(vp, ev.viewPoint, button, Tool_1.InputSource.Touch);
    }
    /** Can be called by tools that wish to emulate a mouse motion event for onTouchMove. */
    async convertTouchMoveToMotion(ev) {
        const vp = ev.viewport;
        if (undefined === vp)
            return;
        return this.onMotion(vp, ev.viewPoint, Tool_1.InputSource.Touch);
    }
    /** Can be called by tools to invoke their [[InteractiveTool.onDynamicFrame]] method without requiring a motion event. */
    simulateMotionEvent() {
        // NOTE: Prefer last resolved motion over current cursor location which could be out of the view, or moved from last AccuSnap etc.
        this.updateDynamics(this.currentInputState.lastMotionEvent, undefined, true);
    }
    /** @internal */
    setIncompatibleViewportCursor(restore) {
        if (restore) {
            if (undefined === this._saveCursor)
                return;
            this.toolState.locateCircleOn = this._saveLocateCircle;
            IModelApp_1.IModelApp.viewManager.setViewCursor(this._saveCursor);
            this._saveCursor = undefined;
            return;
        }
        if (undefined !== this._saveCursor)
            return;
        this._saveLocateCircle = this.toolState.locateCircleOn;
        this._saveCursor = IModelApp_1.IModelApp.viewManager.cursor;
        this.toolState.locateCircleOn = false;
        IModelApp_1.IModelApp.viewManager.setViewCursor("not-allowed");
    }
    /** Performs default handling of mouse wheel event (zoom in/out) */
    async processWheelEvent(ev, doUpdate) {
        await WheelEventProcessor.process(ev, doUpdate);
        IModelApp_1.IModelApp.viewManager.invalidateDecorationsAllViews();
        this.updateDynamics(ev);
        return Tool_1.EventHandled.Yes;
    }
    /** @internal */
    async onSelectedViewportChanged(previous, current) {
        IModelApp_1.IModelApp.accuDraw.onSelectedViewportChanged(previous, current);
        if (undefined === current)
            return this.callOnCleanup();
        if (undefined !== this._viewTool)
            this._viewTool.onSelectedViewportChanged(previous, current);
        if (undefined !== this._inputCollector)
            this._inputCollector.onSelectedViewportChanged(previous, current);
        if (undefined !== this._primitiveTool)
            await this._primitiveTool.onSelectedViewportChanged(previous, current);
    }
    setLocateCircleOn(locateOn) {
        if (undefined === this._saveCursor)
            this.toolState.locateCircleOn = locateOn;
        else
            this._saveLocateCircle = locateOn;
    }
    setLocateCursor(enableLocate) {
        const viewManager = IModelApp_1.IModelApp.viewManager;
        this.setCursor(viewManager.inDynamicsMode ? viewManager.dynamicsCursor : viewManager.crossHairCursor);
        this.setLocateCircleOn(enableLocate);
        viewManager.invalidateDecorationsAllViews();
    }
    /** Controls how the button event location is adjusted for the active tool */
    get coordinateLockOverrides() { return this.toolState.coordLockOvr; }
    set coordinateLockOverrides(coordLockOvr) { this.toolState.coordLockOvr = coordLockOvr; }
    /** @internal */
    async callOnCleanup() {
        await this.exitViewTool();
        await this.exitInputCollector();
        if (undefined !== this._primitiveTool)
            await this._primitiveTool.onCleanup();
    }
}
exports.ToolAdmin = ToolAdmin;
/**
 * Default processor to handle wheel events.
 * @internal
 */
class WheelEventProcessor {
    static async process(ev, doUpdate) {
        const vp = ev.viewport;
        if (undefined === vp)
            return;
        await this.doZoom(ev);
        if (doUpdate) {
            // AccuSnap hit won't be invalidated without cursor motion (closes info window, etc.).
            IModelApp_1.IModelApp.accuSnap.clear();
        }
    }
    static async doZoom(ev) {
        const vp = ev.viewport;
        if (undefined === vp)
            return ViewStatus_1.ViewStatus.InvalidViewport;
        let zoomRatio = ToolSettings_1.ToolSettings.wheelZoomRatio;
        if (zoomRatio < 1)
            zoomRatio = 1;
        if (ev.wheelDelta > 0)
            zoomRatio = 1 / zoomRatio;
        let isSnapOrPrecision = false;
        const target = core_geometry_1.Point3d.create();
        if (IModelApp_1.IModelApp.tentativePoint.isActive) {
            // Always use Tentative location, adjusted point, not cross
            isSnapOrPrecision = true;
            target.setFrom(IModelApp_1.IModelApp.tentativePoint.getPoint());
        }
        else {
            // Never use AccuSnap location as initial zoom clears snap causing zoom center to "jump"
            isSnapOrPrecision = Tool_1.CoordSource.Precision === ev.coordsFrom;
            target.setFrom(isSnapOrPrecision ? ev.point : ev.rawPoint);
        }
        const view = vp.view;
        let globalAlignment;
        if (view.is3d() && view.iModel.ecefLocation)
            globalAlignment = { target, transition: zoomRatio > 1 };
        const animationOptions = {
            animateFrustumChange: true,
            cancelOnAbort: true,
            animationTime: Viewport_1.ScreenViewport.animation.time.wheel.milliseconds,
            easingFunction: core_common_1.Easing.Cubic.Out,
            onExtentsError: (err) => view.outputStatusMessage(err),
            globalAlignment,
        };
        const currentInputState = IModelApp_1.IModelApp.toolAdmin.currentInputState;
        let status;
        const now = Date.now();
        if (view.is3d() && view.isCameraOn) {
            if (!isSnapOrPrecision) {
                let lastEvent = currentInputState.lastWheelEvent;
                if (undefined !== lastEvent && lastEvent.viewport &&
                    now - lastEvent.time < ToolSettings_1.ToolSettings.doubleClickTimeout.milliseconds &&
                    lastEvent.viewport.view.equals(view) && lastEvent.viewPoint.distanceSquaredXY(ev.viewPoint) < 10) {
                    target.setFrom(lastEvent.point);
                    lastEvent.time = now;
                }
                else {
                    const newTarget = vp.pickNearestVisibleGeometry(target);
                    if (undefined !== newTarget) {
                        target.setFrom(newTarget);
                    }
                    else {
                        view.getTargetPoint(target);
                    }
                    currentInputState.lastWheelEvent = lastEvent = ev.clone();
                    lastEvent.point.setFrom(target);
                }
            }
            const transform = core_geometry_1.Transform.createFixedPointAndMatrix(target, core_geometry_1.Matrix3d.createScale(zoomRatio, zoomRatio, zoomRatio));
            const eye = view.getEyePoint();
            const newEye = transform.multiplyPoint3d(eye);
            const offset = eye.vectorTo(newEye);
            // when you're too close to an object, the wheel zoom operation will stop. We set a "bump distance" so you can blast through obstacles.
            const bumpDist = Math.max(ToolSettings_1.ToolSettings.wheelZoomBumpDistance, view.minimumFrontDistance());
            if (offset.magnitude() < bumpDist) {
                offset.scaleToLength(bumpDist, offset); // move bump distance, just to get to the other side.
                target.addInPlace(offset);
                newEye.setFrom(eye.plus(offset));
                currentInputState.lastWheelEvent = undefined; // we need to search on the "other side" of what we were bumping into
            }
            const zDir = view.getZVector();
            target.setFrom(newEye.plusScaled(zDir, zDir.dotProduct(newEye.vectorTo(target))));
            if (ViewStatus_1.ViewStatus.Success === (status = view.lookAt({ eyePoint: newEye, targetPoint: target, upVector: view.getYVector(), lensAngle: view.camera.lens, opts: animationOptions })))
                vp.synchWithView(animationOptions);
        }
        else {
            const targetNpc = vp.worldToNpc(target);
            const trans = core_geometry_1.Transform.createFixedPointAndMatrix(targetNpc, core_geometry_1.Matrix3d.createScale(zoomRatio, zoomRatio, 1));
            const viewCenter = trans.multiplyPoint3d(core_geometry_1.Point3d.create(.5, .5, .5));
            vp.npcToWorld(viewCenter, viewCenter);
            return vp.zoom(viewCenter, zoomRatio, animationOptions);
        }
        // if we scrolled out, we may have invalidated the current AccuSnap path
        await IModelApp_1.IModelApp.accuSnap.reEvaluate();
        return status;
    }
}
exports.WheelEventProcessor = WheelEventProcessor;
//# sourceMappingURL=ToolAdmin.js.map