From 33e432ec0ad31d3a448c57462a108368e1d3e9dc Mon Sep 17 00:00:00 2001 From: Casper Boone Date: Sat, 21 Jun 2025 18:22:27 +0200 Subject: [PATCH] Add action to quickly create a button shortcut --- companion/lib/Controls/Controller.ts | 85 ++++++++++++++++++++++++- companion/lib/Graphics/Renderer.ts | 2 +- shared-lib/lib/SocketIO.ts | 1 + webui/src/Buttons/ButtonGridActions.tsx | 23 ++++++- 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/companion/lib/Controls/Controller.ts b/companion/lib/Controls/Controller.ts index c032e7447e..12fabe0404 100644 --- a/companion/lib/Controls/Controller.ts +++ b/companion/lib/Controls/Controller.ts @@ -15,7 +15,7 @@ import { ControlTrigger } from './ControlTypes/Triggers/Trigger.js' import { nanoid } from 'nanoid' import { TriggerEvents } from './TriggerEvents.js' import debounceFn from 'debounce-fn' -import type { SomeButtonModel } from '@companion-app/shared/Model/ButtonModel.js' +import type { SomeButtonModel, NormalButtonModel } from '@companion-app/shared/Model/ButtonModel.js' import type { ClientTriggerData, TriggerCollection, TriggerModel } from '@companion-app/shared/Model/TriggerModel.js' import type { SomeControl } from './IControlFragments.js' import type { Registry } from '../Registry.js' @@ -30,6 +30,8 @@ import { VariablesAndExpressionParser } from '../Variables/VariablesAndExpressio import LogController from '../Log/Controller.js' import { DataStoreTableView } from '../Data/StoreBase.js' import { TriggerCollections } from './TriggerCollections.js' +import { EntityModelType } from '@companion-app/shared/Model/EntityModel.js' +import { ButtonControlBase } from './ControlTypes/Button/Base.js' export const TriggersListRoom = 'triggers:list' const ActiveLearnRoom = 'learn:active' @@ -302,6 +304,46 @@ export class ControlsController { return false }) + client.onPromise('controls:shortcut', (fromLocation, toLocation) => { + // Don't try shortcutting to itself + if ( + fromLocation.pageNumber === toLocation.pageNumber && + fromLocation.column === toLocation.column && + fromLocation.row === toLocation.row + ) + return false + + // Make sure target page number is valid + if (!this.#registry.page.isPageValid(toLocation.pageNumber)) return false + + // Make sure there is something to shortcut to + const fromControlId = this.#registry.page.getControlIdAt(fromLocation) + if (!fromControlId) return false + + const fromControl = this.getControl(fromControlId) + if (!fromControl) return false + + // Delete the control at the destination + const toControlId = this.#registry.page.getControlIdAt(toLocation) + if (toControlId) { + this.deleteControl(toControlId) + } + + const createRotaryActions = fromControl.supportsOptions && fromControl.options.rotaryActions + const newControlJson = this.createShortcutButtonDefinitionModel(fromLocation, createRotaryActions) + + const newControlId = CreateBankControlId(nanoid()) + const newControl = this.#createClassForControl(newControlId, 'button', newControlJson, true) + if (newControl) { + this.#controls.set(newControlId, newControl) + + this.#registry.page.setControlIdAt(toLocation, newControlId) + + return true + } + + return false + }) client.onPromise('controls:swap', (fromLocation, toLocation) => { // Don't try moving over itself if ( @@ -1317,6 +1359,47 @@ export class ControlsController { } } + createShortcutButtonDefinitionModel( + targetLocation: ControlLocation, + createRotaryActions: boolean + ): NormalButtonModel { + const locationOptions = { + location_target: 'text', + location_text: formatLocation(targetLocation), + } + + const entity = (type: EntityModelType, definitionId: string) => { + const entity = this.#registry.instance.definitions.createEntityItem('internal', type, definitionId) + if (!entity) { + throw new Error(`Failed to create action entity for definition "${definitionId}"`) + } + entity.options = locationOptions + return entity + } + + return { + type: 'button', + style: ControlButtonNormal.DefaultStyle, + feedbacks: [entity(EntityModelType.Feedback, 'bank_style')], + steps: { + '0': { + action_sets: { + down: [entity(EntityModelType.Action, 'button_pressrelease')], + up: [], + rotate_left: createRotaryActions ? [entity(EntityModelType.Action, 'button_rotate_left')] : undefined, + rotate_right: createRotaryActions ? [entity(EntityModelType.Action, 'button_rotate_right')] : undefined, + }, + options: { runWhileHeld: [] }, + }, + }, + options: { + ...ButtonControlBase.DefaultOptions, + rotaryActions: createRotaryActions, + }, + localVariables: [], + } + } + createVariablesAndExpressionParser( controlLocation: ControlLocation | null | undefined, overrideVariableValues: CompanionVariableValues | null diff --git a/companion/lib/Graphics/Renderer.ts b/companion/lib/Graphics/Renderer.ts index 6348005eda..f9357b1502 100644 --- a/companion/lib/Graphics/Renderer.ts +++ b/companion/lib/Graphics/Renderer.ts @@ -299,7 +299,7 @@ export class GraphicsRenderer { } } else if (drawStyle.style === 'button') { const textAlign = ParseAlignment(drawStyle.alignment) - const pngAlign = ParseAlignment(drawStyle.pngalignment) + const pngAlign = ParseAlignment(drawStyle.pngalignment ?? 'center:center') // For some reason, pngalignment is not always set (undefined) when using the `bank_style` feedback, while we do expect it to be. This is an existing issue, I haven't looked into it yet. processedStyle = { type: 'button', diff --git a/shared-lib/lib/SocketIO.ts b/shared-lib/lib/SocketIO.ts index b2357080ff..423ca62fd1 100644 --- a/shared-lib/lib/SocketIO.ts +++ b/shared-lib/lib/SocketIO.ts @@ -121,6 +121,7 @@ export interface ClientToBackendEventsMap extends AllMultipartUploaderMethods { 'controls:set-style-fields': (controlId: string, styleFields: Record) => boolean 'controls:move': (from: ControlLocation, to: ControlLocation) => boolean 'controls:copy': (from: ControlLocation, to: ControlLocation) => boolean + 'controls:shortcut': (from: ControlLocation, to: ControlLocation) => boolean 'controls:swap': (from: ControlLocation, to: ControlLocation) => boolean 'controls:reset': (location: ControlLocation, newType?: string) => void diff --git a/webui/src/Buttons/ButtonGridActions.tsx b/webui/src/Buttons/ButtonGridActions.tsx index 0bbb4ab60b..299b5b1cfe 100644 --- a/webui/src/Buttons/ButtonGridActions.tsx +++ b/webui/src/Buttons/ButtonGridActions.tsx @@ -2,7 +2,15 @@ import { CButton, CCol } from '@coreui/react' import React, { forwardRef, useCallback, useContext, useImperativeHandle, useRef, useState } from 'react' import { SocketContext } from '~/util.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faArrowsLeftRight, faArrowsAlt, faCompass, faCopy, faEraser, faTrash } from '@fortawesome/free-solid-svg-icons' +import { + faArrowsLeftRight, + faArrowsAlt, + faCompass, + faCopy, + faEraser, + faLink, + faTrash, +} from '@fortawesome/free-solid-svg-icons' import classnames from 'classnames' import { GenericConfirmModal, GenericConfirmModalRef } from '~/Components/GenericConfirmModal.js' import { useResizeObserver } from 'usehooks-ts' @@ -134,6 +142,17 @@ export const ButtonGridActions = forwardRef { + console.error(`shortcut failed: ${e}`) + }) + stopFunction() + } else { + setActiveFunctionButton(location) + } + return true case 'move': if (activeFunctionButton) { const fromInfo = activeFunctionButton @@ -183,6 +202,8 @@ export const ButtonGridActions = forwardRef