UNPKG

@itwin/core-frontend

Version:
812 lines 40.7 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, /** 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-white, 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: { /** 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-muted, #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 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.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; style.outline = "none"; 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 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); 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; const angleWrapper = this._controls.itemFields[AccuDraw_1.ItemField.ANGLE_Item] .parentElement; const distWrapper = this._controls.itemFields[AccuDraw_1.ItemField.DIST_Item] .parentElement; const xWrapper = this._controls.itemFields[AccuDraw_1.ItemField.X_Item] .parentElement; const yWrapper = this._controls.itemFields[AccuDraw_1.ItemField.Y_Item] .parentElement; const zWrapper = this._controls.itemFields[AccuDraw_1.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(AccuDraw_1.ItemField.DIST_Item); createFieldAndLock(AccuDraw_1.ItemField.ANGLE_Item); 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... 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 * 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_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.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"; 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