diff --git a/app/components-react/editor/elements/SourceSelector.tsx b/app/components-react/editor/elements/SourceSelector.tsx index 0ece169946d2..6fc0f22f2876 100644 --- a/app/components-react/editor/elements/SourceSelector.tsx +++ b/app/components-react/editor/elements/SourceSelector.tsx @@ -387,6 +387,7 @@ class SourceSelectorController { * clicking on the source selector selects both sources */ if ( + this.isDualOutputActive && this.dualOutputService.views.hasNodeMap(this.scene.id) && this.dualOutputService.views.activeDisplays.horizontal && this.dualOutputService.views.activeDisplays.vertical @@ -504,6 +505,7 @@ class SourceSelectorController { * in the vertical display, convert the vertical node id to the horizontal node id. */ if ( + this.isDualOutputActive && this.dualOutputService.views.activeDisplays.horizontal && this.dualOutputService.views.activeDisplays.vertical ) { diff --git a/app/services/dual-output/dual-output.ts b/app/services/dual-output/dual-output.ts index c4e82eaa577d..aa4ad22a0ab6 100644 --- a/app/services/dual-output/dual-output.ts +++ b/app/services/dual-output/dual-output.ts @@ -174,7 +174,7 @@ class DualOutputViews extends ViewHandler { } get onlyVerticalDisplayActive() { - return this.activeDisplays.vertical && !this.activeDisplays.horizontal; + return this.dualOutputMode && this.activeDisplays.vertical && !this.activeDisplays.horizontal; } get platformsDualStreaming() { diff --git a/app/services/scenes/scene.ts b/app/services/scenes/scene.ts index 7ced6b012b73..eaa62ee47a5b 100644 --- a/app/services/scenes/scene.ts +++ b/app/services/scenes/scene.ts @@ -151,9 +151,7 @@ export class Scene { getSourceSelectorNodes(): TSceneNode[] { let nodes = this.getNodes(); - const populateWithVerticalNodes = - !this.dualOutputService.views.activeDisplays.horizontal && - this.dualOutputService.views.activeDisplays.vertical; + const populateWithVerticalNodes = this.dualOutputService.views.onlyVerticalDisplayActive; nodes = nodes.filter(node => { // if only the vertical display is active diff --git a/test/regular/api/dual-output.ts b/test/regular/api/dual-output.ts index 7b439778f8df..664e368041e6 100644 --- a/test/regular/api/dual-output.ts +++ b/test/regular/api/dual-output.ts @@ -3,11 +3,87 @@ import { getApiClient } from '../../helpers/api-client'; import { test, useWebdriver, TExecutionContext } from '../../helpers/webdriver'; import { ScenesService, Scene, SceneItem } from 'services/scenes'; import { VideoSettingsService } from 'services/settings-v2/video'; +import { SelectionService } from 'services/api/external-api/selection'; +import { click, focusMain, waitForDisplayed } from '../../helpers/modules/core'; // not a react hook // eslint-disable-next-line react-hooks/rules-of-hooks useWebdriver(); +testSelectionAfterDisablingDualOutput(false); +testSelectionAfterDisablingDualOutput(true); + +async function setDualOutputMode(status: boolean) { + const client = await getApiClient(); + // The RPC fallback exposes this private method; its loading-mode decorator returns a promise. + const dualOutput = client.getResource<{ + setDualOutputMode(status: boolean, skipShowVideoSettings: boolean): Promise; + }>('DualOutputService'); + + // Exercise the normal mode transition without requiring a provider login. + await dualOutput.setDualOutputMode(status, true); + await focusMain(); +} + +function testSelectionAfterDisablingDualOutput(horizontalVisible: boolean) { + test(`Selection after disabling dual output with horizontal ${ + horizontalVisible ? 'visible' : 'hidden' + }`, async t => { + const client = await getApiClient(); + const scenesService = client.getResource('ScenesService'); + const dualOutputService = client.getResource('DualOutputService'); + const selection = client.getResource('SelectionService'); + const scene = scenesService.createScene('Selection transition'); + scenesService.makeSceneActive(scene.id); + const horizontalItem = scene.createAndAddSource('Selection target', 'color_source'); + horizontalItem.fitToScreen(); + + await setDualOutputMode(true); + dualOutputService.toggleDisplay(horizontalVisible, 'horizontal'); + const verticalItem = scene.getItems().find(item => item.display === 'vertical'); + t.truthy(verticalItem, 'Dual output created a vertical partner'); + t.deepEqual( + scene.getSourceSelectorNodes().map(node => node.id), + [horizontalVisible ? horizontalItem.id : verticalItem.id], + 'Dual output source rows follow the visible displays', + ); + + await setDualOutputMode(false); + await waitForDisplayed('#horizontal-display'); + t.deepEqual( + scene.getSourceSelectorNodes().map(node => node.id), + [horizontalItem.id], + 'Single output source rows always use horizontal items', + ); + + await click('[data-name="Selection target"]'); + t.deepEqual(selection.getIds(), [horizontalItem.id], 'List selects only the visible item'); + + selection.reset(); + await t.context.app.client.waitUntil(async () => { + const selectedRows = await t.context.app.client.$$( + '.ant-tree-node-selected [data-name="Selection target"]', + ); + return selectedRows.length === 0; + }); + await click('#horizontal-display'); + await waitForDisplayed('.ant-tree-node-selected [data-name="Selection target"]'); + t.deepEqual(selection.getIds(), [horizontalItem.id], 'Canvas selection highlights the list'); + + await setDualOutputMode(true); + t.is( + dualOutputService.state.videoSettings.activeDisplays.horizontal, + horizontalVisible, + 'The saved horizontal display preference is preserved', + ); + t.deepEqual( + scene.getSourceSelectorNodes().map(node => node.id), + [horizontalVisible ? horizontalItem.id : verticalItem.id], + 'Re-enabling dual output restores the corresponding source rows', + ); + }); +} + function confirmDualOutputSources(t: TExecutionContext, scene: Scene) { const numSceneItems = scene .getItems()