Skip to content

Conversation

@rjmunro
Copy link
Contributor

@rjmunro rjmunro commented Jan 14, 2026

About the Contributor

This pull request is posted on behalf of me.

Type of Contribution

This is a Feature

Current Behavior

You can control scrolling on the prompter screen with many devices, including a nintendo switch joycon, but not an XBox controller.

New Behavior

You can now use an XBox controller to control the prompter screen, and also to take, enabling presenter self-operation.

Scroll control:

  • Right Trigger (RT): Scroll forward - speed is proportional to trigger pressure
  • Left Trigger (LT): Scroll backward - speed is proportional to trigger pressure

Button map:

Button Action
A Take (go to next part)
B Go to the "On-air" story
X Go to the previous story
Y Go to the following story
LB (Left Bumper) Go to the top
RB (Right Bumper) Go to the "Next" story
D-Pad Up Scroll up (fine control)
D-Pad Down Scroll down (fine control)

Testing

  • I have added one or more unit tests for this PR
  • I have updated the relevant unit tests
  • No unit test changes are needed for this PR

Affected areas

  • This PR affects the prompter screen.

Time Frame

  • Not urgent, but I would like to get this merged into the in-development release.

Other Information

In my testing, the way the two buttons work for scrolling worked really well.

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

XBox Controller Support for Prompter Screen

This PR adds XBox controller support to the prompter screen, enabling presenters to control prompter scrolling and navigation using an Xbox GamePad controller.

Changes

Core Implementation

  • New Xbox Controller Class (packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts)
    • Implements XboxController class using HTML5 Gamepad API
    • Detects and filters Xbox-compatible gamepads across various ID patterns
    • Polling-based mechanism via requestAnimationFrame for continuous input detection
    • Trigger-based scrolling with configurable dead zones and spline-based speed interpolation
    • Debounced take action to prevent rapid-fire button presses

Button Mappings

  • A Button: Take (go to next part)
  • B Button: Go to "On-air" story
  • X Button: Go to previous story
  • Y Button: Go to following story
  • LB: Go to top
  • RB: Go to "Next" story
  • D-Pad Up/Down: Fine control scrolling
  • RT/LT Triggers: Proportional forward/backward scrolling with pressure sensitivity

Configuration Updates

  • PrompterView.tsx: Added Xbox configuration support with new fields:

    • xbox_speedMap: Speed mapping array for RT trigger
    • xbox_reverseSpeedMap: Speed mapping array for LT trigger
    • xbox_triggerDeadZone: Trigger deadzone threshold
    • New PrompterConfigMode.XBOX enum member
  • Controller Manager: Integrated XboxController initialization when Xbox mode is enabled

Documentation

  • Comprehensive Xbox controller guide with calibration instructions, usage notes, and troubleshooting
  • Fixed Joycon documentation (added joycon_invertJoystick parameter, corrected typo and description)
  • Updated Contour ShuttleXpress mode documentation for consistency

Testing

No unit tests have been added or updated in this PR. The contributor reports having tested the functionality manually.

Files Modified

  • packages/webui/src/client/ui/Prompter/PrompterView.tsx (+13/-0)
  • packages/webui/src/client/ui/Prompter/controller/manager.ts (+4/-0)
  • packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts (+301/-0)
  • packages/documentation/docs/user-guide/features/prompter.md (+47/-1)

@rjmunro rjmunro requested a review from a team as a code owner January 14, 2026 22:44
@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR adds comprehensive Xbox controller support to the prompter UI, including documentation, configuration handling, and a new XboxController implementation using the HTML5 Gamepad API for button mapping, trigger-based scrolling, and speed interpolation.

Changes

Cohort / File(s) Summary
Documentation
packages/documentation/docs/user-guide/features/prompter.md
Adds Xbox gamepad mode documentation with button mappings, trigger controls, speed/reverse speed maps, dead zone configuration, calibration guide, and troubleshooting. Expands Joycon config docs with joycon_invertJoystick parameter and corrects typos.
Configuration & Initialization
packages/webui/src/client/ui/Prompter/PrompterView.tsx, packages/webui/src/client/ui/Prompter/controller/manager.ts
Extends PrompterConfig interface with Xbox-specific fields (xbox_speedMap, xbox_reverseSpeedMap, xbox_triggerDeadZone), adds PrompterConfigMode.XBOX enum member, parses Xbox parameters from query strings, and conditionally instantiates XboxController in the manager.
Xbox Controller Implementation
packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts
New XboxController class providing gamepad detection, connection/disconnect handling, periodic polling via requestAnimationFrame, button event handling with debounce, trigger-based scrolling with spline-based speed mapping, and debug state reporting.

Sequence Diagram

sequenceDiagram
    participant User
    participant PrompterView as PrompterView<br/>(Config)
    participant Manager as ControlManager
    participant XboxCtrl as XboxController
    participant GamepadAPI as Gamepad API

    User->>PrompterView: Load with ?mode=xbox params
    PrompterView->>PrompterView: Parse xbox_speedMap,<br/>xbox_reverseSpeedMap,<br/>xbox_triggerDeadZone
    PrompterView->>Manager: Initialize with config
    Manager->>XboxCtrl: Create XboxController instance
    XboxCtrl->>GamepadAPI: Listen for gamepad connect/disconnect
    GamepadAPI-->>XboxCtrl: Gamepad connected
    XboxCtrl->>XboxCtrl: startPolling() via requestAnimationFrame
    
    loop Polling Loop
        XboxCtrl->>GamepadAPI: Poll gamepad state
        GamepadAPI-->>XboxCtrl: Button/trigger values
        XboxCtrl->>XboxCtrl: handleButtons() & check triggers
        alt Button Pressed (A, B, X, Y, etc.)
            XboxCtrl->>PrompterView: Trigger action<br/>(take, scroll, segment control)
        end
        alt Trigger Engaged (LT/RT)
            XboxCtrl->>XboxCtrl: Calculate speed via<br/>spline mapping
            XboxCtrl->>PrompterView: Update scroll position
        end
    end
    
    User->>XboxCtrl: Disconnect gamepad
    GamepadAPI-->>XboxCtrl: Gamepad disconnected
    XboxCtrl->>XboxCtrl: Stop polling (destroy)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🎮 A rabbit hops with joy, Xbox in hand,
Triggers pulse and buttons dance across the land,
Speed splines curve as gamepads take their stage,
Control flows smooth through each prompter page! 🐰✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately describes the main feature: adding Xbox controller support with take button to the prompter screen, which is the primary change across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Jan 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@rjmunro
Copy link
Contributor Author

rjmunro commented Jan 14, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts`:
- Around line 65-66: Constructor registers window event listeners for
'gamepadconnected' and 'gamepaddisconnected' using bound callbacks but destroy()
never removes them, causing a leak; store the bound handlers (e.g.,
this.onGamepadConnectedBound = this.onGamepadConnected.bind(this) and
this.onGamepadDisconnectedBound = this.onGamepadDisconnected.bind(this)) when
you attach them, use those stored references in window.addEventListener, and
call window.removeEventListener for both events with the stored bound references
inside destroy() (remove both this.onGamepadConnectedBound and
this.onGamepadDisconnectedBound).
🧹 Nitpick comments (3)
packages/webui/src/client/ui/Prompter/PrompterView.tsx (1)

182-190: Minor: || undefined will ignore explicit 0 value for xbox_triggerDeadZone.

If a user sets xbox_triggerDeadZone=0, the expression Number.parseFloat("0") || undefined evaluates to undefined because 0 is falsy, causing the user's explicit value to be discarded. While a dead zone of exactly 0 is rarely practical, consider using nullish coalescing or explicit NaN check for correctness:

Suggested fix
-		xbox_triggerDeadZone: Number.parseFloat(firstIfArray(queryParams['xbox_triggerDeadZone']) as string) || undefined,
+		xbox_triggerDeadZone: (() => {
+			const val = Number.parseFloat(firstIfArray(queryParams['xbox_triggerDeadZone']) as string)
+			return Number.isNaN(val) ? undefined : val
+		})(),
packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts (2)

295-300: Minor race condition window in polling loop.

Setting updateSpeedHandle = null inside the requestAnimationFrame callback (line 297) before calling updateScrollPosition() creates a brief window where startPolling() could be called (e.g., from gamepadconnected event) and schedule a duplicate polling loop.

Consider restructuring to avoid this:

Suggested fix
 	private updateScrollPosition(): void {
 		// ... existing logic ...
 
 		// Continue polling
-		this.updateSpeedHandle = window.requestAnimationFrame(() => {
-			this.updateSpeedHandle = null
-			this.updateScrollPosition()
-		})
+		this.updateSpeedHandle = window.requestAnimationFrame(this.updateScrollPosition.bind(this))
 	}

Or keep the handle assigned during the callback execution.


278-285: Minor: Redundant undefined checks.

The checks this.currentPosition !== undefined and scrollPosition !== undefined are always true since currentPosition is initialized to 0 and window.scrollY always returns a number. These can be simplified.

Suggested simplification
 			const scrollPosition = window.scrollY
-			if (this.currentPosition !== undefined && scrollPosition !== undefined) {
-				if (speed !== 0 && this.currentPosition === scrollPosition) {
-					// We tried to move but couldn't (reached end)
-					// Could add haptic feedback here if supported
-				}
+			if (speed !== 0 && this.currentPosition === scrollPosition) {
+				// We tried to move but couldn't (reached end)
+				// Could add haptic feedback here if supported
 			}
 			this.currentPosition = scrollPosition
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb736b and b15bb17.

📒 Files selected for processing (4)
  • packages/documentation/docs/user-guide/features/prompter.md
  • packages/webui/src/client/ui/Prompter/PrompterView.tsx
  • packages/webui/src/client/ui/Prompter/controller/manager.ts
  • packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-17T14:49:55.109Z
Learnt from: Julusian
Repo: Sofie-Automation/sofie-core PR: 1585
File: packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md:88-122
Timestamp: 2025-12-17T14:49:55.109Z
Learning: In the sofie-core repository, ensure that code blocks within Markdown files are indented with tabs rather than spaces. This convention is enforced by Prettier in the repository. During reviews, verify that Markdown code blocks use a leading tab for each line of the block (not spaces) and that Prettier's formatting rules reflect this preference.

Applied to files:

  • packages/documentation/docs/user-guide/features/prompter.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Lint Package (webui)
  • GitHub Check: Test Package (main) (22.x, job-worker, true)
  • GitHub Check: SonarQube
🔇 Additional comments (10)
packages/webui/src/client/ui/Prompter/PrompterView.tsx (2)

65-67: LGTM!

The new Xbox configuration fields are properly typed as optional and align well with existing controller config patterns (joycon, pedal).


83-83: LGTM!

The new enum member follows the established naming convention.

packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts (3)

112-124: Consider: Broad fallback detection may match non-Xbox controllers.

The fallback condition on line 122 (gamepad.mapping === 'standard' && gamepad.axes.length >= 4 && gamepad.buttons.length >= 17) could match PlayStation controllers and other standard gamepads. This may be intentional for broader compatibility, but could cause unexpected behavior if multiple controller types are used simultaneously.

If strict Xbox-only detection is desired, you could remove the fallback or make it opt-in via a config parameter.


175-228: LGTM!

The button handling implementation is well-structured:

  • Proper edge detection for button presses
  • Appropriate debounce for the Take action to prevent accidental rapid-fire
  • Clean separation of button state tracking per controller index

230-260: LGTM!

Speed calculation correctly handles trigger inputs with dead zones and spline interpolation. The additive behavior when both triggers are pressed (forward speed minus backward speed) provides intuitive control.

packages/webui/src/client/ui/Prompter/controller/manager.ts (2)

9-9: LGTM!

Import addition follows the established pattern.


43-45: LGTM!

The XboxController integration follows the same pattern as existing controllers (JoyConController, MidiPedalController, etc.), maintaining consistency in the codebase.

packages/documentation/docs/user-guide/features/prompter.md (3)

49-49: LGTM!

The new Xbox mode table entry is properly formatted and links to the correct anchor.


165-172: LGTM!

The Joycon parameter documentation is accurate and aligns with the code implementation.


203-245: LGTM!

Excellent documentation covering:

  • Platform compatibility notes (macOS USB limitation)
  • Clear scroll control explanations
  • Complete button mapping table matching the code
  • Configuration parameters with accurate defaults
  • Practical calibration guide

The documentation is thorough and will help users get started quickly.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 65 to 66
window.addEventListener('gamepadconnected', this.onGamepadConnected.bind(this))
window.addEventListener('gamepaddisconnected', this.onGamepadDisconnected.bind(this))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Event listeners are not removed in destroy(), causing potential memory leak.

The gamepadconnected and gamepaddisconnected event listeners are added in the constructor but never removed in the destroy() method. This could cause memory leaks if the controller is destroyed and recreated.

Suggested fix

Store bound references and remove them in destroy:

+	private boundOnGamepadConnected: (e: GamepadEvent) => void
+	private boundOnGamepadDisconnected: (e: GamepadEvent) => void
+
 	constructor(view: PrompterViewContent) {
 		super()
 		this.prompterView = view
 		// ... existing code ...
 
-		window.addEventListener('gamepadconnected', this.onGamepadConnected.bind(this))
-		window.addEventListener('gamepaddisconnected', this.onGamepadDisconnected.bind(this))
+		this.boundOnGamepadConnected = this.onGamepadConnected.bind(this)
+		this.boundOnGamepadDisconnected = this.onGamepadDisconnected.bind(this)
+		window.addEventListener('gamepadconnected', this.boundOnGamepadConnected)
+		window.addEventListener('gamepaddisconnected', this.boundOnGamepadDisconnected)
 
 		// Start polling if a controller is already connected
 		this.startPolling()
 	}
 
 	public destroy(): void {
+		window.removeEventListener('gamepadconnected', this.boundOnGamepadConnected)
+		window.removeEventListener('gamepaddisconnected', this.boundOnGamepadDisconnected)
 		if (this.updateSpeedHandle !== null) {
 			window.cancelAnimationFrame(this.updateSpeedHandle)
 			this.updateSpeedHandle = null
 		}
 	}
🤖 Prompt for AI Agents
In `@packages/webui/src/client/ui/Prompter/controller/xbox-controller-device.ts`
around lines 65 - 66, Constructor registers window event listeners for
'gamepadconnected' and 'gamepaddisconnected' using bound callbacks but destroy()
never removes them, causing a leak; store the bound handlers (e.g.,
this.onGamepadConnectedBound = this.onGamepadConnected.bind(this) and
this.onGamepadDisconnectedBound = this.onGamepadDisconnected.bind(this)) when
you attach them, use those stored references in window.addEventListener, and
call window.removeEventListener for both events with the stored bound references
inside destroy() (remove both this.onGamepadConnectedBound and
this.onGamepadDisconnectedBound).

Copy link
Member

@nytamin nytamin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature looks well implemented to me.

While the feature is pretty specific the implementation is self-contained and should have minimal impact on future maintenance.

@rjmunro rjmunro changed the title Add XBox controller support with take button to prompter screen feat: Add XBox controller support with take button to prompter screen Jan 15, 2026
manager.ts:
- Replace .indexOf() > -1 with .includes() for mode checks

xbox-controller-device.ts and joycon-device.ts
- Add readonly modifier to members that are never reassigned:
- Store bound handlers for gamepadconnected/gamepaddisconnected events
- Remove event listeners in destroy() to prevent memory leaks
- For joycon, also cancel any pending requestAnimationFrame in destroy()
@rjmunro rjmunro force-pushed the rjmunro/xbox-controller branch from b15bb17 to 177faef Compare January 15, 2026 13:28
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants