UNPKG

@itwin/core-frontend

Version:
728 lines 36.4 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.AccuDrawViewportUI = void 0; /** @packageDocumentation * @module AccuDraw */ const core_quantity_1 = require("@itwin/core-quantity"); const AccuDraw_1 = require("../AccuDraw"); const ViewRect_1 = require("../common/ViewRect"); const IModelApp_1 = require("../IModelApp"); const AccuDrawTool_1 = require("./AccuDrawTool"); const Tool_1 = require("./Tool"); /** Provides an in viewport user interface for AccuDraw that can optionally follow the cursor or remain at a fixed location. * @beta */ class AccuDrawViewportUI extends AccuDraw_1.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, /** Number of visible characters to show in text input fields. */ fieldSize: 12, /** Row spacing of text input fields for vertical arrangement. */ rowSpacingFactor: 1.2, /** Column spacing of text input fields and buttons for horizontal arrangement. */ columnSpacingFactor: 1.1, /** Corner radius of text input fields and locks buttons. */ borderRadius: "0.5em", /** Background color of unfocused text input fields and unlocked buttons. */ backgroundColor: "rgba(150, 150, 150, 0.5)", /** Settings specific to text input fields and lock button labels. */ text: { /** Font family to use for text input field values and button labels. */ fontFamily: "sans-serif", /** Font size to use for text input field values and button labels. */ fontSize: "9pt", /** Font color to use for text input field values and button labels. */ color: "white", /** Background color of focused text input field. */ focusColor: "rgba(50, 50, 200, 0.75)", }, /** Settings specific to lock buttons. */ button: { /** Background color of locked buttons. */ pressedColor: "rgba(50, 50, 50, 0.75)", /** Margin to use on left and right to position relative to text input field. */ margin: "0.25em", /** Width of border outline. */ outlineWidth: "thin", /** Shadow shown when unlocked to make it appear raised. */ shadow: "0.25em 0.25em 0.2em rgb(75, 75, 75)", }, }; /** 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 Tool_1.BeButtonEvent(); IModelApp_1.IModelApp.toolAdmin.fillEventFromCursorLocation(ev); this.updateControls(ev); } suspendToolTips() { if (!AccuDrawViewportUI.controlProps.suspendLocateToolTip || undefined === (this._toolTipsSuspended = (IModelApp_1.IModelApp.accuSnap.userSettings.toolTip ? true : undefined))) return; IModelApp_1.IModelApp.accuSnap.userSettings.toolTip = false; IModelApp_1.IModelApp.notifications.clearToolTip(); } unsuspendToolTips() { if (undefined === this._toolTipsSuspended) return; IModelApp_1.IModelApp.accuSnap.userSettings.toolTip = true; this._toolTipsSuspended = undefined; } setDynamicKeyinStatus(item) { // This does nothing if keyin status is already dynamic... AccuDrawTool_1.AccuDrawShortcuts.itemFieldCompletedInput(item); } setPartialKeyinStatus(item, selectAll) { if (!this.isDynamicKeyinStatus(item)) return; AccuDrawTool_1.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 (!core_quantity_1.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 (!core_quantity_1.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_1.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_1.IModelApp.toolAdmin.simulateMotionEvent(); return; } const isAngle = (AccuDraw_1.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 = (AccuDraw_1.ItemField.ANGLE_Item === item); const expressionValue = this.parseExpression(currentValue, isAngle); if (undefined === forward) return AccuDrawTool_1.AccuDrawShortcuts.itemFieldAcceptInput(item, expressionValue ?? currentValue); return AccuDrawTool_1.AccuDrawShortcuts.itemFieldNavigate(item, expressionValue ?? currentValue, forward); } acceptSavedValue(item, next) { if (next) AccuDrawTool_1.AccuDrawShortcuts.chooseNextValue(item); else AccuDrawTool_1.AccuDrawShortcuts.choosePreviousValue(item); } doFocusHome(ev, isDown, _item) { ev.preventDefault(); if (!isDown || this._isFocusHome) return; ev.stopPropagation(); this.setFocusHome(); IModelApp_1.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, (AccuDraw_1.ItemField.ANGLE_Item === item ? "°" : ""), false); // Easier "°" input... case ";": return this.processReplacementKey(ev, isDown, item, (AccuDraw_1.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.overflow = "visible"; // Don't clip/hide outline or shadow... style.position = "absolute"; style.top = style.left = "0"; style.height = style.width = "100%"; 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.outlineStyle = locked ? "inset" : "outset"; itemLock.style.boxShadow = locked ? "none" : AccuDrawViewportUI.controlProps.button.shadow; itemLock.style.backgroundColor = locked ? AccuDrawViewportUI.controlProps.button.pressedColor : AccuDrawViewportUI.controlProps.backgroundColor; } initializeItemStyle(style, isButton) { style.pointerEvents = "none"; // Don't receive pointer events... style.position = "absolute"; style.textWrap = "nowrap"; style.textAnchor = "top"; style.textAlign = isButton ? "center" : "left"; const controlProps = AccuDrawViewportUI.controlProps; style.fontFamily = controlProps.text.fontFamily; style.fontSize = controlProps.text.fontSize; style.color = controlProps.text.color; style.backgroundColor = controlProps.backgroundColor; style.borderRadius = controlProps.borderRadius; } createItemField(item) { const itemField = document.createElement("input"); itemField.contentEditable = "true"; itemField.size = AccuDrawViewportUI.controlProps.fieldSize; 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 AccuDraw_1.ItemField.DIST_Item: itemLock.innerHTML = "\u21A6"; // right arrow from bar... break; case AccuDraw_1.ItemField.ANGLE_Item: itemLock.innerHTML = "\u2221"; // measured angle... break; case AccuDraw_1.ItemField.X_Item: itemLock.innerHTML = "X"; break; case AccuDraw_1.ItemField.Y_Item: itemLock.innerHTML = "Y"; break; case AccuDraw_1.ItemField.Z_Item: itemLock.innerHTML = "Z"; break; } const style = itemLock.style; this.initializeItemStyle(style, true); this.updateItemFieldLock(itemLock, item); const button = AccuDrawViewportUI.controlProps.button; style.paddingLeft = style.paddingRight = "0"; style.marginLeft = style.marginRight = button.margin; style.outlineWidth = button.outlineWidth; 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_1.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; this._controls.itemFields[AccuDraw_1.ItemField.ANGLE_Item].hidden = !isPolar; this._controls.itemLocks[AccuDraw_1.ItemField.ANGLE_Item].hidden = !isPolar; this._controls.itemFields[AccuDraw_1.ItemField.DIST_Item].hidden = !isPolar; this._controls.itemLocks[AccuDraw_1.ItemField.DIST_Item].hidden = !isPolar; this._controls.itemFields[AccuDraw_1.ItemField.X_Item].hidden = isPolar; this._controls.itemLocks[AccuDraw_1.ItemField.X_Item].hidden = isPolar; this._controls.itemFields[AccuDraw_1.ItemField.Y_Item].hidden = isPolar; this._controls.itemLocks[AccuDraw_1.ItemField.Y_Item].hidden = isPolar; if (undefined === is3d) return; this._controls.itemFields[AccuDraw_1.ItemField.Z_Item].hidden = !is3d; this._controls.itemLocks[AccuDraw_1.ItemField.Z_Item].hidden = !is3d; } 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(); const is3dLayout = this.is3dCompass(vp); const isHorizontalLayout = props.horizontalArrangement; overlay.appendChild(div); const createFieldAndLock = (item) => { const itemField = itemFields[item] = this.createItemField(item); itemField.style.top = isHorizontalLayout ? "0" : `${rowOffset}px`; itemField.style.left = isHorizontalLayout ? `${columnOffset}px` : "0"; div.appendChild(itemField); if (is3dLayout || AccuDraw_1.ItemField.Z_Item !== item) rowOffset += itemField.offsetHeight * props.rowSpacingFactor; itemWidth = itemField.offsetWidth; itemHeight = itemField.offsetHeight; const itemLock = itemLocks[item] = this.createItemFieldLock(item); itemLock.style.top = itemField.style.top; itemLock.style.left = isHorizontalLayout ? `${columnOffset + itemWidth}px` : `${itemWidth}px`; itemLock.style.width = itemLock.style.height = `${itemHeight}px`; // Make square of same height as text field... div.appendChild(itemLock); lockWidth = itemLock.offsetWidth; if (is3dLayout || AccuDraw_1.ItemField.Z_Item !== item) columnOffset += (itemWidth + lockWidth) * props.columnSpacingFactor; }; let rowOffset = 0; let columnOffset = 0; let itemWidth = 0; let itemHeight = 0; let lockWidth = 0; const itemFields = []; const itemLocks = []; createFieldAndLock(AccuDraw_1.ItemField.DIST_Item); createFieldAndLock(AccuDraw_1.ItemField.ANGLE_Item); rowOffset = 0; columnOffset = 0; createFieldAndLock(AccuDraw_1.ItemField.X_Item); createFieldAndLock(AccuDraw_1.ItemField.Y_Item); createFieldAndLock(AccuDraw_1.ItemField.Z_Item); // Both polar and rectangular modes support Z in 3d views... div.style.width = isHorizontalLayout ? `${columnOffset}px` : `${itemWidth + lockWidth + 5}px`; div.style.height = isHorizontalLayout ? `${itemHeight * props.rowSpacingFactor}px` : `${rowOffset}px`; this._controls = { overlay, div, itemFields, itemLocks }; this.updateControlVisibility(AccuDraw_1.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); } 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_1.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.text.focusColor : AccuDrawViewportUI.controlProps.backgroundColor); 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(AccuDraw_1.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(); } } exports.AccuDrawViewportUI = AccuDrawViewportUI; //# sourceMappingURL=AccuDrawViewportUI.js.map