/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module AccuDraw
 */
import { Parser } from "@itwin/core-quantity";
import { AccuDraw, CompassMode, ItemField } from "../AccuDraw";
import { ViewRect } from "../common/ViewRect";
import { IModelApp } from "../IModelApp";
import { AccuDrawShortcuts } from "./AccuDrawTool";
import { BeButtonEvent } from "./Tool";
/** Provides an in viewport user interface for AccuDraw that can optionally follow the cursor or remain at a fixed location.
 * @beta
 */
export class AccuDrawViewportUI extends AccuDraw {
    _focusItem;
    _controls;
    _toolTipsSuspended;
    _expression;
    /** Settings to control the behavior and visual appearance of the viewport controls.
     * @note Colors were chosen for visibility against viewport background and contents.
     */
    static controlProps = {
        /** Suspend locate tooltip when controls are shown, may wish to disable when using fixed location. */
        suspendLocateToolTip: true,
        /** Show controls at a fixed location in the view (currently bottom middle) instead of following the cursor. */
        fixedLocation: false,
        /** Layout controls in a single row horizontally instead of in columns vertically as an option when using fixed location. */
        horizontalArrangement: false,
        /** When controls follow the cursor, the X and Y offsets applied to the current point to position the top left (values in inches based on screen DPI) */
        cursorOffset: { x: .4, y: .1 },
        /** Replace "^", ";", and ".." with "°" or ":" for easier input. */
        simplifiedInput: true,
        /** Enable simple math operations not supported by quantity parser. */
        mathOperations: true,
        /** Settings that apply to both text input fields and lock buttons. */
        field: {
            /** Number of visible characters to show in text input fields. */
            size: 12,
            /** Height of text input fields and lock buttons. */
            height: "var(--iui-component-height-small, 1.75em)",
            /** Border settings for text input fields and lock buttons. */
            border: {
                /** Border width to use for text input fields and lock buttons. */
                width: "1px",
                /** Border style to use for text input fields and lock buttons. */
                style: "solid",
                /** Corner radius of text input fields and locks buttons. */
                radius: "var(--iui-border-radius-1, 0.25rem)",
            },
            /** Settings specific to text input fields and lock button labels. */
            text: {
                /** Font family to use for text input field values and button labels. */
                fontFamily: "var(--iui-font-sans, sans-serif)",
                /** Font size to use for text input field values and button labels. */
                fontSize: "var(--iui-font-size-1, 0.875rem)",
            },
        },
        /** Settings specific to text input fields. */
        input: {
            /** Font color to use for text input field values. */
            color: "var(--iui-color-text-muted, white)",
            /** Padding applied to text input fields. */
            padding: "0 var(--iui-size-s, 0.5rem)",
            /** Settings applied to text input fields when they have focus. */
            focused: {
                /** Font color for focused text input fields. */
                color: "hsla(0, 0%, 100%, 1)",
                /** Background color for focused text input fields. */
                backgroundColor: "hsl(var(--iui-color-accent-hsl, 166 96% 30.7%) / var(--iui-opacity-2, 85%))",
                /** Inner stroke for focused text input fields. */
                innerStroke: `inset 0px 0px 0px 1px var(--iui-color-background, #333c41)`,
                /** Border settings for focused text input fields. */
                border: {
                    /** Border color for focused text input fields. */
                    color: "hsl(var(--iui-color-accent-hsl, 166 96% 51%))",
                },
            },
            /** Settings applied to text input fields when they do not have focus. */
            unfocused: {
                /** Background color for unfocused text input fields. */
                backgroundColor: "hsl(var(--iui-color-background-hsl, 203 6% 21.25%) / var(--iui-opacity-2, 85%))",
                /** Border settings for unfocused text input fields. */
                border: {
                    /** Border color for unfocused text input fields. */
                    color: "var(--iui-color-border, hsla(215, 8%, 30%))",
                },
            },
        },
        /** Settings specific to lock buttons. */
        button: {
            /** Padding applied to lock buttons. */
            padding: "var(--iui-size-2xs, 0.25rem)",
            /** Settings applied to lock buttons when they are unlocked. */
            unlocked: {
                /** Text color for unlocked lock buttons. */
                color: "var(--iui-color-text, #cccccc)",
                /** Background color for unlocked lock buttons. */
                backgroundColor: "hsl(var(--iui-color-background-hsl, 203 6% 21.25%) / var(--iui-opacity-2, 85%))",
                /** Border settings for unlocked lock buttons. */
                border: {
                    /** Border color for unlocked lock buttons. */
                    color: "var(--iui-color-border, hsla(215, 8%, 30%))",
                },
            },
            /** Settings applied to lock buttons when they are locked. */
            locked: {
                /** Text color for locked lock buttons. */
                color: "hsla(0, 0%, 100%, 1)",
                /** Background color for locked lock buttons. */
                backgroundColor: "hsla(0, 0%, 100%, 0.16)",
                /** Border settings for locked lock buttons. */
                border: {
                    /** Border color for locked lock buttons. */
                    color: "hsla(0, 0%, 100%, 1)",
                },
            },
        },
        /** Spacing between fields within a control and between rows of controls. */
        spacing: {
            /** Spacing between input field and lock button within each field group. */
            gap: "var(--iui-size-2xs, 0.25rem)",
            /** Spacing between field groups (distance/angle, x, y, z controls). */
            margin: "var(--iui-size-s, 0.75rem)",
        },
    };
    /** Create a new instance of this class to set as [[IModelAppOptions.accuDraw]] for this session. */
    constructor() {
        super();
        this._focusItem = this.defaultFocusItem();
    }
    /** Call to set a vertical layout that follows the cursor. This is the default configuration. */
    setVerticalCursorLayout() {
        const props = AccuDrawViewportUI.controlProps;
        props.horizontalArrangement = false;
        props.fixedLocation = false;
        props.suspendLocateToolTip = true;
    }
    /** Call to set a horizontal layout that is anchored to the bottom middle of the view. */
    setHorizontalFixedLayout() {
        const props = AccuDrawViewportUI.controlProps;
        props.horizontalArrangement = true;
        props.fixedLocation = true;
        props.suspendLocateToolTip = false;
    }
    /** Call to update the currently displayed controls after settings are changed. */
    refreshControls() {
        if (undefined === this._controls)
            return;
        this.removeControls();
        if (!this.isActive)
            return;
        // Shouldn't need to call IModelApp.toolAdmin.simulateMotionEvent() in this case...
        const ev = new BeButtonEvent();
        IModelApp.toolAdmin.fillEventFromCursorLocation(ev);
        this.updateControls(ev);
    }
    suspendToolTips() {
        if (!AccuDrawViewportUI.controlProps.suspendLocateToolTip || undefined === (this._toolTipsSuspended = (IModelApp.accuSnap.userSettings.toolTip ? true : undefined)))
            return;
        IModelApp.accuSnap.userSettings.toolTip = false;
        IModelApp.notifications.clearToolTip();
    }
    unsuspendToolTips() {
        if (undefined === this._toolTipsSuspended)
            return;
        IModelApp.accuSnap.userSettings.toolTip = true;
        this._toolTipsSuspended = undefined;
    }
    setDynamicKeyinStatus(item) {
        // This does nothing if keyin status is already dynamic...
        AccuDrawShortcuts.itemFieldCompletedInput(item);
    }
    setPartialKeyinStatus(item, selectAll) {
        if (!this.isDynamicKeyinStatus(item))
            return;
        AccuDrawShortcuts.itemFieldNewInput(item);
        if (undefined === this._controls || !selectAll)
            return;
        const itemField = this._controls.itemFields[item];
        itemField.setSelectionRange(0, itemField.value.length);
    }
    evaluateExpression(operator, operandA, operandB) {
        switch (operator) {
            case "+":
                return operandA + operandB;
            case "-":
                return operandA - operandB;
            case "*":
                return operandA * operandB;
            case "/":
                return operandA / operandB;
            default:
                return operandA;
        }
    }
    parseExpression(currentValue, isAngle) {
        if (undefined === this._expression)
            return undefined; // Not an expression...
        // Attempt to parse and apply operation to current value...
        const operator = currentValue.lastIndexOf(this._expression.operator);
        if (-1 === operator) {
            this._expression = undefined; // Operator has been edited out of string, parse current value...
            return undefined;
        }
        const parserSpec = (isAngle ? this.getAngleParser() : this.getLengthParser());
        const formatterSpec = (isAngle ? this.getAngleFormatter() : this.getLengthFormatter());
        if (undefined === parserSpec || undefined === formatterSpec)
            return undefined; // Nothing to do...
        const operandAStr = currentValue.substring(0, operator);
        const parseResultA = parserSpec.parseToQuantityValue(operandAStr);
        if (!Parser.isParsedQuantity(parseResultA))
            return undefined; // First operand isn't valid, try to parse current value (which is also likely to fail)...
        const operandBStr = currentValue.substring(operator + this._expression.operator.length);
        const operatorKey = this._expression.operator[1];
        const isNumber = ("*" === operatorKey || "/" === operatorKey); // Treat as number for */ and quantity for +-...
        let operandB;
        if (isNumber) {
            operandB = parseFloat(operandBStr);
            if (Number.isNaN(operandB))
                return operandAStr; // Second operand is invalid number, set value to first operand which is valid...
        }
        else {
            const parseResultB = parserSpec.parseToQuantityValue(operandBStr);
            if (!Parser.isParsedQuantity(parseResultB))
                return operandAStr; // Second operand is invalid quantity, set value to first operand which is valid...
            operandB = parseResultB.value;
        }
        const operandA = parseResultA.value;
        const newValue = this.evaluateExpression(operatorKey, operandA, operandB);
        const newValueStr = IModelApp.quantityFormatter.formatQuantity(newValue, formatterSpec);
        return newValueStr;
    }
    async processPartialInput(item) {
        if (undefined === this._controls)
            return;
        const itemField = this._controls.itemFields[item];
        const currentValue = itemField.value;
        // If value was cleared, unlock field and sync internal state to cursor location while preserving partial keyin status...
        if (0 === currentValue.length) {
            await this.processFieldInput(item, currentValue, false);
            IModelApp.toolAdmin.simulateMotionEvent();
            return;
        }
        const isAngle = (ItemField.ANGLE_Item === item);
        const expressionValue = this.parseExpression(currentValue, isAngle);
        return this.processFieldInput(item, expressionValue ?? currentValue, false);
    }
    async acceptPartialInput(item, forward) {
        if (undefined === this._controls)
            return;
        const itemField = this._controls.itemFields[item];
        const currentValue = itemField.value;
        // Accepting with cleared value needs to set state back to dynamic and sync field to internal state...
        if (0 === currentValue.length && undefined === forward) {
            this.setDynamicKeyinStatus(item);
            return this.updateItemFieldValue(itemField, item);
        }
        const isAngle = (ItemField.ANGLE_Item === item);
        const expressionValue = this.parseExpression(currentValue, isAngle);
        if (undefined === forward)
            return AccuDrawShortcuts.itemFieldAcceptInput(item, expressionValue ?? currentValue);
        return AccuDrawShortcuts.itemFieldNavigate(item, expressionValue ?? currentValue, forward);
    }
    acceptSavedValue(item, next) {
        if (next)
            AccuDrawShortcuts.chooseNextValue(item);
        else
            AccuDrawShortcuts.choosePreviousValue(item);
    }
    doFocusHome(ev, isDown, _item) {
        ev.preventDefault();
        if (!isDown || this._isFocusHome)
            return;
        ev.stopPropagation();
        this.setFocusHome();
        IModelApp.viewManager.invalidateDecorationsAllViews();
    }
    async doChooseSavedValue(ev, isDown, item, next) {
        ev.preventDefault();
        ev.stopPropagation();
        if (!isDown)
            return;
        this.acceptSavedValue(item, next);
    }
    async doNavigate(ev, isDown, item, forward) {
        ev.preventDefault();
        ev.stopPropagation();
        if (!isDown)
            return;
        return this.acceptPartialInput(item, forward);
    }
    async doAcceptInput(ev, isDown, item) {
        ev.preventDefault();
        if (!isDown || this.isDynamicKeyinStatus(item))
            return;
        ev.stopPropagation();
        return this.acceptPartialInput(item);
    }
    doNewInput(_ev, isDown, item) {
        if (!isDown)
            return;
        this.setPartialKeyinStatus(item, false);
    }
    async doDeleteInput(_ev, isDown, item) {
        if (isDown)
            return this.setPartialKeyinStatus(item, false);
        return this.processPartialInput(item);
    }
    processReplacementKey(ev, isDown, item, replacement, allowStart) {
        if (undefined === this._controls)
            return false;
        ev.preventDefault();
        if (!isDown || 0 === replacement.length || this.isDynamicKeyinStatus(item))
            return true;
        const itemField = this._controls.itemFields[item];
        if (null === itemField.selectionStart || null === itemField.selectionEnd)
            return true;
        const currentValue = itemField.value;
        if (!allowStart && (!currentValue.length || !itemField.selectionStart || !itemField.selectionEnd))
            return true;
        const selectionStart = (itemField.selectionStart > itemField.selectionEnd ? itemField.selectionEnd : itemField.selectionStart);
        itemField.value = currentValue.substring(0, selectionStart) + replacement + currentValue.substring(itemField.selectionEnd);
        itemField.selectionStart = itemField.selectionEnd = selectionStart + 1;
        return false;
    }
    processRepeatedKey(ev, isDown, item, replacement) {
        if (undefined === this._controls)
            return false;
        const itemField = this._controls.itemFields[item];
        const currentValue = itemField.value;
        if (!currentValue.length || !itemField.selectionStart || !itemField.selectionEnd)
            return false;
        const selectionStart = (itemField.selectionStart > itemField.selectionEnd ? itemField.selectionEnd : itemField.selectionStart);
        if (selectionStart !== itemField.selectionEnd)
            return false;
        const selectionPrevious = selectionStart - 1;
        if (currentValue[selectionPrevious] !== (isDown ? ev.key : replacement))
            return false;
        ev.preventDefault();
        if (!isDown)
            return false;
        itemField.value = currentValue.substring(0, selectionPrevious) + replacement + currentValue.substring(selectionStart);
        itemField.selectionStart = itemField.selectionEnd = selectionPrevious + 1;
        return false;
    }
    doProcessOverrideKey(ev, isDown, item) {
        if (undefined === this._controls || !AccuDrawViewportUI.controlProps.simplifiedInput)
            return false;
        switch (ev.key) {
            case "^":
                return this.processReplacementKey(ev, isDown, item, (ItemField.ANGLE_Item === item ? "°" : ""), false); // Easier "°" input...
            case ";":
                return this.processReplacementKey(ev, isDown, item, (ItemField.ANGLE_Item === item ? "°" : ":"), false); // Easier ":" input still useful w/o MU:SU:PU?
            case ".":
                return this.processRepeatedKey(ev, isDown, item, ":"); // Still useful replacing ".." with ":" for numeric keypad users w/o MU:SU:PU?
            default:
                return false;
        }
    }
    doProcessExpressionKey(ev, isDown, item) {
        if (undefined === this._controls || !AccuDrawViewportUI.controlProps.mathOperations)
            return false;
        const itemField = this._controls.itemFields[item];
        const currentValue = itemField.value;
        switch (ev.key) {
            case "ArrowLeft":
            case "ArrowRight":
                if (undefined === this._expression || !isDown || this.isDynamicKeyinStatus(item) || !itemField.selectionStart)
                    break;
                const moveLeft = ("ArrowLeft" === ev.key);
                const operatorPosIns = currentValue.lastIndexOf(this._expression.operator);
                if (itemField.selectionStart !== (moveLeft ? operatorPosIns + this._expression.operator.length : operatorPosIns))
                    break;
                // Treat expression operator string as a single character when moving the text insertion cursor...
                itemField.selectionStart = itemField.selectionEnd = moveLeft
                    ? operatorPosIns
                    : operatorPosIns + this._expression.operator.length;
                ev.preventDefault();
                return true;
            case "Backspace":
            case "Delete":
                if (undefined === this._expression || !isDown || this.isDynamicKeyinStatus(item) || !itemField.selectionStart)
                    break;
                const deleteBefore = ("Backspace" === ev.key);
                const operatorPosDel = currentValue.lastIndexOf(this._expression.operator);
                if (itemField.selectionStart !== (deleteBefore ? operatorPosDel + this._expression.operator.length : operatorPosDel))
                    break;
                // Treat expression operator string as single character for delete...
                itemField.value = currentValue.substring(0, operatorPosDel);
                itemField.selectionStart = itemField.selectionEnd = itemField.value.length;
                this._expression = undefined;
                ev.preventDefault();
                return true;
            case " ":
                if (!isDown || !this.isDynamicKeyinStatus(item))
                    break;
                this.setPartialKeyinStatus(item, false); // Replacing current w/space isn't useful, append to end to support + or - more conveniently...
                return true;
            case "+":
            case "-":
            case "*":
            case "/":
                if (!isDown || undefined !== this._expression)
                    break;
                if (!currentValue.length || !itemField.selectionStart || itemField.selectionStart !== currentValue.length)
                    break;
                const haveSpace = (" " === currentValue[itemField.selectionStart - 1]);
                const requireSpace = ("+" === ev.key || "-" === ev.key); // These are valid for 1st character to replace current value...
                if (!(requireSpace ? haveSpace : (haveSpace || this.isDynamicKeyinStatus(item))))
                    break;
                const operator = ` ${ev.key} `;
                const expression = `${currentValue + (haveSpace ? operator.substring(1) : operator)}`;
                itemField.value = expression;
                itemField.selectionStart = itemField.selectionEnd = itemField.value.length;
                this._expression = { item, operator };
                this.setPartialKeyinStatus(item, false);
                ev.preventDefault();
                return true;
        }
        return false;
    }
    async doProcessKey(ev, isDown, item) {
        if (!this.itemFieldInputIsValid(ev.key, item)) {
            ev.preventDefault(); // Ignore potential shortcuts...
            return;
        }
        if (this.doProcessOverrideKey(ev, isDown, item))
            return;
        if (this.doProcessExpressionKey(ev, isDown, item))
            return;
        if (isDown)
            return this.setPartialKeyinStatus(item, true);
        return this.processPartialInput(item);
    }
    async onKeyboardEvent(ev, isDown) {
        if (ev.ctrlKey || ev.altKey || ev.metaKey)
            return; // Ignore qualifiers other than shift...
        switch (ev.key) {
            case "Escape":
                return this.doFocusHome(ev, isDown, this._focusItem);
            case "PageDown":
                return this.doChooseSavedValue(ev, isDown, this._focusItem, true);
            case "PageUp":
                return this.doChooseSavedValue(ev, isDown, this._focusItem, false);
            case "Tab":
                return this.doNavigate(ev, isDown, this._focusItem, !ev.shiftKey);
            case "ArrowDown":
                if (ev.shiftKey)
                    return this.doChooseSavedValue(ev, isDown, this._focusItem, true);
                return this.doNavigate(ev, isDown, this._focusItem, true);
            case "ArrowUp":
                if (ev.shiftKey)
                    return this.doChooseSavedValue(ev, isDown, this._focusItem, false);
                return this.doNavigate(ev, isDown, this._focusItem, false);
            case "Enter":
                return this.doAcceptInput(ev, isDown, this._focusItem);
            case "Home":
            case "End":
            case "Insert":
                return this.doNewInput(ev, isDown, this._focusItem);
            case "ArrowLeft":
            case "ArrowRight":
                if (this.doProcessExpressionKey(ev, isDown, this._focusItem))
                    return;
                return this.doNewInput(ev, isDown, this._focusItem);
            case "Backspace":
            case "Delete":
                if (this.doProcessExpressionKey(ev, isDown, this._focusItem))
                    return;
                return this.doDeleteInput(ev, isDown, this._focusItem);
            default:
                return this.doProcessKey(ev, isDown, this._focusItem);
        }
    }
    removeControls() {
        if (undefined === this._controls)
            return;
        this._controls.overlay.remove();
        this._controls = undefined;
        this.unsuspendToolTips();
        this.removedControlRect();
    }
    createControlDiv() {
        const div = document.createElement("div");
        div.className = "accudraw-controls";
        const style = div.style;
        style.pointerEvents = "none";
        style.position = "absolute";
        style.display = "flex";
        const isHorizontal = AccuDrawViewportUI.controlProps.horizontalArrangement;
        style.flexDirection = isHorizontal ? "row" : "column";
        if (isHorizontal) {
            // Make the space between each control group bigger than the space between fields within a group
            style.columnGap = AccuDrawViewportUI.controlProps.spacing.margin;
        }
        else {
            // Make the space between each control group equal than the space between fields within a group
            style.rowGap = AccuDrawViewportUI.controlProps.spacing.gap;
        }
        return div;
    }
    updateItemFieldKeyinStatus(itemField, item) {
        const isDynamic = this.isDynamicKeyinStatus(item);
        if (isDynamic && item === this._expression?.item)
            this._expression = undefined; // Only valid when entering partial input...
        itemField.style.caretColor = isDynamic ? "transparent" : itemField.style.color;
    }
    updateItemFieldValue(itemField, item) {
        const value = this.getFormattedValueByIndex(item);
        itemField.value = value;
        this.updateItemFieldKeyinStatus(itemField, item);
    }
    updateItemFieldLock(itemLock, item) {
        const locked = this.getFieldLock(item);
        itemLock.style.backgroundColor = locked
            ? AccuDrawViewportUI.controlProps.button.locked.backgroundColor
            : AccuDrawViewportUI.controlProps.button.unlocked.backgroundColor;
        itemLock.style.border = `${AccuDrawViewportUI.controlProps.field.border.width} ${AccuDrawViewportUI.controlProps.field.border.style} ${locked
            ? AccuDrawViewportUI.controlProps.button.locked.border.color
            : AccuDrawViewportUI.controlProps.button.unlocked.border.color}`;
        itemLock.style.color = locked
            ? AccuDrawViewportUI.controlProps.button.locked.color
            : AccuDrawViewportUI.controlProps.button.unlocked.color;
    }
    initializeItemStyle(style, isButton) {
        style.pointerEvents = "none"; // Don't receive pointer events...
        style.textWrap = "nowrap";
        style.textAnchor = "top";
        style.boxSizing = "border-box";
        const controlProps = AccuDrawViewportUI.controlProps;
        style.height = controlProps.field.height;
        const baseBorder = `${controlProps.field.border.width} ${controlProps.field.border.style} `;
        switch (isButton) {
            case true:
                style.display = "flex";
                style.justifyContent = "center";
                style.alignItems = "center";
                style.backgroundColor = controlProps.button.unlocked.backgroundColor;
                style.padding = controlProps.button.padding;
                style.border = baseBorder + controlProps.button.unlocked.border.color;
                style.color = controlProps.button.unlocked.color;
                style.aspectRatio = "1";
                break;
            case false:
                style.backgroundColor = controlProps.input.unfocused.backgroundColor;
                /**
                 * StrataKit resets the `outline` style here: https://github.com/iTwin/stratakit/blob/d08ecb28ca4e304094489522eb8e17c9a10865b3/packages/foundations/src/~unlayered.css#L10
                 * This causes the input to have a white outline when focused unless we override it again with `!important`.
                 * Note: `style.outline = "none !important"` does not work because the CSSOM property setter ignores the `!important` flag; the `priority` must be passed via `CSSStyleDeclaration.setProperty`.
                 * See MDN: https://developer.mozilla.org/docs/Web/API/CSSStyleDeclaration/setProperty
                 */
                style.setProperty("outline", "none", "important");
                style.padding = controlProps.input.padding;
                style.border = baseBorder + controlProps.input.unfocused.border.color;
                style.color = controlProps.input.color;
                style.width = "120px";
                break;
        }
        style.fontFamily = controlProps.field.text.fontFamily;
        style.fontSize = controlProps.field.text.fontSize;
        style.borderRadius = controlProps.field.border.radius;
    }
    createItemField(item) {
        const itemField = document.createElement("input");
        itemField.contentEditable = "true";
        itemField.size = AccuDrawViewportUI.controlProps.field.size;
        const style = itemField.style;
        this.initializeItemStyle(style, false);
        this.updateItemFieldValue(itemField, item);
        itemField.onkeydown = async (ev) => { await this.onKeyboardEvent(ev, true); };
        itemField.onkeyup = async (ev) => { await this.onKeyboardEvent(ev, false); };
        itemField.onfocus = (ev) => { this.onFocusChange(ev, item, true); };
        itemField.onblur = (ev) => { this.onFocusChange(ev, item, false); };
        return itemField;
    }
    createItemFieldLock(item) {
        const itemLock = document.createElement("button");
        itemLock.type = "button";
        itemLock.contentEditable = "false";
        itemLock.disabled = true; // Don't receive focus...
        switch (item) {
            case ItemField.DIST_Item:
                itemLock.innerHTML = "\u21A6"; // right arrow from bar...
                break;
            case ItemField.ANGLE_Item:
                itemLock.innerHTML = "\u2221"; // measured angle...
                break;
            case ItemField.X_Item:
                itemLock.innerHTML = "X";
                break;
            case ItemField.Y_Item:
                itemLock.innerHTML = "Y";
                break;
            case ItemField.Z_Item:
                itemLock.innerHTML = "Z";
                break;
        }
        const style = itemLock.style;
        this.initializeItemStyle(style, true);
        this.updateItemFieldLock(itemLock, item);
        return itemLock;
    }
    /** Called after the controls have been removed from the view. */
    removedControlRect() { }
    /** Called after the position of the controls in the supplied view is updated. */
    changedControlRect(_rect, _vp) { }
    /** Use to override the position of the controls in the supplied view. */
    modifyControlRect(_rect, _vp) { }
    /** Return the ViewRect currently occupied by the controls in the supplied view. */
    currentControlRect(vp) {
        if (undefined === this._controls || this._controls.overlay.parentElement !== vp.vpDiv)
            return undefined;
        const viewRect = vp.vpDiv.getBoundingClientRect();
        const elemRect = this._controls.div.getBoundingClientRect();
        const controlRect = new ViewRect(elemRect.left - viewRect.left, elemRect.top - viewRect.top, elemRect.right - viewRect.left, elemRect.bottom - viewRect.top);
        return controlRect;
    }
    updateControlVisibility(isPolar, is3d) {
        if (undefined === this._controls)
            return;
        const angleWrapper = this._controls.itemFields[ItemField.ANGLE_Item]
            .parentElement;
        const distWrapper = this._controls.itemFields[ItemField.DIST_Item]
            .parentElement;
        const xWrapper = this._controls.itemFields[ItemField.X_Item]
            .parentElement;
        const yWrapper = this._controls.itemFields[ItemField.Y_Item]
            .parentElement;
        const zWrapper = this._controls.itemFields[ItemField.Z_Item]
            .parentElement;
        angleWrapper.style.display = !isPolar ? "none" : "flex";
        distWrapper.style.display = !isPolar ? "none" : "flex";
        xWrapper.style.display = isPolar ? "none" : "flex";
        yWrapper.style.display = isPolar ? "none" : "flex";
        if (undefined === is3d)
            return;
        zWrapper.style.display = !is3d ? "none" : "flex";
    }
    updateControls(ev) {
        const vp = ev.viewport;
        if (undefined === vp || !this.isActive)
            return;
        if (undefined !== this._controls && this._controls.overlay.parentElement !== vp.vpDiv)
            this.removeControls(); // Could be enhanced to save/restore partial input of currently focused item...
        const props = AccuDrawViewportUI.controlProps;
        if (undefined === this._controls) {
            const overlay = vp.addNewDiv("accudraw-overlay", true, 35);
            const div = this.createControlDiv();
            overlay.appendChild(div);
            const createFieldAndLock = (item) => {
                const fieldWrapper = document.createElement("div");
                fieldWrapper.style.display = "flex";
                fieldWrapper.style.flexDirection = "row";
                fieldWrapper.style.alignItems = "center";
                fieldWrapper.style.justifyContent = "center";
                fieldWrapper.style.columnGap = AccuDrawViewportUI.controlProps.spacing.gap;
                fieldWrapper.style.rowGap = AccuDrawViewportUI.controlProps.spacing.gap;
                const itemField = (itemFields[item] = this.createItemField(item));
                fieldWrapper.appendChild(itemField);
                const itemLock = (itemLocks[item] = this.createItemFieldLock(item));
                fieldWrapper.appendChild(itemLock);
                div.appendChild(fieldWrapper);
            };
            const itemFields = [];
            const itemLocks = [];
            createFieldAndLock(ItemField.DIST_Item);
            createFieldAndLock(ItemField.ANGLE_Item);
            createFieldAndLock(ItemField.X_Item);
            createFieldAndLock(ItemField.Y_Item);
            createFieldAndLock(ItemField.Z_Item); // Both polar and rectangular modes support Z in 3d views...
            this._controls = { overlay, div, itemFields, itemLocks };
            this.updateControlVisibility(CompassMode.Polar === this.compassMode, this.is3dCompass(vp));
            this.setFocusItem(this._focusItem);
            this.suspendToolTips();
            vp.onChangeView.addOnce(() => this.removeControls()); // Clear on view change/closure...
        }
        const viewRect = vp.viewRect;
        const position = vp.worldToView(ev.point);
        if (props.fixedLocation) {
            position.x = (viewRect.left + ((viewRect.width - this._controls.div.offsetWidth) * 0.5));
            position.y = (viewRect.bottom - this._controls.div.offsetHeight * 1.2);
        }
        else {
            position.x += Math.floor(vp.pixelsFromInches(props.cursorOffset.x)) + 0.5;
            position.y += Math.floor(vp.pixelsFromInches(props.cursorOffset.y)) + 0.5;
        }
        const controlRect = new ViewRect(position.x, position.y, position.x + this._controls.div.offsetWidth, position.y + this._controls.div.offsetHeight);
        this.modifyControlRect(controlRect, vp);
        if (!controlRect.isContained(viewRect))
            return; // Keep showing at last valid location...
        this._controls.div.style.left = `${controlRect.left}px`;
        this._controls.div.style.top = `${controlRect.top}px`;
        this.changedControlRect(controlRect, vp);
        return;
    }
    get _isFocusHome() {
        return (document.body === document.activeElement);
    }
    get _isFocusAccuDraw() {
        return (undefined !== this._controls && document.activeElement?.parentElement === this._controls.itemFields[this._focusItem].parentElement);
    }
    setFocusHome() {
        const element = document.activeElement;
        if (element && element !== document.body)
            element.blur();
        document.body.focus();
    }
    /** Return whether keyboard shortcuts can or can't be used.
     * Used to show a visual indication of whether keyboard shortcuts will be processed.
     * Keyboard shortcuts can be supported when focus is either on AccuDraw or Home.
     * When returning false the compass displays in monochrome.
     */
    get hasInputFocus() {
        // Indicate when keyboard shortcuts can't be used (i.e. focus not at AccuDraw or Home) by changing compass to monochrome...
        return (this._isFocusHome || this._isFocusAccuDraw);
    }
    /** Get the item field that currently has input focus.
     */
    getFocusItem() {
        if (!this._isFocusAccuDraw)
            return undefined;
        return this._focusItem;
    }
    /** Request to set focus to the specified AccuDraw input field to start entering values.
     * The focused input field will be indicated by the background color.
     */
    setFocusItem(index) {
        this._focusItem = index;
        if (undefined === this._controls)
            return;
        const itemField = this._controls.itemFields[this._focusItem];
        itemField.focus();
    }
    /** Request to set focus to the active AccuDraw input field to start entering values.
     * The focused input field will be indicated by the background color.
     */
    grabInputFocus() {
        // Set focus to active input field for entering values...
        if (this._isFocusAccuDraw)
            return;
        this.setFocusItem(this._focusItem);
    }
    onFocusChange(_ev, item, focusIn) {
        if (undefined === this._controls)
            return;
        // NOTE: Using "setSelectionRange" while value is changing in dynamics isn't pretty, use background+caret color instead...
        const itemField = this._controls.itemFields[item];
        itemField.style.backgroundColor = focusIn
            ? AccuDrawViewportUI.controlProps.input.focused.backgroundColor
            : AccuDrawViewportUI.controlProps.input.unfocused.backgroundColor;
        itemField.style.border = focusIn
            ? `${AccuDrawViewportUI.controlProps.field.border.width} ${AccuDrawViewportUI.controlProps.field.border.style} ${AccuDrawViewportUI.controlProps.input.focused.border.color}`
            : `${AccuDrawViewportUI.controlProps.field.border.width} ${AccuDrawViewportUI.controlProps.field.border.style} ${AccuDrawViewportUI.controlProps.input.unfocused.border.color}`;
        itemField.style.boxShadow = focusIn
            ? AccuDrawViewportUI.controlProps.input.focused.innerStroke
            : "none";
        itemField.style.color = focusIn
            ? AccuDrawViewportUI.controlProps.input.focused.color
            : AccuDrawViewportUI.controlProps.input.color;
        this.updateItemFieldKeyinStatus(itemField, item);
        if (!focusIn)
            this.setDynamicKeyinStatus(item);
    }
    /** Change notification for when the compass is shown or hidden.
     * Used to hide the viewport controls when the compass is no longer displayed.
     */
    onCompassDisplayChange(state) {
        if ("show" === state)
            return;
        this.removeControls();
    }
    /** Change notification for when the compass mode switches between polar and rectangular inputs.
     * Used to show or hide the input fields that are applicable to the current mode.
     */
    onCompassModeChange() {
        this.updateControlVisibility(CompassMode.Polar === this.compassMode);
        this.setFocusItem(this.defaultFocusItem());
    }
    /** Change notification for when the supplied input field switches between dynamic and partial input.
     * When an input field is in the dynamic state, its value changes according to the current button event unless the field is locked.
     * When an input field is in the partial state, its value is not changed or formatted to allow the value to be changed.
     * Locking a field is expected to change the input state to partial.
     * Unlocking a field or accepting a value by focusing is expected to change the input state to dynamic.
     */
    onFieldKeyinStatusChange(item) {
        if (undefined === this._controls)
            return;
        this.updateItemFieldKeyinStatus(this._controls.itemFields[item], item);
    }
    /** Change notification for when the supplied input field value has been modified.
     * Used to update the displayed input field with the value from the active angle or distance formatter.
     */
    onFieldValueChange(item) {
        if (undefined === this._controls)
            return;
        this.updateItemFieldValue(this._controls.itemFields[item], item);
    }
    /** Change notification for when the supplied input field lock status is modified.
     * Used to update the displayed lock toggles to reflect the current state.
     */
    onFieldLockChange(item) {
        if (undefined === this._controls)
            return;
        this.updateItemFieldLock(this._controls.itemLocks[item], item);
    }
    /** Change notification of a motion event in the view.
     * Used to show as well as update the dynamic input field values to reflect the current deltas when active.
     * Automatically switches the focused input field between x and y in rectangular mode based on
     * cursor position when axis isn't locked to support more intuitive user input and "smart lock" keyboard shortcut.
     */
    onMotion(ev) {
        this.updateControls(ev);
        this.processMotion();
    }
}
//# sourceMappingURL=AccuDrawViewportUI.js.map