diff --git a/companion/lib/Controls/ControlTypes/Button/Base.ts b/companion/lib/Controls/ControlTypes/Button/Base.ts index 1931795ad9..9ab5a5af1a 100644 --- a/companion/lib/Controls/ControlTypes/Button/Base.ts +++ b/companion/lib/Controls/ControlTypes/Button/Base.ts @@ -23,7 +23,7 @@ import { DrawStyleButtonStateProps } from '@companion-app/shared/Model/StyleMode * Individual Contributor License Agreement for Companion along with * this program. */ -export abstract class ButtonControlBase> +export abstract class ButtonControlBase extends ControlBase implements ControlWithOptions, ControlWithPushed { @@ -34,7 +34,10 @@ export abstract class ButtonControlBase + deps.variables.values.executeExpression( + expression, + deps.page.getLocationOfControlId(this.controlId), + requiredType, + injectedVariableValues + ), }, this.sendRuntimePropsChange.bind(this) ) @@ -196,17 +206,14 @@ export abstract class ButtonControlBase 1) { - result.step_cycle = this.entities.getActiveStepIndex() + 1 - } - return result } @@ -244,7 +251,7 @@ export abstract class ButtonControlBase this.postProcessImport()) @@ -213,6 +214,8 @@ export class ControlButtonNormal * @param allChangedVariables - variables with changes */ onVariablesChanged(allChangedVariables: Set): void { + this.entities.stepCheckExpressionOnVariablesChanged(allChangedVariables) + if (this.#last_draw_variables) { for (const variable of allChangedVariables.values()) { if (this.#last_draw_variables.has(variable)) { @@ -225,6 +228,19 @@ export class ControlButtonNormal } } + /** + * Update an option field of this control + */ + optionsSetField(key: string, value: any): boolean { + const changed = super.optionsSetField(key, value) + + if (key === 'stepProgression' || key === 'stepExpression') { + this.entities.stepExpressionUpdate(this.options) + } + + return changed + } + /** * Update the style fields of this control * @param diff - config diff to apply diff --git a/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts b/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts index b636f552f2..22d8f3cc51 100644 --- a/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts +++ b/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts @@ -150,6 +150,8 @@ export class ControlTrigger instanceDefinitions: deps.instance.definitions, internalModule: deps.internalModule, moduleHost: deps.instance.moduleHost, + executeExpressionInControl: (expression, requiredType, injectedVariableValues) => + deps.variables.values.executeExpression(expression, null, requiredType, injectedVariableValues), }) this.#eventBus = eventBus diff --git a/companion/lib/Controls/Entities/EntityListPoolBase.ts b/companion/lib/Controls/Entities/EntityListPoolBase.ts index 261ac88189..b39aacd03b 100644 --- a/companion/lib/Controls/Entities/EntityListPoolBase.ts +++ b/companion/lib/Controls/Entities/EntityListPoolBase.ts @@ -12,7 +12,9 @@ import type { ModuleHost } from '../../Instance/Host.js' import type { InternalController } from '../../Internal/Controller.js' import { isEqual } from 'lodash-es' import type { InstanceDefinitionsForEntity } from './Types.js' -import { ButtonStyleProperties } from '@companion-app/shared/Model/StyleModel.js' +import type { ButtonStyleProperties } from '@companion-app/shared/Model/StyleModel.js' +import type { CompanionVariableValues } from '@companion-module/base' +import type { ExecuteExpressionResult } from '../../Variables/Util.js' export interface ControlEntityListPoolProps { instanceDefinitions: InstanceDefinitionsForEntity @@ -21,6 +23,11 @@ export interface ControlEntityListPoolProps { controlId: string commitChange: (redraw?: boolean) => void triggerRedraw: () => void + executeExpressionInControl: ( + expression: string, + requiredType?: string, + injectedVariableValues?: CompanionVariableValues + ) => ExecuteExpressionResult } export abstract class ControlEntityListPoolBase { diff --git a/companion/lib/Controls/Entities/EntityListPoolButton.ts b/companion/lib/Controls/Entities/EntityListPoolButton.ts index 1c268f30e1..8bff641e1b 100644 --- a/companion/lib/Controls/Entities/EntityListPoolButton.ts +++ b/companion/lib/Controls/Entities/EntityListPoolButton.ts @@ -1,4 +1,4 @@ -import { NormalButtonModel, NormalButtonSteps } from '@companion-app/shared/Model/ButtonModel.js' +import { NormalButtonModel, NormalButtonOptions, NormalButtonSteps } from '@companion-app/shared/Model/ButtonModel.js' import { EntityModelType, SomeEntityModel, @@ -13,6 +13,21 @@ import type { ControlActionSetAndStepsManager } from './ControlActionSetAndSteps import { cloneDeep } from 'lodash-es' import { validateActionSetId } from '@companion-app/shared/ControlId.js' import type { ControlEntityInstance } from './EntityInstance.js' +import { assertNever } from '@companion-app/shared/Util.js' + +interface CurrentStepFromExpression { + type: 'expression' + + expression: string + + lastStepId: string + lastVariables: Set +} +interface CurrentStepFromId { + type: 'id' + + id: string +} export class ControlEntityListPoolButton extends ControlEntityListPoolBase implements ControlActionSetAndStepsManager { /** @@ -26,22 +41,32 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple readonly #steps = new Map() + readonly #executeExpressionInControl: ControlEntityListPoolProps['executeExpressionInControl'] readonly #sendRuntimePropsChange: () => void /** - * The id of the currently selected (next to be executed) step + * The current step */ - #current_step_id: string = '0' + #currentStep: CurrentStepFromExpression | CurrentStepFromId = { type: 'id', id: '0' } #hasRotaryActions = false get currentStepId(): string { - return this.#current_step_id + switch (this.#currentStep.type) { + case 'id': + return this.#currentStep.id + case 'expression': + return this.#currentStep.lastStepId + default: + assertNever(this.#currentStep) + throw new Error('Unsupported step mode') + } } constructor(props: ControlEntityListPoolProps, sendRuntimePropsChange: () => void) { super(props) + this.#executeExpressionInControl = props.executeExpressionInControl this.#sendRuntimePropsChange = sendRuntimePropsChange this.#feedbacks = new ControlEntityList( @@ -55,7 +80,7 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple } ) - this.#current_step_id = '0' + this.#currentStep = { type: 'id', id: '0' } this.#steps.set('0', this.#getNewStepValue(null, null)) } @@ -70,7 +95,7 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple this.#steps.set(id, this.#getNewStepValue(stepObj.action_sets, stepObj.options)) } - this.#current_step_id = this.getStepIds()[0] + this.#currentStep = { type: 'id', id: this.getStepIds()[0] } // TODO - other modes? } /** @@ -336,10 +361,96 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple * @returns The index of current step */ getActiveStepIndex(): number { - const out = this.getStepIds().indexOf(this.#current_step_id) + const out = this.getStepIds().indexOf(this.currentStepId) return out !== -1 ? out : 0 } + /** + * Propogate variable changes, and update the current step if the variables affect it + */ + stepCheckExpressionOnVariablesChanged(changedVariables: Set): void { + if (this.#currentStep.type !== 'expression') return + + for (const variableName of this.#currentStep.lastVariables) { + if (changedVariables.has(variableName)) { + if (this.#stepCheckExpression(true)) { + // Something changed, so redraw + this.triggerRedraw() + } + return + } + } + } + + /** + * Re-execute the expression, and update the current step if it has changed + * @param updateClient Whether to inform the client if the step changed + * @returns Whether a change was made + */ + #stepCheckExpression(updateClient: boolean): boolean { + if (this.#currentStep.type !== 'expression') return false + + let changed = false + + const stepIds = this.getStepIds() + + const latestValue = this.#executeExpressionInControl(this.#currentStep.expression, 'number') + if (latestValue.ok) { + let latestIndex = Math.max(Math.min(Number(latestValue.value) - 1, stepIds.length - 1), 0) + if (isNaN(latestIndex)) latestIndex = 0 + + const newStepId = stepIds[latestIndex] + + // Check if this will change the expected state + changed = this.#currentStep.lastStepId !== newStepId + + // Update the state + this.#currentStep.lastStepId = newStepId + this.#currentStep.lastVariables = latestValue.variableIds + } else { + // Lets always go to the first step, to ensure we have a sane and predictable value + + this.logger.warn(`Step expression failed to evaluate: ${latestValue.error}`) + + const firstStepId = stepIds[0] + + // Check if this will change the expected state + changed = this.#currentStep.lastStepId !== firstStepId + + // Update the state + this.#currentStep.lastStepId = firstStepId + this.#currentStep.lastVariables = latestValue.variableIds + } + + // Inform clients of the change + if (changed && updateClient) this.#sendRuntimePropsChange() + + return changed + } + + /** + * Update the step operation mode or expression upon button options change + * @param options + */ + stepExpressionUpdate(options: NormalButtonOptions): void { + if (options.stepProgression === 'expression') { + // It may have changed, assume it has and purge the existing state + this.#currentStep = { + type: 'expression', + expression: options.stepExpression || '', + lastStepId: this.getStepIds()[0], + lastVariables: new Set(), + } + + this.#stepCheckExpression(true) + } else { + if (this.#currentStep.type === 'expression') { + // Stick to whatever is currently selected + this.#currentStep = { type: 'id', id: this.currentStepId } + } + } + } + /** * Add a step to this control * @returns Id of new step @@ -348,24 +459,17 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple const existingKeys = this.getStepIds() .map((k) => Number(k)) .filter((k) => !isNaN(k)) - if (existingKeys.length === 0) { - // add the default '0' set - this.#steps.set('0', this.#getNewStepValue(null, null)) - this.commitChange(true) + const stepId = existingKeys.length === 0 ? '0' : `${Math.max(...existingKeys) + 1}` - return '0' - } else { - // add one after the last - const max = Math.max(...existingKeys) + this.#steps.set(stepId, this.#getNewStepValue(null, null)) - const stepId = `${max + 1}` - this.#steps.set(stepId, this.#getNewStepValue(null, null)) + // Ensure current step is valid + this.#stepCheckExpression(true) - this.commitChange(true) + this.commitChange(true) - return stepId - } + return stepId } /** @@ -373,10 +477,13 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple * @param amount Number of steps to progress */ stepAdvanceDelta(amount: number): boolean { + // If using an expression, don't allow manual progression + if (this.#currentStep.type !== 'id') return false + if (amount && typeof amount === 'number') { const all_steps = this.getStepIds() if (all_steps.length > 0) { - const current = all_steps.indexOf(this.#current_step_id) + const current = all_steps.indexOf(this.#currentStep.id) let newIndex = (current === -1 ? 0 : current) + amount while (newIndex < 0) newIndex += all_steps.length @@ -413,6 +520,9 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple const newStepId = `${max + 1}` this.#steps.set(newStepId, newStep) + // Ensure current step is valid + this.#stepCheckExpression(false) + // Ensure the ui knows which step is current this.#sendRuntimePropsChange() @@ -427,6 +537,9 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple * @param index The step index to make the next */ stepMakeCurrent(index: number): boolean { + // If using an expression, don't allow manual progression + if (this.#currentStep.type !== 'id') return false + if (typeof index === 'number') { const stepId = this.getStepIds()[index - 1] if (stepId !== undefined) { @@ -456,15 +569,20 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple this.#steps.delete(stepId) // Update the current step - const oldIndex = oldKeys.indexOf(stepId) - let newIndex = oldIndex + 1 - if (newIndex >= oldKeys.length) { - newIndex = 0 - } - if (newIndex !== oldIndex) { - this.#current_step_id = oldKeys[newIndex] + if (this.#currentStep.type === 'id') { + const oldIndex = oldKeys.indexOf(stepId) + let newIndex = oldIndex + 1 + if (newIndex >= oldKeys.length) { + newIndex = 0 + } + if (newIndex !== oldIndex) { + this.#currentStep.id = oldKeys[newIndex] - this.#sendRuntimePropsChange() + this.#sendRuntimePropsChange() + } + } else { + // Ensure current step is valid + this.#stepCheckExpression(true) } // Save the change, and perform a draw @@ -478,13 +596,16 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple * @param stepId The step id to make the next */ stepSelectCurrent(stepId: string): boolean { + // If using an expression, don't allow manual progression + if (this.#currentStep.type !== 'id') return false + const step = this.#steps.get(stepId) if (!step) return false // Ensure it isn't currently pressed // this.setPushed(false) - this.#current_step_id = stepId + this.#currentStep.id = stepId this.#sendRuntimePropsChange() @@ -507,6 +628,9 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple this.#steps.set(stepId1, step2) this.#steps.set(stepId2, step1) + // Ensure current step is valid + this.#stepCheckExpression(true) + this.commitChange(false) return true @@ -528,9 +652,22 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple return true } - validateCurrentStepIdAndGetNext(): [null, null] | [string, string] { - const this_step_raw = this.#current_step_id + validateCurrentStepIdAndGetNextProgression(): [string | null, string | null] { + if (this.#currentStep.type === 'expression') { + // When in the expression mode, the next step is unknown, but we can produce a sane current step id + + if (this.#stepCheckExpression(true)) { + // Something changed, so redraw + this.triggerRedraw() + } + + return [this.#currentStep.lastStepId, null] + } + const stepIds = this.getStepIds() + + // For the automatic/manual progression + const this_step_raw = this.#currentStep.id if (stepIds.length > 0) { // verify 'this_step_raw' is valid const this_step_index = stepIds.findIndex((s) => s == this_step_raw) || 0 @@ -547,7 +684,7 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple } getActionsToExecuteForSet(setId: ActionSetId): ControlEntityInstance[] { - const [this_step_id] = this.validateCurrentStepIdAndGetNext() + const [this_step_id] = this.validateCurrentStepIdAndGetNextProgression() if (!this_step_id) return [] const step = this.#steps.get(this_step_id) diff --git a/companion/lib/Data/Upgrade.ts b/companion/lib/Data/Upgrade.ts index b532cd1ae4..0199d30339 100644 --- a/companion/lib/Data/Upgrade.ts +++ b/companion/lib/Data/Upgrade.ts @@ -10,6 +10,7 @@ import type { SomeExportv6 } from '@companion-app/shared/Model/ExportModel.js' import v5tov6 from './Upgrades/v5tov6.js' import v6tov7 from './Upgrades/v6tov7.js' import v7tov8 from './Upgrades/v7tov8.js' +import v8tov9 from './Upgrades/v8tov9.js' const logger = LogController.createLogger('Data/Upgrade') @@ -21,6 +22,7 @@ const allUpgrades = [ v5tov6, // v3.5 - replace action delay property https://github.com/bitfocus/companion/pull/3163 v6tov7, // v4.0 - rework 'entities' for better nesting https://github.com/bitfocus/companion/pull/3185 v7tov8, // v4.0 - break out into more tables + v8tov9, // v4.1 - convert button stepAutoProgress to stepProgression ] const targetVersion = allUpgrades.length + 1 diff --git a/companion/lib/Data/Upgrades/v6tov7.ts b/companion/lib/Data/Upgrades/v6tov7.ts index a5cb1147f8..e29dc9b30f 100644 --- a/companion/lib/Data/Upgrades/v6tov7.ts +++ b/companion/lib/Data/Upgrades/v6tov7.ts @@ -32,7 +32,7 @@ function convertDatabaseToV7(db: DataStoreBase, _logger: Logger) { function convertImportToV7(obj: SomeExportv4): SomeExportv6 { if (obj.type == 'full') { - const newObj: ExportFullv6 = { ...cloneDeep(obj), version: 6 } + const newObj: ExportFullv6 = { ...cloneDeep(obj), version: 7 } if (newObj.pages) { for (const page of Object.values(newObj.pages)) { convertPageControls(page) @@ -45,11 +45,11 @@ function convertImportToV7(obj: SomeExportv4): SomeExportv6 { } return newObj } else if (obj.type == 'page') { - const newObj: ExportPageModelv6 = { ...cloneDeep(obj), version: 6 } + const newObj: ExportPageModelv6 = { ...cloneDeep(obj), version: 7 } convertPageControls(newObj.page) return newObj } else if (obj.type == 'trigger_list') { - const newObj: ExportTriggersListv6 = { ...cloneDeep(obj), version: 6 } + const newObj: ExportTriggersListv6 = { ...cloneDeep(obj), version: 7 } for (const trigger of Object.values(newObj.triggers)) { fixupControlEntities(trigger) } diff --git a/companion/lib/Data/Upgrades/v8tov9.ts b/companion/lib/Data/Upgrades/v8tov9.ts new file mode 100644 index 0000000000..c5ca9405a7 --- /dev/null +++ b/companion/lib/Data/Upgrades/v8tov9.ts @@ -0,0 +1,79 @@ +import type { DataStoreBase } from '../StoreBase.js' +import type { Logger } from '../../Log/Controller.js' +import { cloneDeep } from 'lodash-es' +import type { SomeExportv4 } from '@companion-app/shared/Model/ExportModelv4.js' +import type { + ExportControlv6, + ExportFullv6, + ExportPageContentv6, + ExportPageModelv6, + ExportTriggersListv6, + SomeExportv6, +} from '@companion-app/shared/Model/ExportModel.js' + +/** + * do the database upgrades to convert from the v8 to the v9 format + */ +function convertDatabaseToV9(db: DataStoreBase, _logger: Logger) { + if (!db.store) return + + const controls = db.getTableView('controls') + + for (const [controlId, control] of Object.entries(controls.all())) { + // Fixup control + fixupControlEntities(control) + + controls.set(controlId, control) + } +} + +function convertImportToV9(obj: SomeExportv4): SomeExportv6 { + if (obj.type == 'full') { + const newObj: ExportFullv6 = { ...cloneDeep(obj), version: 9 } + if (newObj.pages) { + for (const page of Object.values(newObj.pages)) { + convertPageControls(page) + } + } + return newObj + } else if (obj.type == 'page') { + const newObj: ExportPageModelv6 = { ...cloneDeep(obj), version: 9 } + convertPageControls(newObj.page) + return newObj + } else if (obj.type == 'trigger_list') { + const newObj: ExportTriggersListv6 = { ...cloneDeep(obj), version: 9 } + return newObj + } else { + // No change + return obj + } +} + +function fixupControlEntities(control: ExportControlv6): void { + if (control.type === 'button') { + if (!control.options.stepProgression) { + control.options.stepProgression = control.options.stepAutoProgress ? 'auto' : 'manual' + delete control.options.stepAutoProgress + } + } else if (control.type === 'trigger') { + // Nothing to do + } else { + // Unknown control type! + } +} + +function convertPageControls(page: ExportPageContentv6): ExportPageContentv6 { + for (const row of Object.values(page.controls)) { + if (!row) continue + for (const control of Object.values(row)) { + fixupControlEntities(control) + } + } + + return page +} + +export default { + upgradeStartup: convertDatabaseToV9, + upgradeImport: convertImportToV9, +} diff --git a/companion/lib/Graphics/Controller.ts b/companion/lib/Graphics/Controller.ts index 02f3474361..c8ca59b261 100644 --- a/companion/lib/Graphics/Controller.ts +++ b/companion/lib/Graphics/Controller.ts @@ -180,7 +180,9 @@ export class GraphicsController extends EventEmitter { // Update step values[`b_step_${location.pageNumber}_${location.row}_${location.column}`] = - buttonStyle?.style === 'button' ? (buttonStyle.step_cycle ?? 1) : undefined + buttonStyle?.style === 'button' ? buttonStyle.stepCurrent : undefined + values[`b_step_count_${location.pageNumber}_${location.row}_${location.column}`] = + buttonStyle?.style === 'button' ? buttonStyle.stepCount : undefined // Submit the updated values if (this.#pendingVariables) { @@ -320,9 +322,11 @@ export class GraphicsController extends EventEmitter { cloud: false, cloud_error: false, button_status: undefined, - step_cycle: undefined, action_running: false, + stepCurrent: 1, + stepCount: 1, + show_topbar: buttonStyle.show_topbar, alignment: buttonStyle.alignment ?? 'center:center', pngalignment: buttonStyle.pngalignment ?? 'center:center', diff --git a/companion/lib/Graphics/Renderer.ts b/companion/lib/Graphics/Renderer.ts index 97bd45f3d9..022fa015f6 100644 --- a/companion/lib/Graphics/Renderer.ts +++ b/companion/lib/Graphics/Renderer.ts @@ -285,8 +285,8 @@ export class GraphicsRenderer { img.box(0, 0, 72, 13.5, colorBlack) img.horizontalLine(13.5, colorButtonYellow) - if (typeof drawStyle.step_cycle === 'number' && location) { - step = `.${drawStyle.step_cycle}` + if (drawStyle.stepCount > 1 && location) { + step = `.${drawStyle.stepCurrent}` } if (location === undefined) { diff --git a/companion/lib/Instance/Definitions.ts b/companion/lib/Instance/Definitions.ts index 20bc888425..d881ef3c35 100644 --- a/companion/lib/Instance/Definitions.ts +++ b/companion/lib/Instance/Definitions.ts @@ -303,7 +303,7 @@ export class InstanceDefinitions { type: 'button', options: { rotaryActions: definition.options?.rotaryActions ?? false, - stepAutoProgress: definition.options?.stepAutoProgress ?? true, + stepProgression: (definition.options?.stepAutoProgress ?? true) ? 'auto' : 'manual', }, style: { textExpression: false, diff --git a/companion/lib/Variables/Values.ts b/companion/lib/Variables/Values.ts index e43c57bc43..08df656dea 100644 --- a/companion/lib/Variables/Values.ts +++ b/companion/lib/Variables/Values.ts @@ -180,6 +180,9 @@ export class VariablesValues extends EventEmitter { '$(this:step)': location ? `$(internal:b_step_${location.pageNumber}_${location.row}_${location.column})` : VARIABLE_UNKNOWN_VALUE, + '$(this:step_count)': location + ? `$(internal:b_step_count_${location.pageNumber}_${location.row}_${location.column})` + : VARIABLE_UNKNOWN_VALUE, } } } diff --git a/shared-lib/lib/Model/ButtonModel.ts b/shared-lib/lib/Model/ButtonModel.ts index 5185450c61..5a12042f30 100644 --- a/shared-lib/lib/Model/ButtonModel.ts +++ b/shared-lib/lib/Model/ButtonModel.ts @@ -35,11 +35,13 @@ export type NormalButtonSteps = Record< } > -export interface ButtonOptionsBase {} +export interface ButtonOptionsBase { + stepProgression: 'auto' | 'manual' | 'expression' + stepExpression?: string +} export interface NormalButtonOptions extends ButtonOptionsBase { rotaryActions: boolean - stepAutoProgress: boolean } export type ButtonStatus = 'good' | 'warning' | 'error' diff --git a/shared-lib/lib/Model/ExportModel.ts b/shared-lib/lib/Model/ExportModel.ts index b2c57c91a0..290ad9ffb0 100644 --- a/shared-lib/lib/Model/ExportModel.ts +++ b/shared-lib/lib/Model/ExportModel.ts @@ -5,7 +5,7 @@ import type { CustomVariablesModel } from './CustomVariableModel.js' export type SomeExportv6 = ExportFullv6 | ExportPageModelv6 | ExportTriggersListv6 export interface ExportBase { - readonly version: 6 | 7 | 8 + readonly version: 6 | 7 | 8 | 9 readonly type: Type } diff --git a/shared-lib/lib/Model/StyleModel.ts b/shared-lib/lib/Model/StyleModel.ts index 589bf5efb6..dc7c707d61 100644 --- a/shared-lib/lib/Model/StyleModel.ts +++ b/shared-lib/lib/Model/StyleModel.ts @@ -8,7 +8,10 @@ export type DrawStyleModel = export interface DrawStyleButtonStateProps { pushed: boolean - step_cycle: number | undefined + + stepCurrent: number + stepCount: number + cloud: boolean | undefined cloud_error: boolean | undefined button_status: 'error' | 'warning' | 'good' | undefined diff --git a/webui/src/Buttons/EditButton/ButtonEditorTabs.tsx b/webui/src/Buttons/EditButton/ButtonEditorTabs.tsx index 540741b2e6..69e9f0b280 100644 --- a/webui/src/Buttons/EditButton/ButtonEditorTabs.tsx +++ b/webui/src/Buttons/EditButton/ButtonEditorTabs.tsx @@ -20,6 +20,7 @@ interface ButtonEditorTabsProps { controlId: string location: ControlLocation steps: NormalButtonSteps + disabledSetStep: boolean runtimeProps: Record rotaryActions: boolean extraTabs?: ButtonEditorExtraTabs[] @@ -29,6 +30,7 @@ export function ButtonEditorTabs({ controlId, location, steps, + disabledSetStep, runtimeProps, rotaryActions, extraTabs, @@ -119,6 +121,7 @@ export function ButtonEditorTabs({ selectedIndex={selectedIndex} selectedKey={selectedKey} selectedStepProps={selectedStepProps} + disabledSetStep={disabledSetStep} /> )} diff --git a/webui/src/Buttons/EditButton/ControlActionStepTab.tsx b/webui/src/Buttons/EditButton/ControlActionStepTab.tsx index 039e24fa4d..e01635a5ac 100644 --- a/webui/src/Buttons/EditButton/ControlActionStepTab.tsx +++ b/webui/src/Buttons/EditButton/ControlActionStepTab.tsx @@ -20,6 +20,7 @@ export interface ControlActionStepTabProps { selectedIndex: number selectedKey: string selectedStepProps: NormalButtonSteps[0] + disabledSetStep: boolean } export function ControlActionStepTab({ @@ -32,6 +33,7 @@ export function ControlActionStepTab({ selectedIndex, selectedKey, selectedStepProps, + disabledSetStep, }: ControlActionStepTabProps) { return ( <> @@ -55,8 +57,11 @@ export function ControlActionStepTab({ service.setCurrentStep(selectedKey)} title="Make this step the current step, without executing any actions." > diff --git a/webui/src/Buttons/EditButton/EditButton.tsx b/webui/src/Buttons/EditButton/EditButton.tsx index f91c238d43..b4fbaea414 100644 --- a/webui/src/Buttons/EditButton/EditButton.tsx +++ b/webui/src/Buttons/EditButton/EditButton.tsx @@ -239,6 +239,7 @@ function NormalButtonEditor({ location={location} controlId={controlId} steps={config.steps || {}} + disabledSetStep={config?.options?.stepProgression === 'expression'} runtimeProps={runtimeProps} rotaryActions={config?.options?.rotaryActions} extraTabs={NormalButtonExtraTabs} diff --git a/webui/src/Controls/ControlOptionsEditor.tsx b/webui/src/Controls/ControlOptionsEditor.tsx index 17633f91b1..8d6bf6a29c 100644 --- a/webui/src/Controls/ControlOptionsEditor.tsx +++ b/webui/src/Controls/ControlOptionsEditor.tsx @@ -4,6 +4,11 @@ import { SocketContext } from '~/util.js' import { GenericConfirmModal, GenericConfirmModalRef } from '~/Components/GenericConfirmModal.js' import { InlineHelp } from '~/Components/InlineHelp.js' import { NormalButtonOptions } from '@companion-app/shared/Model/ButtonModel.js' +import { DropdownInputField } from '~/Components/DropdownInputField.js' +import { DropdownChoice } from '@companion-module/base' +import { TextInputField } from '~/Components/TextInputField.js' +import { ControlLocalVariables } from '~/LocalVariableDefinitions.js' + interface ControlOptionsEditorProps { controlId: string options: NormalButtonOptions @@ -26,10 +31,8 @@ export function ControlOptionsEditor({ controlId, options, configRef }: ControlO [socket, controlId, configRef] ) - const setStepAutoProgressValue = useCallback( - (val: boolean) => setValueInner('stepAutoProgress', val), - [setValueInner] - ) + const setStepProgressionValue = useCallback((val: any) => setValueInner('stepProgression', val), [setValueInner]) + const setStepExpressionValue = useCallback((val: string) => setValueInner('stepExpression', val), [setValueInner]) const setRotaryActions = useCallback( (val: boolean) => { if (!val && confirmRef.current && configRef.current && configRef.current.options.rotaryActions === true) { @@ -55,17 +58,14 @@ export function ControlOptionsEditor({ controlId, options, configRef }: ControlO
- Progress + Step Progression
- { - setStepAutoProgressValue(!options.stepAutoProgress) - }} +
@@ -84,6 +84,29 @@ export function ControlOptionsEditor({ controlId, options, configRef }: ControlO /> + + {options.stepProgression === 'expression' && ( +
+
+ +
+
+ )} ) } + +const STEP_PROGRESSION_CHOICES: DropdownChoice[] = [ + { id: 'auto', label: 'Auto' }, + { id: 'manual', label: 'Manual' }, + { id: 'expression', label: 'Expression' }, +] diff --git a/webui/src/LocalVariableDefinitions.tsx b/webui/src/LocalVariableDefinitions.tsx index e339b3a62a..8fe79889ae 100644 --- a/webui/src/LocalVariableDefinitions.tsx +++ b/webui/src/LocalVariableDefinitions.tsx @@ -22,6 +22,10 @@ export const ControlLocalVariables: DropdownChoiceInt[] = [ value: 'this:step', label: 'The current step of this button', }, + { + value: 'this:step_count', + label: 'The number of steps on this button', + }, { value: 'this:page_name', label: 'This page name',