Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions companion/lib/Graphics/ConvertGraphicsElements/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { JsonValue } from 'type-fest'
import type { ExecuteExpressionResult } from '@companion-app/shared/Expression/ExpressionResult.js'
import type { HorizontalAlignment, VerticalAlignment } from '@companion-app/shared/Graphics/Util.js'
import type { ControlLocation } from '@companion-app/shared/Model/Common.js'
import type { ExpressionOrValue } from '@companion-app/shared/Model/Options.js'
import { isExpressionOrValue, type ExpressionOrValue } from '@companion-app/shared/Model/Options.js'
import type { DrawImageBuffer } from '@companion-app/shared/Model/StyleModel.js'
import {
stringifyVariableValue,
Expand Down Expand Up @@ -77,7 +77,7 @@ export class ElementExpressionHelper<T> {

#getValue(propertyName: keyof T): ExpressionOrValue<JsonValue | undefined> {
const override = this.#elementOverrides?.get(String(propertyName))
return override ? override : (this.#element as any)[propertyName]
return override ?? (this.#element as any)[propertyName] ?? { isExpression: false, value: undefined }
}

getUnknown(propertyName: keyof T, defaultValue: VariableValue): VariableValue | undefined {
Expand All @@ -96,7 +96,7 @@ export class ElementExpressionHelper<T> {
getParsedString(propertyName: keyof T, defaultValue: string): string {
const value = this.#getValue(propertyName)
if (value.isExpression) {
return stringifyVariableValue(this.getUnknown(propertyName, defaultValue)) ?? ''
return stringifyVariableValue(this.getUnknown(propertyName, defaultValue)) ?? defaultValue
} else {
return this.parseVariablesInString(stringifyVariableValue(value.value) ?? '', defaultValue)
}
Expand All @@ -105,14 +105,20 @@ export class ElementExpressionHelper<T> {
getNumber(propertyName: keyof T, defaultValue: number, scale = 1): number {
const value = this.#getValue(propertyName)

if (!value.isExpression) return Number(value.value) * scale
if (!value.isExpression) {
const num = Number(value.value)
return isNaN(num) ? defaultValue : num * scale
}

const result = this.executeExpressionAndTrackVariables(value.value, 'number')
if (!result.ok) {
return defaultValue
}

return Number(result.value) * scale
// Number(undefined) = NaN and typeof NaN === 'number', so ok:true can still yield NaN
// (e.g. when a referenced variable doesn't exist). Treat NaN as a missing value.
const num = Number(result.value)
return isNaN(num) ? defaultValue : num * scale
}

getString<TVal extends string | null | undefined>(propertyName: keyof T, defaultValue: TVal): TVal {
Expand All @@ -123,16 +129,15 @@ export class ElementExpressionHelper<T> {
return stringifyVariableValue(value.value) as TVal
}

const result = this.executeExpressionAndTrackVariables(value.value, 'string')
const result = this.executeExpressionAndTrackVariables(value.value, undefined)
if (!result.ok) {
return defaultValue
}

if (typeof result.value !== 'string') {
return defaultValue
}

return result.value as TVal
// stringifyVariableValue returns undefined only when result.value is undefined
// (e.g. the referenced variable doesn't exist). Treat that as a missing value.
const stringified = stringifyVariableValue(result.value)
return (stringified ?? defaultValue) as TVal
}

getEnum<TVal extends string | number>(propertyName: keyof T, values: TVal[], defaultValue: TVal): TVal {
Expand All @@ -157,6 +162,18 @@ export class ElementExpressionHelper<T> {
return actualValue
}

/**
* Like getEnum, but compares the string value to the enum values in a tolerant way:
* Matches only the first non-whitespace character, case-insensitively.
*/
getTolerantEnum<TVal extends string>(propertyName: keyof T, values: readonly TVal[], defaultValue: TVal): TVal {
const raw = this.getString(propertyName, defaultValue)
const trimmed = String(raw ?? '')
.trim()
.toLowerCase()
return values.find((v) => v.toLowerCase().startsWith(trimmed[0])) ?? defaultValue
}

getBoolean(propertyName: keyof T, defaultValue: boolean): boolean {
const value = this.#getValue(propertyName)

Expand Down Expand Up @@ -194,6 +211,21 @@ export class ElementExpressionHelper<T> {
return 'center'
}
}

/**
* Create a helper for a row of an internal:list or internal:table (or other child object)
*/
forRow(row: unknown): ElementExpressionHelper<Record<string, ExpressionOrValue<JsonValue | undefined>>> {
const normalised: Record<string, ExpressionOrValue<JsonValue | undefined>> = {}
if (row && typeof row === 'object' && !Array.isArray(row)) {
for (const key of Object.keys(row)) {
const val = (row as Record<string, unknown>)[key]
normalised[key] = isExpressionOrValue(val) ? val : { isExpression: false, value: val as JsonValue | undefined }
}
}
return new ElementExpressionHelper(this.#parser, this.#usedVariables, normalised, undefined)
}

getVerticalAlignment(propertyName: keyof T): VerticalAlignment {
const value = this.#getValue(propertyName)

Expand Down
Loading
Loading