diff --git a/companion/lib/Controls/ControlTypes/Button/Base.ts b/companion/lib/Controls/ControlTypes/Button/Base.ts index 68dddf9352..6ecf26ae4b 100644 --- a/companion/lib/Controls/ControlTypes/Button/Base.ts +++ b/companion/lib/Controls/ControlTypes/Button/Base.ts @@ -61,7 +61,7 @@ export abstract class ButtonControlBase | null = null @@ -82,6 +82,13 @@ export abstract class ButtonControlBase + deps.variables.values.executeExpression( + expression, + deps.page.getLocationOfControlId(this.controlId), + requiredType, + injectedVariableValues + ), }, this.sendRuntimePropsChange.bind(this) ) @@ -215,7 +222,8 @@ export abstract class ButtonControlBase this.postProcessImport()) @@ -118,9 +119,8 @@ export class ControlButtonNormal const style = super.getDrawStyle() if (!style) return style - if (this.entities.getStepIds().length > 1) { - style.step_cycle = this.entities.getActiveStepIndex() + 1 - } + style.stepCurrent = this.entities.getActiveStepIndex() + 1 + style.stepCount = this.entities.getStepIds().length return style } @@ -159,13 +159,21 @@ export class ControlButtonNormal /** * Update an option field of this control */ - optionsSetField(key: string, value: any): boolean { + optionsSetField(key0: string, value: any): boolean { + const key = key0 as keyof NormalButtonOptions + // Check if rotary_actions should be added/remove if (key === 'rotaryActions') { this.entities.setupRotaryActionSets(!!value, true) } - return super.optionsSetField(key, value) + const changed = super.optionsSetField(key, value) + + if (key === 'stepProgression' || key === 'stepExpression') { + this.entities.stepExpressionUpdate(this.options) + } + + return changed } /** @@ -175,7 +183,7 @@ export class ControlButtonNormal * @param force Trigger actions even if already in the state */ pressControl(pressed: boolean, surfaceId: string | undefined, force: boolean): void { - const [thisStepId, nextStepId] = this.entities.validateCurrentStepIdAndGetNext() + const [thisStepId, nextStepId] = this.entities.validateCurrentStepIdAndGetNextProgression() let pressedDuration = 0 let pressedStep = thisStepId @@ -210,7 +218,7 @@ export class ControlButtonNormal if ( thisStepId !== null && nextStepId !== null && - this.options.stepAutoProgress && + this.options.stepProgression === 'auto' && !pressed && (pressedStep === undefined || thisStepId === pressedStep) ) { @@ -289,6 +297,16 @@ export class ControlButtonNormal }) } + /** + * Propagate variable changes + * @param allChangedVariables - variables with changes + */ + onVariablesChanged(allChangedVariables: Set): void { + super.onVariablesChanged(allChangedVariables) + + this.entities.stepCheckExpressionOnVariablesChanged(allChangedVariables) + } + /** * Convert this control to JSON * To be sent to the client and written to the db diff --git a/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts b/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts index d14307595e..4be77d4ff7 100644 --- a/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts +++ b/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts @@ -155,6 +155,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 043dd968e0..cff1b11983 100644 --- a/companion/lib/Controls/Entities/EntityListPoolBase.ts +++ b/companion/lib/Controls/Entities/EntityListPoolBase.ts @@ -13,6 +13,8 @@ import type { InternalController } from '../../Internal/Controller.js' import { isEqual } from 'lodash-es' import type { ButtonStyleProperties } from '@companion-app/shared/Model/StyleModel.js' import type { InstanceDefinitionsForEntity } from './Types.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 42ef124107..fd930b8f43 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 { /** @@ -41,12 +56,13 @@ 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' } /** * The base style without feedbacks applied @@ -56,7 +72,15 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple #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') + } } get baseStyle(): ButtonStyleProperties { @@ -66,6 +90,7 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple constructor(props: ControlEntityListPoolProps, sendRuntimePropsChange: () => void) { super(props) + this.#executeExpressionInControl = props.executeExpressionInControl this.#sendRuntimePropsChange = sendRuntimePropsChange this.#feedbacks = new ControlEntityList( @@ -79,7 +104,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)) } @@ -96,7 +121,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? } /** @@ -359,10 +384,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), 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 @@ -371,24 +482,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 } /** @@ -396,10 +500,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 @@ -441,6 +548,9 @@ export class ControlEntityListPoolButton extends ControlEntityListPoolBase imple this.logger.silly(`stepDuplicate failed postProcessImport for ${this.controlId} failed: ${e.message}`) }) + // Ensure current step is valid + this.#stepCheckExpression(false) + // Ensure the ui knows which step is current this.#sendRuntimePropsChange() @@ -455,6 +565,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) { @@ -484,15 +597,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 @@ -506,13 +624,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() @@ -535,6 +656,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 @@ -556,9 +680,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 @@ -575,7 +712,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 abbb254f38..8cca7b2137 100644 --- a/companion/lib/Data/Upgrade.ts +++ b/companion/lib/Data/Upgrade.ts @@ -9,6 +9,7 @@ import type { DataDatabase } from './Database.js' 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' const logger = LogController.createLogger('Data/Upgrade') @@ -19,6 +20,7 @@ const allUpgrades = [ v4tov5, // v3.5 - first round of sqlite rearranging v5tov6, // v3.5 - replace action delay property https://github.com/bitfocus/companion/pull/3163 v6tov7, // v3.6 - rework 'entities' for better nesting https://github.com/bitfocus/companion/pull/3185 + v7tov8, // v3.6 - 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 9e0ff9bea4..418f9861b3 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) @@ -40,11 +40,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/v7tov8.ts b/companion/lib/Data/Upgrades/v7tov8.ts new file mode 100644 index 0000000000..50d2c47da8 --- /dev/null +++ b/companion/lib/Data/Upgrades/v7tov8.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 v7 to the v8 format + */ +function convertDatabaseToV8(db: DataStoreBase, _logger: Logger) { + if (!db.store) return + + const controls = db.getTable('controls') + + for (const [controlId, control] of Object.entries(controls)) { + // Fixup control + fixupControlEntities(control) + + db.setTableKey('controls', controlId, control) + } +} + +function convertImportToV8(obj: SomeExportv4): SomeExportv6 { + if (obj.type == 'full') { + const newObj: ExportFullv6 = { ...cloneDeep(obj), version: 8 } + 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: 8 } + convertPageControls(newObj.page) + return newObj + } else if (obj.type == 'trigger_list') { + const newObj: ExportTriggersListv6 = { ...cloneDeep(obj), version: 8 } + 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: convertDatabaseToV8, + upgradeImport: convertImportToV8, +} diff --git a/companion/lib/Graphics/Controller.ts b/companion/lib/Graphics/Controller.ts index 20b3693baa..3c0aa5c69e 100644 --- a/companion/lib/Graphics/Controller.ts +++ b/companion/lib/Graphics/Controller.ts @@ -177,7 +177,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) { @@ -317,9 +319,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 b404255a70..0591399f64 100644 --- a/companion/lib/Graphics/Renderer.ts +++ b/companion/lib/Graphics/Renderer.ts @@ -310,8 +310,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 > 0 && location) { + step = `.${drawStyle.stepCurrent}` } if (location === undefined) { diff --git a/companion/lib/Instance/Definitions.ts b/companion/lib/Instance/Definitions.ts index e63a08f088..7694b46ae9 100644 --- a/companion/lib/Instance/Definitions.ts +++ b/companion/lib/Instance/Definitions.ts @@ -307,7 +307,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 298350cb11..ba743485e9 100644 --- a/companion/lib/Variables/Values.ts +++ b/companion/lib/Variables/Values.ts @@ -189,6 +189,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..308a36c243 100644 --- a/shared-lib/lib/Model/ButtonModel.ts +++ b/shared-lib/lib/Model/ButtonModel.ts @@ -39,7 +39,8 @@ export interface ButtonOptionsBase {} export interface NormalButtonOptions extends ButtonOptionsBase { rotaryActions: boolean - stepAutoProgress: boolean + stepProgression: 'auto' | 'manual' | 'expression' + stepExpression?: string } export type ButtonStatus = 'good' | 'warning' | 'error' diff --git a/shared-lib/lib/Model/ExportModel.ts b/shared-lib/lib/Model/ExportModel.ts index 1b446425be..710df2b1a6 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 + readonly version: 6 | 7 | 8 readonly type: Type } diff --git a/shared-lib/lib/Model/StyleModel.ts b/shared-lib/lib/Model/StyleModel.ts index 1d8786061f..d2ca54a7ba 100644 --- a/shared-lib/lib/Model/StyleModel.ts +++ b/shared-lib/lib/Model/StyleModel.ts @@ -12,7 +12,10 @@ export interface DrawStyleButtonModel extends ButtonStyleProperties { imageBuffers: DrawImageBuffer[] 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.tsx b/webui/src/Buttons/EditButton.tsx index 48092b7b98..6b04a5214e 100644 --- a/webui/src/Buttons/EditButton.tsx +++ b/webui/src/Buttons/EditButton.tsx @@ -349,6 +349,7 @@ export const EditButton = observer(function EditButton({ location, onKeyUp }: Ed location={location} controlId={controlId} steps={config.steps || {}} + disabledSetStep={config?.options?.stepProgression === 'expression'} runtimeProps={runtimeProps} rotaryActions={config?.options?.rotaryActions} feedbacks={config.feedbacks} @@ -368,12 +369,22 @@ interface TabsSectionProps { controlId: string location: ControlLocation steps: NormalButtonSteps + disabledSetStep: boolean runtimeProps: Record rotaryActions: boolean feedbacks: SomeEntityModel[] } -function TabsSection({ style, controlId, location, steps, runtimeProps, rotaryActions, feedbacks }: TabsSectionProps) { +function TabsSection({ + style, + controlId, + location, + steps, + disabledSetStep, + runtimeProps, + rotaryActions, + feedbacks, +}: TabsSectionProps) { const socket = useContext(SocketContext) const confirmRef = useRef(null) @@ -583,8 +594,11 @@ function TabsSection({ style, controlId, location, steps, runtimeProps, rotaryAc setCurrentStep(selectedKey)} > Select diff --git a/webui/src/Controls/ControlOptionsEditor.tsx b/webui/src/Controls/ControlOptionsEditor.tsx index 310e901dfb..78fee2836b 100644 --- a/webui/src/Controls/ControlOptionsEditor.tsx +++ b/webui/src/Controls/ControlOptionsEditor.tsx @@ -3,6 +3,11 @@ import React, { MutableRefObject, useCallback, useContext, useRef } from 'react' import { SocketContext } from '../util.js' import { GenericConfirmModal, GenericConfirmModalRef } from '../Components/GenericConfirmModal.js' import { InlineHelp } from '../Components/InlineHelp.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 controlType: string @@ -31,10 +36,8 @@ export function ControlOptionsEditor({ [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) { @@ -70,24 +73,32 @@ export function ControlOptionsEditor({ <> {' '} -
- {controlType === 'button' && ( - <> + {controlType === 'button' && ( + <> +
- Progress + Step Progression
- { - setStepAutoProgressValue(!options.stepAutoProgress) + setStepProgressionValue(!options.stepAutoProgress) }} - /> + /> */} +
+ +
@@ -104,9 +115,32 @@ export function ControlOptionsEditor({ }} />
- - )} -
+
+ + {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 a567947453..93a0d40a6f 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 step on this button', + }, { value: 'this:page_name', label: 'This page name',