ViewModels Reference
Every ViewModel class in PadForge: what it backs, what it exposes, and how state flows between the UI and the services layer.
v4 (2026-07-12): Refreshed for v4. Remote Link (DashboardViewModel + SettingsViewModel, and the RemoteLinkTrustedPeer / RemoteLinkNearbyPeer rows), the per-(slot, device) DeviceSlotConfig (renamed from PlayStationSlotConfig), KbmSlotConfig (SOCD / Snap Tap), multi-source mapping rows (MappingSourceItem), the mouse-gesture partial, and the Ember dashboard vocabulary all landed since the v3 rewrite. The HIDMaestro SDK surface, OpenXInput shim, thread-pool lifecycle, and bubble-up cascade live on HIDMaestro Deep Dive. If anything here drifts from the live source, the live source wins.
v4.1 (2026-07-30): The Nintendo output type (#215) joins the family. PadViewModel gains the Bass Shakers tab (#236), the slot SOCD card (#240), the flick rotation offset, gyro Acceleration, and the macro Layer dropdown. MacroItem carries the #238 / #253 fire-mode wave and per-macro layer scope (#254). Mapping rows and sources gain Flip Output, per-source Acceleration, and the #251 axis-latch macro actions. The Touchpad partial grows the libinput pointer Response, momentum, and pointer-region cards. DeviceSlotConfig adds synthetic touch pressure (#239) and the Switch home-LED lane (#226).
v4.2: The VR output type (#49) lands as VirtualControllerType.Vr (6), with a VrControllerIcon sidebar entry, a VrOutputSnapshot, and a SteamVR row in both driver-status surfaces (DashboardViewModel.IsSteamVrInstalled, plus the install / uninstall pair on SettingsViewModel). The pad page grows two slot-tier tabs: Bass Shakers at index 16 and Output at index 17 (SOCD plus Keep Controller Awake). DeviceSlotConfig gains headphone-jack volume, the DualSense audio-output path, and persona haptics, with MacroActionType.HeadphoneVolumeUp / HeadphoneVolumeDown (52 / 53) driving the volume. Sony headset head trackers get their own device class (HeadsetMotion, #188). RawHidState replaced the old ExtendedRawState, so the Extended snapshot is now RawHidOutputSnapshot.
v4.3: The mapping picker gains one shared per-slot choice list plus a search box and device-visibility filter (#322 / discussion #302), both on PadViewModel. Voice macros (#317) add the "Microphone" device class, the Voice Macros preview on DevicesViewModel, DeviceRowViewModel.ShowManageVoicePhrases, and MacroActionType.VoiceListenWhileHeld (54). MacroAction grows the pressure-scaled turbo block (#290). StickConfigItem grows stick trackball momentum (#291), the touchpad partial grows the shared momentum knobs, and PadViewModel grows the Gyro Tilt envelope (#292). The Dashboard's web-controller card gains a URL and QR (#296), its SteamVR row goes tiered (#287), and Settings gains the low-battery notification trio (#293).
v4.4: Head tracking moves from SettingsViewModel to DashboardViewModel, which also gains the Razer Chroma (#373), Logitech LIGHTSYNC (#382) and Razer Sensa (#374) enable-plus-status pairs. The Dashboard's driver-status strip is gone, leaving three bare installed flags and the full cards on Settings. DeviceRowViewModel gains ShowQuickCharge (#372), and IsInternalVirtual grows the handheld://, sensor:// and headtrack:// schemes. PadViewModel gains the ExtendedConfig reseed on a profile switch, the Grip option list and IsLoadingPadSetting (#392), the grouped MacroTypeCatalog picker, menu macro cells and .pficons icon packages (#390). The Profiles page gains external control (#366) and the per-profile polling override (#365).
All ViewModels live in PadForge.App/ViewModels/ (PadForge.ViewModels namespace). Built on CommunityToolkit.Mvvm (ObservableObject, RelayCommand).
ViewModelBase
File: ViewModelBase.cs
Abstract base class for all ViewModels. Extends ObservableObject (INotifyPropertyChanged, SetProperty).
public abstract class ViewModelBase : ObservableObject
{
private string _title = string.Empty;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
}
| Property |
Type |
Description |
Title |
string |
Display title for the view. Used by navigation and page headers. |
Culture / Localization
The constructor subscribes to Strings.CultureChanged for live language switching. When the UI language changes, every ViewModel's OnCultureChanged() runs, letting derived classes refresh culture-dependent text.
Strings.CultureChanged uses a weak event pattern. Instance-method subscribers are stored as (WeakReference<Target>, MethodInfo) pairs, so short-lived ViewModels can be garbage-collected without unsubscribing. Dead entries are pruned on each raise. Static-method subscribers use strong references.
protected ViewModelBase()
{
Strings.CultureChanged += OnCultureChanged; // weak. No GC leak
}
protected virtual void OnCultureChanged() { }
NavControllerItemViewModel
File: MainViewModel.cs (defined alongside MainViewModel)
Sidebar entry for one virtual controller. Shows controller type icon, slot number, and per-type instance label.
| Property |
Type |
Description |
PadIndex |
int |
Zero-based pad slot index (0–15). Read-only. |
Tag |
string |
Navigation tag ("Pad1"–"Pad16"). Computed as $"Pad{PadIndex + 1}". |
SlotNumber |
int |
1-based controller number among active slots. |
InstanceLabel |
string |
Per-type instance label (e.g., "1", "2"). |
IconKey |
string |
Resource key for controller type icon (e.g., "XboxControllerIcon", "DS4ControllerIcon", "NintendoControllerIcon", "ExtendedControllerIcon", "MidiControllerIcon", "KeyboardMouseControllerIcon", "VrControllerIcon"). |
IsEnabled |
bool |
Virtual controller enabled. |
ConnectedDeviceCount |
int |
Mapped physical devices connected. |
MappedDeviceCount |
int |
Physical devices assigned to this slot in config, connected or not. Lets the mini card split "assigned but awaiting" (idle) from "nothing assigned" (cold), the same split the Dashboard card makes. Added by commit 2006ac6b. |
IsInitializing |
bool |
Virtual controller initializing. |
IsVirtualControllerConnected |
bool |
Live virtual controller is alive (created + reporting IsConnected). Drives the sidebar green-vs-yellow indicator so the slot stays green through the HM-inactivity grace window and only turns yellow once the timeout tears the VC down. |
IsCreateFailed |
bool |
The slot's latest virtual-controller create attempt failed (engine createFailed latch). Distinct from awaiting devices, so the flame tooltip does not blame absent devices for a failed create. |
Sidebar wiring: MainWindow code-behind rebuilds one wpf-ui NavigationViewItem per NavControllerItems entry on NavControllerItemsRefreshed (RebuildControllerSection / CreateControllerNavItem), and subscribes each item's PropertyChanged so SlotNumber, InstanceLabel, IconKey (matched as a plain string in UpdateControllerNavItemContent), IsEnabled, and the flame-state flags update the card in place.
MainViewModel
File: MainViewModel.cs
DataContext for: MainWindow
Root ViewModel. Manages navigation state, 16 pad ViewModels, sidebar controller entries, and app-wide status.
Child ViewModels
| Property |
Type |
Description |
Pads |
ObservableCollection<PadViewModel> |
16 pad ViewModels (one per slot). Created in constructor. |
NavControllerItems |
ObservableCollection<NavControllerItemViewModel> |
Sidebar items for created slots. Rebuilt by RefreshNavControllerItems(). |
Dashboard |
DashboardViewModel |
Dashboard overview ViewModel. |
Devices |
DevicesViewModel |
Devices list ViewModel. |
Settings |
SettingsViewModel |
Application settings ViewModel. |
Navigation Properties
| Property |
Type |
Description |
SelectedNavTag |
string |
Current nav tag: "Dashboard", "Pad1"–"Pad16", "Profiles", "Devices", "Settings", "About". Also notifies IsPadPageSelected and SelectedPadIndex. |
IsPadPageSelected |
bool |
True if a Pad page (Pad1–Pad16) is selected. Computed. |
SelectedPadIndex |
int |
Zero-based pad index for the selected Pad page, or -1. Computed. |
SelectedPad |
PadViewModel |
PadViewModel for the selected Pad page, or null. Computed. |
App-wide Status Properties
| Property |
Type |
Default |
Description |
StatusText |
string |
"Ready" |
Status bar text. Plain writes are info-class and decay after ~5 s (StatusDecayMs). Every write restamps the decay window. |
IsEngineRunning |
bool |
false |
Polling loop active. Calls RefreshEngineStatus(). |
HasActiveSlots |
bool |
false |
Any virtual controller slots exist. Calls RefreshEngineStatus(). |
EngineStatusText |
string |
- |
Computed localized text: Engine_Forging ("Forging", engine running with active slots) / Common_Idle ("Idle", running with no created slots) / Common_Stopped ("Stopped"). Note: this is the localized display text. DashboardViewModel.EngineStateKey carries the non-localized "Running" / "Idle" / "Stopped" key for XAML DataTriggers. |
EngineStatusBrush |
Brush |
- |
Computed: Red (#F44336) = Stopped, Amber (#FFB300) = Idle (engine running, no created slots), Green (#4CAF50) = running with active slots (the Forging state). Frozen (thread-safe). |
PollingFrequency |
double |
0 |
Current polling frequency in Hz. |
ConnectedDeviceCount |
int |
0 |
Connected input device count. |
Commands
| Command |
CanExecute |
Description |
StartEngineCommand |
!IsEngineRunning |
Raises StartEngineRequested. Wired by MainWindow code-behind. |
StopEngineCommand |
IsEngineRunning |
Raises StopEngineRequested. |
Events
| Event |
Args |
Description |
StartEngineRequested |
EventArgs |
Start engine. |
StopEngineRequested |
EventArgs |
Stop engine. |
NavControllerItemsRefreshed |
EventArgs |
Raised after RefreshNavControllerItems() completes. MainWindow uses this instead of CollectionChanged to avoid rapid sidebar rebuilds. |
Key Methods
| Method |
Description |
RefreshNavControllerItems() |
Rebuilds sidebar entries from SettingsManager.SlotCreated[]. Computes per-type instance numbers and updates each pad's Title, SlotLabel, TypeInstanceLabel. Raises NavControllerItemsRefreshed only when the active-slot set changed (add / remove / reorder). Property-only changes (icon, label, enabled) update in place without the event. |
RefreshEngineStatus() |
Notifies EngineStatusText and EngineStatusBrush. |
RefreshCommands() |
Notifies CanExecuteChanged on start/stop commands. Call after IsEngineRunning changes. |
SetStatus(string, bool persist = false) |
Writes the status bar with an explicit class. persist: true (failures, prompts a later write always terminates) exempts the message from the decay sweep until the next write. |
ClearDecayedStatus() |
Clears a decayed info message. Re-checks IsStatusDecayDue first, so a write that lands mid-sweep wins. Called from MainWindow's 5 s driver-status timer. |
DashboardViewModel
File: DashboardViewModel.cs
DataContext for: Dashboard page
Overview of all controller slots, engine status, connected devices, and driver status.
Slot Summaries
| Property |
Type |
Description |
SlotSummaries |
ObservableCollection<SlotSummary> |
Cards for created slots. Rebuilt by RefreshActiveSlots(). |
ShowAddController |
bool |
"Add Controller" visible (any type has capacity). |
Engine Status
| Property |
Type |
Default |
Description |
EngineStatus |
string |
"Stopped" |
Localized engine status. |
EngineStateKey |
string |
"Stopped" |
Non-localized state key ("Running", "Stopped", "Idle") for XAML DataTriggers. |
PollingFrequency |
double |
0 |
Polling frequency in Hz. Also notifies PollingFrequencyText. |
PollingFrequencyText |
string |
- |
Computed: formatted frequency (e.g., "987.3 Hz") or dash when zero. |
Device Counts
| Property |
Type |
Description |
TotalDevices |
int |
Total detected devices (online + offline). |
OnlineDevices |
int |
Currently connected devices. |
MappedDevices |
int |
Devices with an active slot mapping. |
Driver Status
HIDMaestro ships inside the executable as a managed SDK, so it never had an install row here. The Dashboard's driver-status strip was removed on 2026-09-02. The full HidHide, Windows MIDI Services, and SteamVR cards live on Settings. What remains on DashboardViewModel are three bare flags with no status text of their own. MainWindow's periodic driver-status refresh writes all three.
| Property |
Type |
Description |
IsHidHideInstalled |
bool |
HidHide installed. Written by the status refresh. No Dashboard surface currently reads it. |
IsMidiServicesInstalled |
bool |
Windows MIDI Services installed. A slot card's MIDI type button shows a "no" cursor and the Main_MIDI_RequiresMidiServices tooltip when false. |
IsSteamVrInstalled |
bool |
SteamVR installed, which gates the VR slot type (#49). A slot card's VR type button shows a "no" cursor and the Main_VR_RequiresSteamVR tooltip when false. The tiered SteamVR status row (#287) lives on the Settings card, which reads the live statics itself. |
DSU Motion Server
| Property |
Type |
Default |
Description |
EnableDsuMotionServer |
bool |
false |
DSU (cemuhook) motion server enabled. |
DsuMotionServerPort |
int |
26760 |
UDP port. Clamped to 1024–65535. |
DsuServerStatus |
string |
"Stopped" |
DSU server status. |
IsDsuServerRunning |
bool |
false |
Serving truth for the DSU card flame (#175): the actual server lifecycle, not the enable checkbox. Set by InputService at the same points that write DsuServerStatus. |
| Command |
Description |
ResetDsuPortCommand |
Resets DsuMotionServerPort to 26760. |
Head Tracking (#355)
A UDP listener on OpenTrack's port plus a FreeTrack 2.0 shared-memory reader, surfaced as the Head Tracker device row. Each setter mirrors into the static PadForge.Common.Input.HeadTrackingRuntime that the poll thread's device sweep reads, so a write lands whatever its source (global load, profile apply, the user). The enable rides profiles as a nullable leg (ProfileData.EnableHeadTracking). Port, FreeTrack toggle, and the two ranges are global. These properties moved here from SettingsViewModel on 2026-09-02. MainWindow's Dashboard dirty-gate allowlist moved with them, so a head-tracking edit still marks the settings file dirty.
| Property |
Type |
Default |
Description |
HeadTrackingEnabled |
bool |
false |
Adds the Head Tracker row. Mirrors to HeadTrackingRuntime.Enabled. |
HeadTrackingUdpPort |
int |
4242 |
Port OpenTrack's "UDP over network" output sends to. Clamped to 1–65535. Mirrors to HeadTrackingRuntime.UdpPort. |
HeadTrackingFreeTrack |
bool |
true |
Read the FreeTrack 2.0 shared memory as well. Mirrors to HeadTrackingRuntime.FreeTrackEnabled. |
HeadTrackingRotationRange |
int |
90 |
Degrees of head rotation at full axis deflection. Clamped to 1–180. |
HeadTrackingTranslationRange |
int |
30 |
Centimeters of head travel at full axis deflection. Clamped to 1–500. |
HeadTrackingStatus |
string |
"Stopped" |
The Head Tracker row's source line (which source is live, or why neither is). Pushed by InputService on the dashboard tick whenever the device's StatusVersion moves. Null coalesces back to "Stopped". |
| Command |
Description |
ResetHeadTrackingPortCommand |
Resets the port to 4242. |
ResetHeadTrackingRotationRangeCommand |
Resets the rotation range to 90. |
ResetHeadTrackingTranslationRangeCommand |
Resets the translation range to 30. |
Vendor Lighting and Haptic Mirrors
Three optional vendor bridges, each an enable plus a status string that null-coalesces to "Stopped". All three landed on the Dashboard on 2026-09-02.
| Property |
Type |
Default |
Description |
EnableChromaLightbar |
bool |
false |
Razer Chroma lightbar mirror (#373). |
ChromaStatus |
string |
"Stopped" |
Chroma mirror status. |
EnableLightsyncLightbar |
bool |
false |
Logitech LIGHTSYNC lightbar mirror (#382). |
LightsyncStatus |
string |
"Stopped" |
LIGHTSYNC mirror status. |
EnableSensaHaptics |
bool |
false |
Razer Sensa HD rumble translation (#374). |
SensaStatus |
string |
"Stopped" |
Sensa translation status. |
Web Controller Server
| Property |
Type |
Default |
Description |
EnableWebController |
bool |
false |
Web controller server enabled. |
WebControllerPort |
int |
8080 |
HTTP/WebSocket port. Clamped to 1024–65535. |
WebControllerStatus |
string |
"Stopped" |
Web controller server status. |
IsWebControllerRunning |
bool |
false |
Serving truth for the Web Controller card flame (#175): actual server lifecycle, not the enable checkbox. Set by InputService alongside WebControllerStatus. |
WebControllerClientCount |
int |
0 |
Connected web controller clients. |
WebControllerUrl |
string |
null |
The live server URL, https:// when the secure lane bound (#296). Empty when stopped. |
WebControllerQr |
ImageSource |
null |
QR of WebControllerUrl so a phone opens the controller by scanning (#296). Null when stopped, which hides the image. |
HasWebControllerQr |
bool |
false |
Whether a QR is available. Drives its visibility. |
| Command |
Description |
ResetWebPortCommand |
Resets WebControllerPort to 8080. |
CopyWebControllerUrlCommand |
Copies WebControllerUrl to the clipboard. No-op when empty. |
Remote Link (#138)
Peer-to-peer controller relay over the LAN. The Dashboard hosts the server toggles and the outbound-connect box. The paired-peer manager and identity-protection state live on SettingsViewModel and are surfaced here through the RemoteLink reference below.
| Property |
Type |
Default |
Description |
RemoteLink |
SettingsViewModel |
- |
The Settings VM, set once at startup. Holds TrustedPeers, NearbyUnpaired, and the identity-protection block so the Dashboard's Remote Link section can show paired peers, identity mode, and nearby PCs in one place. |
EnableRemoteLink |
bool |
false |
Whether the Remote Link server is listening for paired peers. |
AutoReconnect |
bool |
true |
When a paired PC appears on the LAN, establish the link without a click. |
RemoteLinkPort |
int |
27500 |
TCP control + UDP data port. Clamped to 1024–65535. |
RemoteLinkStatus |
string |
"Stopped" |
Remote Link server status text. |
IsRemoteLinkRunning |
bool |
false |
Serving truth for the Remote Link card flame (#175): true only while the link server object is live. The status text can carry identity-unlock errors while the server never started, so the flame keys on this, not the text or the enable checkbox. |
RemoteLinkConnectHost |
string |
"" |
Host (or host:port) the user types to initiate an outbound pairing. |
RemoteLinkMyCode |
string |
"" |
This PC's connection code (#294): a self-contained code embedding the public and private endpoints, minted while Remote Link runs. Empty until the STUN probe returns. Set by InputService. Notifies HasRemoteLinkMyCode. |
HasRemoteLinkMyCode |
bool |
- |
Computed: a shareable code is available to show or copy. |
RemoteLinkNatWarning |
string |
"" |
Network warning (#294): set when the STUN probe found no public address (UDP blocked) or an endpoint-dependent NAT direct connections cannot traverse. Empty when the network looks punchable. Notifies HasRemoteLinkNatWarning. |
HasRemoteLinkNatWarning |
bool |
- |
Computed: RemoteLinkNatWarning is non-empty. |
| Command |
Description |
ResetRemoteLinkPortCommand |
Resets RemoteLinkPort to 27500. |
ConnectToPeerCommand |
Raises ConnectToPeerRequested with the trimmed RemoteLinkConnectHost when non-empty. |
CopyRemoteLinkCodeCommand |
Copies RemoteLinkMyCode to the clipboard. No-op when empty. |
| Event |
Args |
Description |
ConnectToPeerRequested |
string |
User asked to connect/pair with a typed host[:port]. |
Touchpad Overlay (3.2)
On-screen transparent touchpad window that drives the DS4 / DualSense touchpad on the assigned PlayStation slot.
| Property |
Type |
Default |
Description |
EnableTouchpadOverlay |
bool |
false |
Overlay visible. |
EnableMenuOverlay |
bool |
true |
Radial / touch menu overlay (#9): render the MenuOverlayWindow HUD while a menu is engaged. When off, menus still hover and commit blind (the runtime never depends on the window). |
EnableShiftLayerFlyout |
bool |
true |
Show the shift-layer flyout while a slot sits on a non-Base layer. Purely a display of engagement state the poll thread already computes, so turning it off changes nothing about which layer is active. |
EnableProfileOverlay |
bool |
true |
Show the profile-switch overlay when a profile changes. The switch still happens when off, this only suppresses the announcement. |
TouchpadOverlayOpacity |
double |
0.25 |
Surface opacity 0.0–1.0. Notifies TouchpadOverlayOpacityPercent. |
TouchpadOverlayOpacityPercent |
int |
25 |
0–100 integer view of opacity for NumberBox binding. |
TouchpadOverlayMonitor |
int |
0 |
Monitor index the overlay is pinned to (0 = primary). |
TouchpadOverlayLeft / Top |
double |
-1 |
Window position. -1 = centered on the chosen monitor. |
TouchpadOverlayWidth / Height |
double |
500 / 250 |
Window size. Clamped to >= 150 and >= 80 respectively. |
IsTouchpadOverlayRunning |
bool |
false |
Overlay window currently shown. Drives TouchpadOverlayStatus. |
TouchpadOverlayStatus |
string |
- |
Localized status string ("Running" / "Stopped"). |
| Command |
Description |
ResetOpacityCommand |
Resets opacity to default 0.25. |
ResetTouchpadOverlayPositionCommand |
Raises ResetTouchpadOverlayPositionRequested. InputService recenters the live overlay (or seeds defaults if not open) and clears persisted Left / Top. |
| Event |
Description |
ResetTouchpadOverlayPositionRequested |
Recenter request from the UI. |
Key Methods
| Method |
Description |
RefreshActiveSlots(IList<int>, bool) |
Rebuilds SlotSummaries for active slots. Updates display labels, re-applies the remembered focus selection, sets ShowAddController. Called by InputService. |
SetSelectedPad(int) |
Marks the slot whose pad page is in focus so its card wears the selection glow. -1 clears it. The index is remembered, so a later RefreshActiveSlots rebuild re-applies the flag to fresh summaries. |
SlotSummary
File: DashboardViewModel.cs (nested class)
Summary card for one virtual controller slot on the Dashboard.
| Property |
Type |
Default |
Description |
PadIndex |
int |
- |
Zero-based slot index. Read-only. |
SlotLabel |
string |
- |
Display label (e.g., "Virtual Controller 1"). |
MappedDevices |
ObservableCollection<PadViewModel.MappedDeviceInfo> |
null |
Live reference to the pad's mapped-device list, so the crucible card renders a per-device roster with battery glyphs (#175). Set by InputService. Same-reference sets are no-ops. |
DeviceName |
string |
"No device" |
Primary mapped device name. |
BatteryText |
string |
"" |
Battery of the first mapped device reporting one, e.g. "78%". Empty otherwise (#175, issue #167 lane). |
IsActive |
bool |
false |
Has at least one online mapped device. |
IsSelected |
bool |
false |
Whether this slot's pad page is the one currently in focus. Drives the persistent selection glow on the card. Set by DashboardViewModel.SetSelectedPad, not the same as IsActive. |
IsVirtualControllerConnected |
bool |
false |
Virtual controller connected. Notifies StatusText. |
IsInitializing |
bool |
false |
Virtual controller initializing. Notifies StatusText. |
IsCreateFailed |
bool |
false |
The slot's latest create attempt failed (engine createFailed latch). Its own status: a failed slot with online devices is neither forging nor awaiting devices. Notifies StatusText. |
MappedDeviceCount |
int |
0 |
Devices mapped to this slot. Notifies HasMappedDevices and StatusText. |
HasMappedDevices |
bool |
- |
Computed: MappedDeviceCount > 0. Ember heat gating (#175): cards with zero mappings stay cold (no ember rim, no glow, steel seg tile) even when enabled. |
ConnectedDeviceCount |
int |
0 |
Mapped devices connected. |
StatusText |
string |
"Idle" |
Ember status vocabulary (#175). For an enabled, non-initializing slot the getter checks three branches in order. A failed create returns Main_VcFailed first. Then zero mappings returns Dashboard_StatusCold ("Cold"). It returns Main_AwaitingDevices ("Awaiting devices") for a mapped slot with nothing connected once the live VC is gone. While the VC survives the inactivity grace (60 s default), a device dropout must not flap the card to awaiting. Every other case passes through the engine-assigned setter value: Main_Active ("Forging") when a device is online, Common_Idle ("Idle"), Common_Disabled ("Disabled"), or Main_Initializing ("Initializing"). The old "No mapping" string is intercepted by the Cold branch. |
IsEnabled |
bool |
true |
Slot enabled for output. |
SlotNumber |
int |
1 |
1-based controller number among active slots. |
TypeInstanceLabel |
string |
"1" |
Per-type instance label. |
OutputType |
VirtualControllerType |
Xbox |
Virtual controller output type. (XML on-disk name is "Microsoft" via [XmlEnum] for v2/early-v3 back-compat. In-code identifier is Xbox.) |
StageLedger |
ObservableCollection<SlotStageInfo> |
empty |
Pipeline stage ledger (#175 item 10): one entry per configuration stage the slot's assigned devices actually have (sticks / triggers / gyro / lighting / touchpad / audio), in that order. Rebuilt per slot by InputService.RefreshSlotStageLedger on its 1 s slow lane. Entries mutate in place when membership is unchanged so the card doesn't re-template. |
SlotStageInfo
File: DashboardViewModel.cs
One stage entry in a slot card's pipeline heat ledger (#175 item 10).
| Property |
Type |
Description |
Kind |
string |
Stable stage identity ("Sticks".."Audio"). Membership comparisons key on this so in-place updates are possible. Read-only. |
Glyph |
string |
Segoe font glyph for the font stages (same characters as the matching tab page headers). Empty for shape stages. Read-only. |
IsStickShape |
bool |
True for the Sticks entry. Rendered with the Zacksly stick ring + disc geometries instead of a font glyph. Read-only. |
IsTriggerShape |
bool |
True for the Triggers entry. Rendered with the Zacksly trigger body geometry. Read-only. |
IsHot |
bool |
True when the stage carries a non-default configuration on any assigned device. Ember when hot, ashen steel when inert. |
Summary |
string |
Composite change key for the tooltip readout (all lines joined). Empty when inert, which disables the tooltip. Notifies HasSummary. |
HasSummary |
bool |
Computed: tooltip gate, no tooltip on inert stages. |
SummaryLines |
ObservableCollection<StageSummaryLine> |
Per-device readout lines for the hover tooltip, in binding order. Devices still at defaults read STOCK. |
StageSummaryLine
File: DashboardViewModel.cs
One tooltip line of a stage's readout: device-class glyph, device name, then mono value tokens.
| Property |
Type |
Description |
DeviceGlyph |
string |
Device-class glyph. Empty for slot-level lines. |
DeviceName |
string |
Body-face device name, carrying its trailing " · " separator. Empty for slot-level lines like the audio master volume. |
Tokens |
string |
Mono value tokens. |
SettingsViewModel
File: SettingsViewModel.cs
DataContext for: Settings page
Application-level settings: theme, language, drivers, profiles, and engine configuration.
Theme
| Property |
Type |
Default |
Description |
SelectedThemeIndex |
int |
0 |
0 = System, 1 = Light, 2 = Dark. Raises ThemeChanged. |
| Event |
Args |
Description |
ThemeChanged |
int |
Raised when theme selection changes. Arg = theme index. |
Language
| Property |
Type |
Description |
AvailableLanguages |
ObservableCollection<CultureInfo> |
UI languages: en, de, fr, ja, ko, zh-Hans, pt-BR, es, it, nl. |
SelectedLanguage |
CultureInfo |
Current UI language. Applies immediately via Strings.ChangeCulture(). Defaults to current culture or English. |
| Member |
Type |
Description |
LanguageCode |
string |
Persisted language code for serialization. |
SetLanguageFromCode(string) |
method |
Sets language from persisted code on startup without raising CultureChanged. |
ResetLanguageToSystemDefault() |
method |
Sets SelectedLanguage back to the OS UI culture (or English when unsupported). |
Driver Status: HIDMaestro
HIDMaestro is shipped as an embedded managed SDK (HIDMaestro.Core, bundled at Resources/HIDMaestro/HIDMaestro.Core.dll). The user-mode driver still needs to be registered with Windows once. That happens inside the engine via HMContext.InstallDriver() (called from InputManager.Step5.VirtualDevices.cs:EnsureHMaestroContext), not through DriverInstaller. The SettingsViewModel exposes a read-only version string read from the bundled assembly at startup.
| Property |
Type |
Description |
HIDMaestroVersion |
string |
HIDMaestro SDK version. Initialized from the embedded HIDMaestro.Core assembly via GetEmbeddedHidMaestroVersion(). |
Driver Status: HidHide
| Property |
Type |
Description |
IsHidHideInstalled |
bool |
HidHide installed. |
HidHideStatusText |
string |
Computed: "Installed" / "Not Installed". |
HidHideVersion |
string |
HidHide version. |
HidHideWhitelistPaths |
ObservableCollection<string> |
Whitelisted application paths. |
SelectedWhitelistPath |
string |
Selected whitelist path. Refreshes RemoveWhitelistPathCommand. |
| Command |
CanExecute |
Description |
InstallHidHideCommand |
!IsHidHideInstalled |
Raises InstallHidHideRequested. |
UninstallHidHideCommand |
IsHidHideInstalled && !HasAnyHidHideDevices() |
Raises UninstallHidHideRequested. |
AddWhitelistPathCommand |
IsHidHideInstalled |
Raises AddWhitelistPathRequested. |
RemoveWhitelistPathCommand |
SelectedWhitelistPath != null |
Removes selected path, raises WhitelistChanged. |
| Event |
Description |
InstallHidHideRequested |
Install HidHide. |
UninstallHidHideRequested |
Uninstall HidHide. |
AddWhitelistPathRequested |
Add whitelist path (opens file dialog). |
WhitelistChanged |
Whitelist was modified (add or remove). |
Driver Status: Windows MIDI Services
| Property |
Type |
Description |
IsMidiServicesInstalled |
bool |
Windows MIDI Services available. |
MidiServicesStatusText |
string |
Computed: "Installed" / "Not Installed". |
MidiServicesVersion |
string |
MIDI Services version. |
IsMidiOsSupported |
bool |
Static: true if OS build >= 26100 (Win11 24H2). |
MidiOsSupported |
bool |
Instance forwarder over the static above. A XAML Binding path resolves against the DataContext instance and cannot reach a static member, so SettingsPage binds this one. Never raises PropertyChanged because the OS build cannot change while the app runs. |
| Command |
CanExecute |
Description |
InstallMidiServicesCommand |
!IsMidiServicesInstalled && IsMidiOsSupported |
Raises InstallMidiServicesRequested. |
UninstallMidiServicesCommand |
IsMidiServicesInstalled && !HasAnyMidiSlots() |
Raises UninstallMidiServicesRequested. |
Driver Status: SteamVR (#49)
The VR slot type needs SteamVR present. PadForge can install it Steam-free through steamcmd, and it only offers to remove the copy it created itself.
| Property |
Type |
Description |
IsSteamVrInstalled |
bool |
A SteamVR install of either shape is present (Steam client, or the Steam-free steamcmd one). Notifies SteamVrStatusText and ShowSteamVrUninstall. |
SteamVrStatusText |
string |
Computed, tiered (#287), deepest true state wins: "Not Installed", then controllers-live / driver-connected / running read off HMaestroVRController.GlobalDriverStatus() and OpenVrConsumerService.ServerConnected, falling back to "Installed". This is where the SteamVR status reads now that the Dashboard's driver strip is gone. |
SteamVrInstallDir |
string |
Where the Steam-free install will land (seeded from DriverInstaller.SteamVrInstallDir). Not persisted by PadForge: after a successful install the HIDMaestro path hint is the durable record. |
IsSteamVrOwned |
bool |
The present install is the Steam-free one PadForge created. A Steam-client install is never PadForge's to remove. |
ShowSteamVrUninstall |
bool |
Computed: IsSteamVrInstalled && IsSteamVrOwned. |
| Command |
CanExecute |
Description |
InstallSteamVrCommand |
!IsSteamVrInstalled |
Raises InstallSteamVrRequested. |
UninstallSteamVrCommand |
IsSteamVrInstalled && IsSteamVrOwned && !HasAnyVrSlots() |
Raises UninstallSteamVrRequested. |
| Event |
Description |
InstallSteamVrRequested |
Install SteamVR Steam-free. |
UninstallSteamVrRequested |
Remove the PadForge-owned install. |
| Method |
Description |
RefreshSteamVrStatus() |
Raises the SteamVR row's change notification. Called by MainWindow's periodic status refresh, since SteamVrStatusText reads live statics rather than stored fields. |
Driver Uninstall Guards
| Member |
Type |
Description |
HasAnyMidiSlots |
Func<bool> |
Set by MainWindow. True if any slot uses MIDI. Blocks UninstallMidiServicesCommand. |
HasAnyVrSlots |
Func<bool> |
Set by MainWindow. True if any created slot is a VR slot. Blocks UninstallSteamVrCommand. |
HasAnyHidHideDevices |
Func<bool> |
Set by MainWindow. True if any device has HidHide enabled. Blocks UninstallHidHideCommand. |
RefreshDriverGuards() |
method |
Re-evaluates uninstall CanExecute for HidHide, MIDI Services, and SteamVR. Call after slot creation/deletion/type changes. |
Engine Settings
| Property |
Type |
Default |
Description |
AutoStartEngine |
bool |
true |
Auto-start engine on launch. |
MinimizeToTray |
bool |
false |
Minimize to system tray instead of taskbar. |
BatteryNotifyEnabled |
bool |
true |
Low-battery notification master toggle (#293). |
BatteryNotifyThreshold |
int |
15 |
Percent at or below which the edge-triggered notification fires. Clamped 5–50. |
BatteryNotifyVibrate |
bool |
false |
Also buzz the device on the low-battery edge, using the identify pulse train. |
AssignOfferNewDevice |
bool |
true |
Assignment Prompts: offer a device PadForge has never seen before to the virtual controller whose page is open when it connects. |
AssignOfferEmptySlot |
bool |
true |
Assignment Prompts: offer any connecting device, new or already known, to the open virtual controller when that slot has no devices assigned. |
HandheldButtonsEnabled |
bool |
false |
Handheld PC Buttons (#343): adds the per-machine Hidden Buttons row and the System Motion row. The setter mirrors into HandheldButtonRegistry.FeatureEnabled, which the engine sweep reads. |
StartMinimized |
bool |
false |
Start minimized. |
DiagnosticsLoggingEnabled |
bool |
false |
Diagnostics logging (#303): mirrors the engine's in-memory event ring to diagnostics.log continuously. Persisted, so it survives restarts and covers auto-started sessions where a launch flag cannot. |
DiagnosticsFolderPath |
string |
- |
Read-only forwarder over DiagnosticsLogControl.Folder: where the log and snapshots land. Shown on the Diagnostics card. |
StartAtLogin |
bool |
false |
Auto-start at login. |
EnablePollingOnFocusLoss |
bool |
true |
Continue polling on focus loss. |
PollingOverrideNote |
string |
null |
Why the global polling knob is not in charge (#365 follow-up). While the active profile carries a polling override, this names the profile and the rate it imposes, and the Settings page shows it under the knob. Null or empty collapses the note. Written by InputService.ApplyEffectivePollingRate, the same owner that writes the live rate, so note and loop cannot disagree. |
PollingRateMs |
int |
1 |
Polling interval in ms. Clamped to 1–16. |
HmInactivityDestroyTimeoutSeconds |
int |
60 |
Seconds the engine waits for any mapped device to return online before tearing down the live HM virtual controller and freeing its kernel slot. Clamped to 0–3600. 0 disables the timeout. The slot's configuration (mappings, profile, position, enabled state) is preserved end-to-end. Only the live VC is destroyed. Once mapped devices come back online, the VC recreates automatically at the same visual position. Surviving Xbox HM VCs at higher visual positions bubble down to keep xinputhid indices contiguous after the teardown. |
EnableInputHiding |
bool |
true |
Master switch for device hiding (HidHide + input hooks). |
KeepHidHideCloaksBetweenLaunches |
bool |
false |
When true, HidHide cloaks stay asserted after PadForge exits so other apps still see the physical devices hidden. When false (default), cloaks clear on shutdown. |
Settings File
| Property |
Type |
Description |
SettingsFilePath |
string |
Path to the loaded settings file. |
HasUnsavedChanges |
bool |
Unsaved changes exist. |
| Command |
Description |
SaveCommand |
Raises SaveRequested. |
ReloadCommand |
Raises ReloadRequested. |
ResetCommand |
Raises ResetRequested. |
OpenSettingsFolderCommand |
Raises OpenSettingsFolderRequested. |
| Event |
Description |
SaveRequested |
Save settings. |
ReloadRequested |
Reload from disk. |
ResetRequested |
Reset settings. |
OpenSettingsFolderRequested |
Open settings folder. |
Diagnostic Info
| Property |
Type |
Description |
SdlVersion |
string |
SDL3 version. |
ApplicationVersion |
string |
Application version. |
RuntimeVersion |
string |
.NET runtime version. |
Display Preferences
| Property |
Type |
Default |
Description |
Use2DControllerView |
bool |
false |
Show 2D controller view instead of 3D. |
| Member |
Type |
Default |
Description |
EnableCommunityConfigLookup |
bool |
false |
Master opt-in for the Steam Workshop clients. Persisted, dirty-gate allowlisted. |
ShowLegacyWorkshopConfigs |
bool |
false |
Legacy sub-toggle. Persisted, dirty-gate allowlisted. |
ClearWorkshopCacheCommand |
RelayCommand |
|
Raises ClearWorkshopCacheRequested, handled by MainWindow (empties SteamWorkshopCache). |
CheckWorkshopUpdatesCommand |
RelayCommand |
|
Raises CheckWorkshopUpdatesRequested, handled by MainWindow (batched provenance check). |
BrowseCommunityConfigsCommand |
RelayCommand |
|
The Profiles-page entry. No CanExecute gate: with the opt-in off the dialog opens on its cold state and offers the enable step. |
BrowseStarterProfilesCommand |
RelayCommand |
|
Opens the starter-profile gallery (#256) through BrowseStarterProfilesRequested. Always enabled: the catalog ships in the box, so there is no opt-in and no network. |
Remote Link Peer Manager (#138)
The trusted-peer list and identity-protection mode live here. The Dashboard's Remote Link section binds them through DashboardViewModel.RemoteLink.
| Property |
Type |
Description |
TrustedPeers |
ObservableCollection<RemoteLinkTrustedPeer> |
Trusted paired PCs, shown in the Settings peer manager. |
NearbyUnpaired |
ObservableCollection<RemoteLinkNearbyPeer> |
PadForge PCs discovered on the LAN that aren't paired yet. |
HasNearbyUnpaired |
bool |
Computed: NearbyUnpaired.Count > 0. |
IdentityProtectionModes |
IReadOnlyList<string> |
Dropdown options in index order: 0 Secure, 1 password-portable, 2 open-portable. |
IdentityProtectionModeIndex |
int |
Selected protection mode (0/1/2). User changes raise IdentityProtectionModeChangeRequested. Programmatic reverts use SetIdentityProtectionModeSilently so no event re-fires. Notifies IdentityProtectionHint. |
IdentityProtectionHint |
string |
Computed: one-line guidance under the dropdown for the selected mode. |
| Command |
Description |
RevokeAllPeersCommand |
Raises PeerRevokeAllRequested. |
| Event |
Args |
Description |
PeerRevokeRequested |
string |
Revoke one peer by fingerprint. |
PeerRevokeAllRequested |
- |
Revoke every peer. |
PeerRenameRequested |
string, string |
Rename a peer (fingerprint, new name), persisted. |
PeerConnectRequested |
string |
Connect to a paired-but-offline peer (host:port). |
IdentityProtectionModeChangeRequested |
int |
User picked a different protection mode (new index). |
| Method |
Description |
RefreshTrustedPeers(IEnumerable<PeerTrust>, IReadOnlyCollection<string>) |
Rebuilds TrustedPeers from the trust store. Called on pair / revoke / rename, not for online refresh. |
UpdatePeerOnlineStatus(IReadOnlyCollection<string>) |
Updates each peer's online dot in place from the live connection set. |
UpdatePeerReachability(IReadOnlyDictionary<string,string>) |
Updates each paired peer's reachable host:port in place from LAN discovery. |
SetNearbyUnpaired(IEnumerable<RemoteLinkNearbyPeer>) |
Replaces the nearby-unpaired list. |
SetIdentityProtectionModeSilently(int) |
Sets the mode without raising the change request (init from settings, or revert a canceled switch). |
First-Run, Window, and Foreground State
| Property |
Type |
Default |
Description |
ForegroundExeName |
string |
"-" |
Foreground exe filename the auto-switch monitor last saw (#175 item 8). Runtime-only, fed at 1 Hz by InputService's UI timer, never persisted. |
IsForegroundMatched |
bool |
false |
True while the foreground exe matches a profile. Runtime-only. |
NoProfileHasExecutables |
bool |
- |
Computed: no profile carries an executable match rule (#175 item 8). |
LegacyDriverCleanupOffered |
bool |
false |
True after the v3 first-run cleanup wizard has been shown. Persists so the dialog appears once. |
FirstRunTourCompleted |
bool |
false |
True once the first-run welcome tour is completed or skipped. Replaces the pre-v4 beside-exe marker file. |
MainWindowLeft / Top |
double |
-1 |
Main window position (profile-independent). |
MainWindowWidth / Height |
double |
1100 / 720 |
Main window size. |
MainWindowState |
int |
0 |
Persisted window state (normal / maximized). |
MainWindowFullScreen |
bool |
false |
Full-screen flag. |
Profiles
| Property |
Type |
Default |
Description |
EnableAutoProfileSwitching |
bool |
false |
Auto-profile switching enabled. |
EnableExternalControl |
bool |
false |
Serve the external-control named pipe (#366) so a launcher or script can activate and deactivate profiles from outside PadForge. InputService watches this property and starts or stops the pipe live. The Profiles page carries the checkbox beside the shortcut combo and the foreground match, because this is a third way a profile activates rather than a global engine service. |
ProfileItems |
ObservableCollection<ProfileListItem> |
empty |
Profile entries for UI list. |
ProfileShortcuts |
ObservableCollection<ProfileShortcutViewModel> |
empty |
Profile-switch shortcut rows for the ProfilesPage shortcuts card. |
SelectedProfile |
ProfileListItem |
null |
Selected profile. Refreshes delete/edit/load/export commands. |
ActiveProfileInfo |
string |
"Default" |
Active profile display text. |
| Command |
CanExecute |
Description |
NewProfileCommand |
always |
Raises NewProfileRequested. |
SaveAsProfileCommand |
always |
Raises SaveAsProfileRequested. |
DeleteProfileCommand |
SelectedProfile != null && !IsDefault |
Raises DeleteProfileRequested. |
EditProfileCommand |
SelectedProfile != null && !IsDefault |
Raises EditProfileRequested. |
LoadProfileCommand |
SelectedProfile != null |
Raises LoadProfileRequested or RevertToDefaultRequested if default. |
ExportProfileCommand |
SelectedProfile != null |
Raises ExportProfileRequested. Exports the selected profile as a .pfprofile. The Default entry exports a snapshot of the current settings. |
ImportProfileCommand |
always |
Raises ImportProfileRequested. Imports a .pfprofile. |
| Event |
Description |
RevertToDefaultRequested |
Revert to default profile. |
NewProfileRequested |
Create new empty profile. |
SaveAsProfileRequested |
Save current settings as profile. |
DeleteProfileRequested |
Delete selected profile. |
EditProfileRequested |
Edit selected profile metadata. |
LoadProfileRequested |
Load selected profile. |
ExportProfileRequested |
Export selected profile to a .pfprofile. |
ImportProfileRequested |
Import a .pfprofile. |
The delete / edit / load / export commands refresh their own CanExecute inline in the SelectedProfile setter. There is no separate refresh method.
ProfileListItem
File: SettingsViewModel.cs (nested class)
Display item for a profile in the Settings list.
| Property |
Type |
Description |
Id |
string |
Profile identifier. |
IsActive |
bool |
Whether this profile is the currently active one. Drives the lit flame in the Profiles list (#175). |
Name |
string |
Display name. |
Executables |
string |
Comma-separated exe names for auto-switch (display form). Notifies HasExecutables, AutoSwitchRuleSummary. |
ExecutablePaths |
string |
Pipe-separated full executable paths (the ForegroundMonitorService match source). Feeds the card's exe icon and mono exe line (#175). |
FirstExecutablePath |
string |
Computed: full path of the first executable, or null. The card's icon converter File.Exists-gates this. |
FirstExecutableName |
string |
Computed: file name of the first executable, or null. |
SecondExecutableName |
string |
Computed: file name of the second executable, or null. The card face shows up to two names before the +N marker. |
ExtraExecutablesSuffix |
string |
Computed: locale-neutral "+N" marker for rules beyond the two shown names, or null. |
HasExecutables |
bool |
Computed: the profile carries any executable match rule. Half of the auto-switch chip gate (#175). |
AutoSwitchRuleSummary |
string |
Computed: exe-match rule summary for the cold auto-switch chip, the display exe list under a localized prefix. |
TopologyLabel |
string |
Slot topology summary. |
HasNoSlots |
bool |
Computed: all type counts are zero. |
XboxCount |
int |
Number of Xbox slots in this profile. |
PlayStationCount |
int |
Number of PlayStation slots. |
NintendoCount |
int |
Number of Nintendo slots (#215). |
ExtendedCount |
int |
Number of Extended slots. |
MidiCount |
int |
Number of MIDI slots. |
KbmCount |
int |
Number of Keyboard+Mouse slots. |
VrCount |
int |
Number of VR slots (#49). |
IsDefault |
bool |
Computed: true if Id == "__default__". |
| Constant |
Value |
Description |
DefaultProfileId |
"__default__" |
Sentinel ID for the Default profile. |
DevicesViewModel
File: DevicesViewModel.cs
DataContext for: Devices page
All detected input devices (online and offline) with raw input state for the selected device.
Device List
| Property |
Type |
Description |
Devices |
ObservableCollection<DeviceRowViewModel> |
All known devices. Updated by InputService on change. |
SelectedDevice |
DeviceRowViewModel |
Selected device. Notifies HasSelectedDevice, refreshes slot buttons and command state. |
HasSelectedDevice |
bool |
Computed: SelectedDevice != null. |
Device Counts
| Property |
Type |
Description |
TotalCount |
int |
Total detected devices. |
OnlineCount |
int |
Connected devices. |
Type Facet Filter (#175)
Chip row above the device list that filters by device class. The chips bind their labels to the count properties. Clicking one sets SelectedFacet.
| Property |
Type |
Default |
Description |
SelectedFacet |
string |
"ALL" |
Active facet token: ALL / GAMEPAD / JOYSTICK / WHEEL / KEYBOARD / MOUSE / OTHER. Locale-neutral literals the chip DataTriggers key off. The setter routes through SetFacet. |
FacetCountGamepad |
int |
0 |
Rows behind the GAMEPAD chip (the stick classes: Gamepad / FirstPerson / Supplemental). |
FacetCountJoystick |
int |
0 |
Rows behind the JOYSTICK chip (joysticks + flight sticks). |
FacetCountWheel |
int |
0 |
Rows behind the WHEEL chip. |
FacetCountKeyboard |
int |
0 |
Rows behind the KEYBOARD chip. |
FacetCountMouse |
int |
0 |
Rows behind the MOUSE chip. |
FacetCountOther |
int |
0 |
Rows behind the OTHER chip (touchpads, MIDI, NFC, microphones, consumer collections, unclassified). |
| Method |
Description |
SetFacet(string) |
Applies the facet filter to the default collection view the ListBox binds through, so the XAML keeps a plain ItemsSource="{Binding Devices}". ALL clears the filter. No-op when the token is unchanged or empty. |
The ALL chip binds TotalCount. RefreshCounts() recomputes every facet count and re-runs the active filter so rows whose type key changed in place re-bucket.
| Property |
Type |
Description |
RawAxes |
ObservableCollection<AxisDisplayItem> |
Axis values (progress bars). |
RawButtons |
ObservableCollection<ButtonDisplayItem> |
Button states (circles). |
RawPovs |
ObservableCollection<PovDisplayItem> |
POV hat values (compass). |
KeyboardKeys |
ObservableCollection<KeyboardKeyItem> |
Keyboard layout items. |
IsKeyboardDevice |
bool |
Selected device is a keyboard. |
IsMouseDevice |
bool |
Selected device is a mouse. |
IsTouchpadDevice |
bool |
Selected device exposes a touchpad surface (DS4 / DualSense / Steam Controller / Steam Deck / overlay touchpad). |
IsMidiDevice |
bool |
Selected device is a MIDI input (#128). Drives the live piano / CC preview off LiveMidi. |
IsNfcDevice |
bool |
Selected device is an NFC reader (#150). Populates NfcTags. |
IsMicrophoneDevice |
bool |
Selected device is a standalone microphone (#317). Hides the numbered button grid, since every button it has is a named phrase. |
ShowVoicePhrases |
bool |
Selected device carries voice phrases (a microphone row, or a Bluetooth DualSense whose embedded mic is not a system device). Drives the Voice Macros section, the voice twin of the NFC Tags section. |
VoicePhrases |
ObservableCollection<NfcTagDisplayItem> |
Named phrase rows for the live preview (#317): "Any Phrase" plus each registered phrase. Reuses the NFC row item. Rebuilt by RebuildVoicePhrases(int). |
VoiceButtonBase |
int |
Raw-button base the current device's phrase buttons sit at (0 on a microphone row, the reserved pad range on a DualSense). -1 when none. Private setter. |
IsConsumerDevice |
bool |
Selected device is a Consumer Control collection (#168). Populates ConsumerButtons. |
IsHeadsetMotionDevice |
bool |
Selected device is a Sony headset head tracker (#188). Collapses the raw Axes / Buttons sections, since a motion-only source has neither and the gyro readout is its whole preview. |
IsHandheldDevice |
bool |
Selected device is the handheld hidden-buttons row (#343). Drives the named button list in place of the numbered grid, the same treatment NFC gets. |
IsSystemMotionDevice |
bool |
Selected device is the machine's own motion sensor row (#343). Motion only, so the raw Axes / Buttons sections collapse like the headset tracker's. |
IsHeadTrackerDevice |
bool |
Selected device is the head tracker row (#355): six named axes, no buttons, plus a source line. |
HeadTrackerStatus |
string |
Which head-tracking source is live, for the details pane (#355). Written by the preview loop. Null coalesces to empty. |
HeadTrackerStatusVersion |
int |
Internal. The device StatusVersion the current HeadTrackerStatus was built from, -1 for none. |
HeadTrackerStatusDevice |
object |
Internal. The device instance that version belongs to. A reopen (port change, FreeTrack toggle) builds a new device whose StatusVersion restarts at 0 and collides with the cached 0, so the cache compares instance as well as version. |
HandheldButtons |
ObservableCollection<NfcTagDisplayItem> |
Named learned-button rows for the handheld preview (#343), each lighting while its button is down. Reuses the NFC row item, with Uid carrying the delivery description. Rebuilt by RebuildHandheldButtons(). |
NfcTags |
ObservableCollection<NfcTagDisplayItem> |
Registered-tag rows for the NFC preview. Rebuilt by RebuildNfcTags(). |
ConsumerButtons |
ObservableCollection<ConsumerButtonDisplayItem> |
Named media-key chips for the Consumer Control preview. |
LiveMidi |
MidiInputState |
Latest MIDI input snapshot the piano / CC preview polls. Plain property, no change notification. |
HasTouchpadData |
bool |
At least one finger is currently in contact. |
TouchpadX0–TouchpadX4 / TouchpadY0–TouchpadY4 |
double |
Per-finger normalized position (0.0–1.0) on the first pad. Five contacts, not two. |
TouchpadDown0–TouchpadDown4 |
bool |
Per-finger contact state. |
HasSecondTouchpadData |
bool |
The device exposes a second pad (Steam Controller / Steam Deck). Switches the labels to TouchpadLabel / Touchpad2Label. |
TouchpadLabel / Touchpad2Label |
string |
Computed pad captions. Touchpad2Label is always the localized touchpad word plus " 2". TouchpadLabel picks up its own " 1" only when a second pad exists. |
Pad2X0–Pad2X4 / Pad2Y0–Pad2Y4 / Pad2Down0–Pad2Down4 |
double / bool |
Second-pad contacts, same five-finger shape. |
MouseMotionX |
double |
Mouse X motion. |
MouseMotionY |
double |
Mouse Y motion. |
MouseScrollIntensity |
double |
Normalized scroll intensity (−1 to 1). |
SelectedButtonTotal |
int |
Button count on selected device. |
HasRawData |
bool |
Raw state data available. |
Gyroscope / Accelerometer
| Property |
Type |
Description |
HasGyroData |
bool |
Gyroscope data available. |
HasAccelData |
bool |
Accelerometer data available. |
GyroX |
double |
Gyroscope X value. |
GyroY |
double |
Gyroscope Y value. |
GyroZ |
double |
Gyroscope Z value. |
AccelX |
double |
Accelerometer X value. |
AccelY |
double |
Accelerometer Y value. |
AccelZ |
double |
Accelerometer Z value. |
HasAccelAuxData |
bool |
Aux (left-side) accelerometer data available (#199). True for a Nunchuk behind a MotionPlus or a left Joy-Con, where the device carries a second accelerometer alongside the primary one. |
AccelAuxX |
double |
Aux accelerometer X value. |
AccelAuxY |
double |
Aux accelerometer Y value. |
AccelAuxZ |
double |
Aux accelerometer Z value. |
HasGyroAuxData |
bool |
Aux (left-side) gyro data available (#252): the left Joy-Con of a combined pair. Paired with the aux accel readout so the user can see which half a reading comes from before binding it. Cleared by ClearRawState(). |
GyroAuxX |
double |
Aux gyro X value. |
GyroAuxY |
double |
Aux gyro Y value. |
GyroAuxZ |
double |
Aux gyro Z value. |
| Property |
Type |
Description |
ActiveSlotItems |
ObservableCollection<SlotButtonItem> |
Slot toggle buttons. Only includes created/active slots. |
Commands
| Command |
CanExecute |
Description |
RefreshCommand |
always |
Raises RefreshRequested. Force-refreshes device list. |
PairCommand |
always |
Raises PairRequested. Opens the Bluetooth pairing flow for controllers that need an in-app pairing ceremony (Wii controllers, #116). |
AssignToSlotCommand |
HasSelectedDevice |
RelayCommand<int>. Raises AssignToSlotRequested with slot index (0–15). |
ToggleSlotCommand |
HasSelectedDevice |
RelayCommand<int>. Toggles slot assignment. Raises ToggleSlotRequested. |
HideDeviceCommand |
HasSelectedDevice |
Raises HideDeviceRequested with the selected device's InstanceGuid. The row itself stays in the collection until the service refreshes the list. |
RemoveDeviceCommand |
HasSelectedDevice |
Removes the row from Devices, clears SelectedDevice, raises RemoveDeviceRequested, then RefreshCounts(). |
Events
| Event |
Args |
Description |
RefreshRequested |
EventArgs |
Refresh requested. |
PairRequested |
EventArgs |
User opened the Bluetooth pairing flow (#116). |
AssignToSlotRequested |
int |
Slot index. |
ToggleSlotRequested |
int |
Slot index. |
HideDeviceRequested |
Guid |
Instance GUID. |
RemoveDeviceRequested |
Guid |
Instance GUID. |
DeviceHidingChanged |
Guid |
HidHide or ConsumeInput toggle changed. |
Key Methods
| Method |
Description |
RebuildRawStateCollections(IReadOnlyList<int> axisIndices, IReadOnlyList<int> buttonIndices, int povCount, bool isKeyboard, bool isMouse, bool isTouchpad, bool isMidi, bool isNfc, IReadOnlyList<ConsumerButtonDisplayItem> consumerButtons, bool isHeadsetMotion, int voiceButtonBase, bool isMicrophone, bool isHandheld, bool isSystemMotion, bool isHeadTracker) |
Internal. Rebuilds axis/button/POV collections for a new device. axisIndices is the sparse list of axis slots the device actually exposes (from SupportedAxisIndices / CapAxisIndices through InputService.ResolveAxisIndices), so each AxisDisplayItem.Index is the real state.Axis[] slot and its label reads Axis {slot}, gaps included. buttonIndices is the sparse list of button positions the device actually exposes (each item's value is stored verbatim as both Index and DisplayNumber). The class-flag booleans default to false, consumerButtons to null, and voiceButtonBase to -1. Handles keyboard, mouse, touchpad, MIDI, NFC, Consumer Control, headset-motion, handheld, system-motion, head-tracker, and voice-phrase cases. |
ClearRawState() |
Internal. Clears all raw state display data, including every class flag, the head-tracker status cache, and the NFC / voice / handheld / Consumer collections. |
RebuildNfcTags() |
Repopulates NfcTags from NfcTagRegistry: "Any NFC Tag" first at button 0, then each registered tag at its stable button index. |
RebuildHandheldButtons() |
Repopulates HandheldButtons from HandheldButtonRegistry.Entries (#343). Called on selection and whenever the registry changes. |
RebuildVoicePhrases(int) |
Repopulates VoicePhrases from VoicePhraseRegistry at the passed button base. A negative base clears the list. |
RefreshSlotButtons() |
Rebuilds ActiveSlotItems from created slots and selected device assignments. |
RefreshAllSlotBadges() |
Re-derives every row's SlotBadges after a slot set changes. |
FindByGuid(Guid) |
Finds a DeviceRowViewModel by instance GUID. |
RefreshCounts() |
Updates TotalCount and OnlineCount, recomputes every facet count, and re-runs the active filter. |
NotifyDeviceHidingChanged(Guid) |
Raises DeviceHidingChanged. |
Internal State
| Member |
Type |
Description |
LastRawStateDeviceGuid |
Guid |
Tracks which device's collections are populated. Prevents unnecessary rebuilds. |
HeadTrackerAxisName(int slot) |
static string |
Localized name of a head-tracker axis slot (#355): Yaw, Pitch, Roll, X, Y, Z for slots 0–5. Null past the six. |
File: DevicesViewModel.cs
Single slot toggle button on the Devices page.
| Property |
Type |
Description |
PadIndex |
int |
Zero-based slot index (0–15). |
SlotNumber |
int |
1-based number among active slots. |
IsAssigned |
bool |
Selected device is assigned to this slot. |
VcType |
VirtualControllerType |
The slot's output type. Notifies the two icon properties below. |
TypeGeometry |
Geometry |
Branded type icon path from SlotTypeIconMap.GeometryFor. Null for the glyph-drawn types. |
TypeGlyph |
string |
Segoe glyph from SlotTypeIconMap.GlyphFor for the types with no path geometry (MIDI E8D6, Keyboard+Mouse E961, VR F119). |
AxisDisplayItem
File: DevicesViewModel.cs
Display item for a single axis value.
| Property |
Type |
Description |
Index |
int |
The real state.Axis[] slot (sparse, gaps preserved). |
Name |
string |
Display name from that slot (e.g., "Axis 0"). |
NormalizedValue |
double |
Value normalized to 0.0–1.0. Bound to ProgressBar. |
RawValue |
int |
Raw value (0–65535). |
File: DevicesViewModel.cs
Display item for a single button state.
| Property |
Type |
Description |
Index |
int |
Raw button slot (sparse, e.g. 16 for touchpad-click). |
DisplayNumber |
int |
Consecutive 0..N-1 shown to the user. The XAML binds DisplayNumber, not Index. |
IsPressed |
bool |
Currently pressed. Bound to circle fill color. |
File: DevicesViewModel.cs
One named chip in the Consumer Control live preview (#168): a canonical media key that lights while held.
| Property |
Type |
Description |
Index |
int |
Slot into state.Buttons[] (the persistence-stable ConsumerUsageTable index). |
Name |
string |
Localized button name. |
IsPressed |
bool |
Currently held. Bound to chip background. |
NfcTagDisplayItem
File: DevicesViewModel.cs
One row in the NFC reader's live tag preview (#150): a registered tag (or "Any NFC Tag") that lights while its button pulses.
| Property |
Type |
Description |
Name |
string |
Tag display name. |
Uid |
string |
Tag UID (empty for "Any NFC Tag"). |
Button |
int |
Stable raw-button index (0 = Any NFC Tag). |
LastActiveTick |
long |
TickCount64 of the last tap. Held past the ~175 ms button pulse so the row does not flicker. |
IsActive |
bool |
Lit while recently tapped. Bound to row background. |
KeyboardKeyItem
File: DevicesViewModel.cs
Display item for a keyboard key with position data. Rendered in a Canvas inside a Viewbox for auto-scaling.
| Property |
Type |
Description |
VKeyIndex |
int |
Windows Virtual Key code. |
Label |
string |
Key label. |
X |
double |
Canvas X position (px). |
Y |
double |
Canvas Y position (px). |
KeyWidth |
double |
Key width (px). |
KeyHeight |
double |
Key height (px). |
IsPressed |
bool |
Currently pressed. |
| Constant |
Value |
Description |
LayoutWidth |
556 |
Canvas width for the keyboard layout. |
LayoutHeight |
136 |
Canvas height for the keyboard layout. |
| Static Method |
Description |
BuildLayout() |
Builds full ANSI QWERTY layout with numpad. Each key mapped to its VK code. |
IsVKeyPressed(bool[], int) |
Checks if a VKey is pressed in a button array. |
PovDisplayItem
File: DevicesViewModel.cs
Display item for a single POV hat switch.
| Property |
Type |
Default |
Description |
Index |
int |
- |
POV hat index. |
Centidegrees |
int |
-1 |
Value in centidegrees (0–35900), or −1 for centered. Notifies IsCentered and AngleDegrees. |
IsCentered |
bool |
- |
Computed: Centidegrees < 0. |
AngleDegrees |
double |
- |
Computed: direction in degrees (0–359) for RotateTransform. |
DeviceRowViewModel
File: DeviceRowViewModel.cs
Single device row on the Devices page. Shows identification, status, capabilities, and input hiding controls.
Identity
| Property |
Type |
Description |
InstanceGuid |
Guid |
Unique instance GUID. |
SdlGuid |
string |
SDL device GUID string used by gamecontrollerdb mappings. |
DeviceName |
string |
Display name. |
ProductName |
string |
Product name. |
ProductGuid |
Guid |
Product GUID in PIDVID format. |
USB Identification
| Property |
Type |
Description |
VendorId |
ushort |
USB Vendor ID. Notifies VendorIdHex. |
ProductId |
ushort |
USB Product ID. Notifies ProductIdHex. |
VendorIdHex |
string |
Computed: hex (e.g., "045E"). |
ProductIdHex |
string |
Computed: hex (e.g., "028E"). |
Status
| Property |
Type |
Default |
Description |
IsOnline |
bool |
false |
Device connected. Notifies StatusText. |
IsEnabled |
bool |
true |
Device enabled for mapping. Notifies StatusText. |
StatusText |
string |
- |
Computed: "Disabled", "Online", or "Offline". |
SerialNumber |
string |
"" |
Device serial, where the transport reports one. |
HasVidPid |
bool |
- |
Computed: either VendorId or ProductId is non-zero. Gates the VID:PID line. |
Battery (#167)
Battery indicator on the device row. Sourced from SDL by InputService's slow lane.
| Property |
Type |
Default |
Description |
BatteryPercent |
int |
-1 |
Battery percentage. -1 when the device reports none (wired, unknown, or offline). Notifies HasBattery, BatteryText, BatteryGlyph. |
BatteryCharging |
bool |
false |
True while the device reports charging or charged. Notifies BatteryGlyph. |
HasBattery |
bool |
- |
Computed: BatteryPercent >= 0. Gates whether the indicator renders. |
BatteryText |
string |
- |
Computed: "78%", or empty when BatteryPercent < 0. |
BatteryGlyph |
string |
- |
Computed: Segoe MDL2 Assets battery glyph bucketed to the nearest tenth. Discharging: Battery0–Battery9 = U+E850–U+E859, Battery10 = U+E83F. Charging: BatteryCharging0–8 = U+E85A–U+E862, BatteryCharging9 = U+E83E, BatteryCharging10 = U+EA93. |
Power: Idle Disconnect (#162) and Quick Charge (#372)
| Property |
Type |
Default |
Description |
IdleDisconnectMinutes |
int |
0 |
Idle countdown in minutes, 0 = off. Clamped >= 0. Persisted to UserDevice in seconds through the hiding-toggle channel. |
ShowIdleDisconnect |
bool |
false |
Whether the idle-disconnect control applies (Bluetooth path detected). Set by the service, not computed on the row. Notifies ShowPowerSection and ShowRawInputDivider. |
QuickChargeEnabled |
bool |
false |
Quick Charge (#372): drop this pad's Bluetooth link when its USB twin comes online. Persisted on UserDevice through the same channel as the hiding toggles. |
ShowQuickCharge |
bool |
false |
Whether the Quick Charge checkbox applies. Deliberately wider than ShowIdleDisconnect: the USB cable rebinds a Sony record to the USB path (identity keys on the shared MAC serial, so no twin record exists), a path the disconnect predicate rejects, which made the checkbox vanish exactly while the pad was plugged in. Seeded by ComputeShowQuickCharge. Notifies ShowPowerSection and ShowRawInputDivider. |
ShowPowerSection |
bool |
- |
Computed: ShowIdleDisconnect \|\| ShowQuickCharge. The Power section draws when either control in it does. |
| Method |
Description |
ComputeShowQuickCharge(string devicePath, ushort vendorId, ushort productId, string serial) |
Internal static, so tests need no view model. True for a BluetoothLinkHelper.IsDisconnectTarget shape, or for a record whose serial parses as a non-zero Bluetooth address. The second leg is exactly the gate the engine's wired path fires on, with no vendor gate: the feature fires for any pad that reports its radio's MAC as the serial on both transports. |
Device-Class Flags
| Property |
Type |
Description |
IsMidiDevice |
bool |
Computed: DeviceTypeKey == "Midi". Drives the live piano / CC preview (#128). |
IsHeadsetMotionDevice |
bool |
Computed: DeviceTypeKey == "HeadsetMotion". True for Sony headset head trackers (#188). Collapses the raw Axes / Buttons preview, which a motion-only source has neither of, and gates the repair action. |
IsHandheldDevice |
bool |
Computed: DeviceTypeKey == "HandheldButtons". The handheld hidden-buttons row (#343). |
IsSystemMotionDevice |
bool |
Computed: DeviceTypeKey == "SystemMotion". The machine's own motion sensor row (#343). |
IsHeadTrackerDevice |
bool |
Computed: DeviceTypeKey == "HeadTracker". The head tracker row (#355). |
ShowLearnHandheldButton |
bool |
Computed: IsHandheldDevice and the path is not peer://. A peer row's buttons are learned on the machine that has them. |
HandheldDaemonRunning |
string |
Comma-joined names of vendor daemons the sweep found running (#343), empty when none. Written by the device list sync. Notifies the two below. |
HasHandheldDaemonWarning |
bool |
Computed: IsHandheldDevice and HandheldDaemonRunning is non-empty. |
HandheldDaemonWarning |
string |
Computed: the localized warning naming the running daemons, empty when there is nothing to warn about. |
ShowRegisterNfcTag |
bool |
Computed: shows the Register / Manage NFC Tags button (#150). True for a standalone PC/SC reader (DeviceTypeKey == "Nfc") and for a Switch right Joy-Con / combined pair / Pro Controller (VID 057E, PID 2007 / 2008 / 2009) on a Bluetooth link, whose own reader registers through the same dialog (#241). Always false on a peer:// row: the tap event never crosses the link, so registration belongs to the owner. |
ShowManageVoicePhrases |
bool |
Computed: shows the Manage Voice Macros button (#317). True for a "Microphone" row, and for a DualSense (VID 054C, PID 0CE6 / 0DF2) on a Bluetooth link, where the embedded mic is not a system device. Same peer:// exclusion as the NFC button. |
HasNfcCapabilityChip |
bool |
Computed: the device is a Switch controller carrying an NFC reader (VID 057E, PID 2007 / 2008 / 2009). A hardware fact, not the transport-gated arming state, so it reads true over USB too. A standalone PC/SC reader is excluded, its device type already says NFC. |
IsBluetoothLink |
bool |
Computed by DeviceTransport.IsBluetooth from the device path and VID:PID. |
Capabilities
| Property |
Type |
Description |
AxisCount |
int |
Number of axes. |
ButtonCount |
int |
Number of buttons. |
PovCount |
int |
Number of POV hat switches. |
DeviceTypeKey |
string |
English type key: "Gamepad", "Joystick", "Wheel", "FlightStick", "FirstPerson", "Supplemental", "Mouse", "Keyboard", "Touchpad", "Midi", "Nfc", "Microphone", "ConsumerControl", "HeadsetMotion", "HandheldButtons", "SystemMotion", "HeadTracker". Anything else falls back to the generic "Device" label. The setter notifies every derived property that reads the key. A row is constructed before the key is known, so one missing from that list evaluates once against the empty string and its binding never updates (#315). |
DeviceType |
string |
Computed: localized type from DeviceTypeKey. |
HasRumble |
bool |
Supports rumble. |
HasGyro |
bool |
Has gyroscope. |
HasAccel |
bool |
Has accelerometer. |
HasTouchpad |
bool |
Exposes a touchpad surface. |
ShowTouchpadCapability |
bool |
Computed: HasTouchpad and the device is not itself a touchpad, so a touchpad row does not advertise itself twice. |
HasCapabilityIcons |
bool |
Computed: HasRumble \|\| HasGyro \|\| ShowTouchpadCapability. Gates the icon strip. |
CapabilitiesSummary |
string |
Computed: e.g., "6 axes, 11 buttons, 1 POV, Rumble, Gyro". |
Slot Assignment
| Property |
Type |
Description |
AssignedSlots |
List<int> |
Assigned pad slot indices (0–15). Set via SetAssignedSlots(). |
SlotBadges |
ObservableCollection<SlotBadge> |
Slot badges for XAML (icon + number). Rebuilt by SetAssignedSlots(). Absence of badges encodes "unassigned". The old IsUnassigned flag and its gray fallback pill were removed in #175 (phase 2 item 9). |
| Method |
Description |
SetAssignedSlots(List<int>) |
Replaces assigned slots, rebuilds SlotBadges with sequential numbering, notifies UI. |
| Property |
Type |
Default |
Description |
HidHideEnabled |
bool |
false |
Hidden via HidHide (driver-level). |
ConsumeInputEnabled |
bool |
false |
Mapped inputs consumed via low-level hooks. |
ForceRawJoystickMode |
bool |
false |
Bypass SDL gamepad remapping. Read raw joystick indices. |
IsHidHideAvailable |
bool |
false |
HidHide installed (controls toggle IsEnabled). |
ShowConsumeToggle |
bool |
- |
Computed: real keyboards and mice only ((Keyboard or Mouse) && !IsInternalVirtual). Consumption suppresses the source at the raw/descriptor layer, which only exists for a real Windows HID device. |
IsInternalVirtual |
bool |
- |
Computed: true for PadForge-internal virtual sources, identified by a URI-scheme DevicePath instead of a real Windows HID path. Nine schemes: web://, overlay://, midi://, peer://, nfc://, mic://, handheld://, sensor://, headtrack://. Those cover web controllers, the touchpad overlay, MIDI-input devices, Remote Link peers, PC/SC readers, WASAPI microphone endpoints, the handheld hidden-buttons row, the machine's own sensor row, and the head tracker. HidHide cannot blacklist a non-HID path, so the "Hide from games" toggle hides itself for these. |
ShowInputHidingSection |
bool |
- |
Computed: !IsInternalVirtual. Drives the Input Hiding section visibility. |
ShowInputModeSection |
bool |
- |
Computed: IsGamepad && !IsInternalVirtual. Drives the Input Mode (Force Raw Joystick) section visibility. |
ShowInputModeOrHidingSection |
bool |
- |
Computed: union of the two above. Gates the separator between Slot Assignment and the hiding/mode sections so a virtual device doesn't leave a dangling divider. |
ShowRawInputDivider |
bool |
- |
Computed: ShowPowerSection \|\| !ShowInputModeOrHidingSection. Two dividers bracket the Power section, so exactly one of them has to survive whatever the device has. Left unconditional, the lower one doubled up on any device with an Input Mode or Hiding section but no Power section. Made to follow the Power section alone, an NFC reader or a microphone would get no rule at all. |
Device Path
| Property |
Type |
Description |
DevicePath |
string |
Device path (diagnostics). Notifies DossierConnectionPath and the IsInternalVirtual family. |
HidHideInstancePath |
string |
Resolved HID instance path for HidHide blacklisting. Notifies DossierConnectionPath. |
DossierConnectionPath |
string |
Computed: the connection path the dossier shows when no HidHide instance path occupies the PATH row. Covers bridged devices like the DS3, whose real transport path (BthPS3 PDO / WinUSB interface) arrives through SdlDeviceWrapper.ExternalDevicePathProvider even though SDL reports no path. Empty for internal virtual sources and when a HidHide path is already shown. |
Display Helpers
| Property |
Type |
Description |
IsGamepad |
bool |
Computed: true if DeviceTypeKey == "Gamepad". |
ShowSubmitMapping |
bool |
Computed: the device can have a community mapping submitted. True for everything except "Gamepad", "Mouse", "Keyboard", "Touchpad", "Midi", "Nfc", "HeadsetMotion", "Microphone", "ConsumerControl", "HandheldButtons", "SystemMotion", and "HeadTracker". |
| Method |
Description |
NotifyDisplayChanged() |
Refreshes computed properties (StatusText, CapabilitiesSummary, VendorIdHex, ProductIdHex, HasVidPid). |
SlotBadge
File: DeviceRowViewModel.cs
Slot assignment badge (icon + number).
| Property |
Type |
Description |
SlotIndex |
int |
Zero-based pad slot index. |
SlotNumber |
int |
Sequential global number (1-based among created slots). |
VcType |
VirtualControllerType |
The slot's output type, which picks the badge art below. |
TypeGeometry / TypeGlyph |
Geometry / string |
Branded icon path, or the Segoe glyph for the types with no path (SlotTypeIconMap). |
PadViewModel
Files: PadViewModel.cs, PadViewModel.Touchpad.cs (partial added in v3.3 for Touchpad-tab properties), PadViewModel.Mouse.cs (partial added for the #200 mouse-gesture tab), PadViewModel.AudioDsp.cs (partial added in 4.3.2 for the #347 Audio DSP chain)
DataContext for: Pad page (one per virtual controller slot)
The largest ViewModel. One per virtual controller slot (16 total). Handles output type, multi-device selection, mapping grid, deadzone/sensitivity, macros, force feedback, Touchpad-tab settings, and live state display.
Identity and Type
| Property |
Type |
Default |
Description |
PadIndex |
int |
- |
Zero-based slot index (0–15). Read-only. |
SlotNumber |
int |
PadIndex + 1 |
1-based number among active slots. |
SlotLabel |
string |
- |
Display label (e.g., "Virtual Controller 1"). |
OutputType |
VirtualControllerType |
Xbox |
Output type. Resets deadzones, rebuilds mappings/stick/trigger configs, syncs macro button style. |
TypeInstanceLabel |
string |
"1" |
Per-type instance label. Set by RefreshNavControllerItems. |
OutputTypeIndex |
int |
- |
ComboBox SelectedIndex, a plain cast over OutputType: 0=Xbox, 1=PlayStation, 2=Extended, 3=Midi, 4=KeyboardMouse, 5=Nintendo, 6=Vr. The setter ignores any value Enum.IsDefined rejects. |
OutputTypeDisplayName |
string |
- |
Computed: localized type name for the tier-1 header. Re-raised on OutputType and culture changes. |
ConfigItemDirtyCallback |
Action |
null |
Called on config item change. Wired to SettingsService.MarkDirty(). |
HIDMaestro Profile
The slot's HID descriptor. Picking a different profile rewires the slot, so the setter does the work of a small migration before anything rebinds.
| Property |
Type |
Description |
ProfileId |
string |
HIDMaestro profile slug ("xbox-360-wired", "dualsense", "logitech-g920"). Null or empty falls back to the active category's default profile in CreateVirtualController. |
AvailableProfiles |
IReadOnlyList<HMProfile> |
Computed: the catalog list for the current category (Xbox / PlayStation / Nintendo / Extended). Empty for MIDI, Keyboard+Mouse, and VR slots, which use no HIDMaestro profile. |
HasHMaestroProfileBar |
bool |
Computed: the slot's category uses an HIDMaestro profile. Gates the profile picker bar on the pad page. True for Xbox, PlayStation, Nintendo, and Extended. |
The ProfileId setter, in order, on a live re-target only (it reads SettingsManager.GetWireStamp before translating, so restore, apply, and import paths, which stamp the incoming wire first, no-op through it):
- Translates existing raw bindings by role on a Nintendo slot, or on any slot where both the outgoing and incoming wires are lettered (
NintendoPreviewMap.IsLettered). Every other live re-target only re-stamps the wire (StampNintendoWire). Raw targets are wire-relative and the lettered families share almost no indices, so without the translation every binding keeps its index and silently changes meaning.
- On an Extended or Nintendo slot, fills empty auto-mappings for what the new profile added (
DeviceService.FillEmptyAutoMappingsForSlot, then SettingsService.RefreshMappingSetsFromLegacy), merged into the slot's MappingSet before the rebuild, since the grid rebuilds from the set and the save pipeline regenerates settings from the grid. Fill is additive, so a binding the user authored or deliberately cleared survives.
- Reseeds
ExtendedConfig from the profile (SyncExtendedConfigFromProfile): ThumbstickCount and TriggerCount from HMProfile.StickCount / TriggerCount, PovCount from HasHat, ButtonCount from the profile's own lettered count. That count is authoritative rather than a Min against the SDK-reported one: taking a Min let a low reported count truncate the surface, which is how Capture, GR, GL, and C went missing from the Switch 2 Pro grid. A Valve lettered profile takes all four counts from NintendoPreviewMap instead, because those descriptors declare almost nothing and the real shape lives in the extended report. Picking the synthetic Custom entry also forces ExtendedConfig.Customize on.
- Rebuilds mappings, stick configs, and trigger configs, re-derives macro and menu button lettering (
SyncMacroButtonStyle), and re-gates the Bass Shakers tab.
Xbox and PlayStation slots have fixed layouts, so they skip the reseed and only rebuild mappings. Profile-gated rows exist inside those fixed layouts too (Xbox Series adds Share, the DualSense family adds Mic Mute, the Edge adds its paddle and Fn pairs), so a profile change within the category still has to rebuild the row list.
Extended Configuration
| Property |
Type |
Description |
ExtendedConfig |
ExtendedSlotConfig |
Per-slot Extended config (preset, axis/button/POV counts). Drives the HID descriptor handed to HIDMaestro for Extended slots. Only meaningful when OutputType == Extended. Subscribes to PropertyChanged for dynamic rebuilds. |
MIDI Configuration
| Property |
Type |
Description |
MidiConfig |
MidiSlotConfig |
Per-slot MIDI config (channel, CC/note mappings). Only meaningful when OutputType == Midi. |
Per-(Slot, Device) Lighting / Adaptive Trigger Configuration
Drives the Adaptive Triggers and Lighting tabs. Keyed per physical device, so two devices on one slot each carry their own lighting/trigger config. See DeviceSlotConfig.
| Property |
Type |
Description |
DeviceConfig |
DeviceSlotConfig |
The Adaptive Triggers / Lighting tab's currently-bound config, following SelectedMappedDevice. The setter re-attaches its PropertyChanged forwarder (ActiveDeviceConfigPropertyChanged) on swap. |
PerDeviceSlotConfigs |
IReadOnlyDictionary<Guid, DeviceSlotConfig> |
Per-device configs keyed by InstanceGuid. The empty-Guid key holds a fallback used before any device is mapped. |
| Method |
Description |
GetOrCreateDeviceConfig(Guid) |
Returns the per-device config, creating a fresh default entry if none exists. Empty Guid returns the anchor config. |
EnumerateDeviceSlotConfigs() |
Snapshot of every per-device config on the slot. Used by macro fan-out. |
EnsureDeviceSlotConfigsForMappedDevices() |
Adds a default config entry for every currently-mapped device. |
Keyboard + Mouse Configuration (#205)
| Property |
Type |
Description |
KbmConfig |
KbmSlotConfig |
Per-slot SOCD / Snap Tap config. Always present, only meaningful when OutputType == KeyboardMouse. See KbmSlotConfig. |
Multi-Device Selection
| Property |
Type |
Description |
MappedDevices |
ObservableCollection<MappedDeviceInfo> |
Physical devices mapped to this slot. |
SelectedMappedDevice |
MappedDeviceInfo |
Selected device for configuration. Raises SelectedDeviceChanged, notifies HasSelectedDevice. |
HasSelectedDevice |
bool |
Computed: SelectedMappedDevice != null. |
MappedDeviceName |
string |
Mapped device name. Default: "No device mapped". |
MappedDeviceGuid |
Guid |
Mapped device GUID. |
IsDeviceOnline |
bool |
Mapped device is online. |
| Event |
Args |
Description |
SelectedDeviceChanged |
MappedDeviceInfo |
Different device selected. InputService reloads PadSetting. |
MappedDeviceInfo (nested class)
| Property |
Type |
Description |
Name |
string |
Device display name. |
InstanceGuid |
Guid |
Device instance GUID. |
IsOnline |
bool |
Device connected. Refreshed every dashboard tick so an offline roster line dims instead of rendering cold (#175 phase 2 item 14). |
BatteryText |
string |
"78%" while the device reports a battery, else empty (#167). Shown per item in the Pad-page device dropdown. |
BatteryGlyph |
string |
Segoe MDL2 battery glyph for the crucible card roster (#175). Same bucketing as DeviceRowViewModel.BatteryGlyph. Notifies HasBattery. |
HasBattery |
bool |
Computed: BatteryGlyph is non-empty. |
TransportGlyph |
string |
Segoe MDL2 transport glyph for the roster (#175): Bluetooth links carry E702, wired stays unmarked. Notifies HasTransportGlyph. |
HasTransportGlyph |
bool |
Computed: TransportGlyph is non-empty. |
TypeGlyph |
string |
Segoe MDL2 device-class glyph (#175), sourced from DeviceTypeGlyph.For. Defaults to the controller glyph (U+E7FC) until InputService resolves the device. |
Combined slot output values, updated at 30 Hz by UpdateFromEngineState(). Bound to the 2D/3D controller visualizer. On a slot whose preview rides the raw surface, UpdateNintendoPreviewFromRaw owns the button and stick properties instead, and UpdateFromEngineState writes only the vibration bars.
Buttons (all bool):
ButtonA, ButtonB, ButtonX, ButtonY, LeftShoulder, RightShoulder, ButtonBack, ButtonStart, LeftThumbButton, RightThumbButton, ButtonGuide, DPadUp, DPadDown, DPadLeft, DPadRight
Family extras on the same lane, each driving the 2D overlay and 3D mesh accent for the pads that carry the button: ButtonShare (Xbox Series), ButtonMute (DualSense mic mute), LeftFunction / RightFunction (DualSense Edge Fn), ButtonC / LeftPaddle / RightPaddle (Switch 2 Pro C, GL and GR, with UpdateFromGamepad writing the paddle pair on PlayStation slots too, so both surfaces share it), and the Valve set ButtonQuickAccess, Paddle1-Paddle4 (translator handedness: R4, L4, R5, L5), LeftGrip / RightGrip (the 2015 pad's rear grips), and LeftTouchpadClick / RightTouchpadClick.
On a Nintendo or Valve slot these come from UpdateNintendoPreviewFromRaw, which walks Models2D.NintendoPreviewMap.ButtonTable(ProfileId) and assigns by name. It is table-driven off the same wire table the mapping grid and the click-to-record path use, so the two directions cannot disagree. The hardcoded index list it replaced was the original Pro Controller's, and it lit the wrong art for eleven of the Switch 2 Pro's twenty-one buttons. Two details fall out of the table: on the 2015 Steam Controller the right pad click is also the right stick button, so RightThumbButton follows RightTouchpadClick, and a profile that spends real buttons on the D-pad (Switch 2 Pro) skips the hat read entirely.
Axes (combined slot values):
| Property |
Type |
Description |
LeftTrigger |
double |
Left trigger (0.0–1.0). |
RightTrigger |
double |
Right trigger (0.0–1.0). |
ThumbLX |
double |
Left stick X (0.0–1.0, 0.5 = center). |
ThumbLY |
double |
Left stick Y (0.0–1.0, 0.5 = center, Y inverted). |
ThumbRX |
double |
Right stick X (0.0–1.0, 0.5 = center). |
ThumbRY |
double |
Right stick Y (0.0–1.0, 0.5 = center, Y inverted). |
Raw values:
| Property |
Type |
Description |
RawThumbLX |
short |
Raw left stick X (−32768 to 32767). |
RawThumbLY |
short |
Raw left stick Y. |
RawThumbRX |
short |
Raw right stick X. |
RawThumbRY |
short |
Raw right stick Y. |
RawLeftTrigger |
ushort |
Raw left trigger (0–65535). |
RawRightTrigger |
ushort |
Raw right trigger (0–65535). |
Per-device values (selected device only, for stick/trigger tab previews):
| Property |
Type |
Description |
DeviceThumbLX |
double |
Selected device left stick X (0.0–1.0). |
DeviceThumbLY |
double |
Selected device left stick Y. |
DeviceThumbRX |
double |
Selected device right stick X. |
DeviceThumbRY |
double |
Selected device right stick Y. |
DeviceRawThumbLX |
short |
Selected device raw left stick X. |
DeviceRawThumbLY |
short |
Selected device raw left stick Y. |
DeviceRawThumbRX |
short |
Selected device raw right stick X. |
DeviceRawThumbRY |
short |
Selected device raw right stick Y. |
DeviceLeftTrigger |
double |
Selected device left trigger (0.0–1.0). |
DeviceRightTrigger |
double |
Selected device right trigger. |
DeviceRawLeftTrigger |
ushort |
Selected device raw left trigger. |
DeviceRawRightTrigger |
ushort |
Selected device raw right trigger. |
Mapping Rows
| Property |
Type |
Description |
Mappings |
ObservableCollection<MappingItem> |
Rows linking physical inputs to output targets. Rebuilt by RebuildMappings(). |
MappingSet |
MappingSet |
Per-virtual-controller mapping store (multi-source rows, shift layers, activators). Drives Mappings on layer / activator change. New in 3.2. See Button and Axis Mappings and Shift Layers. |
ActiveLayerMask |
string |
Active shift layer mask. Base when no layer is engaged. UI rebinds the mapping grid on change. |
LayerTabs |
ObservableCollection<ShiftLayerInfo> |
Tab strip entries above the mapping grid, one per layer (Base + each shift layer). Populated by RebuildLayerTabs from the slot's ShiftActivators. |
HasShiftLayers |
bool |
True when at least one shift activator is authored (i.e. at least one tab beyond Base). |
| Event |
Description |
MappingsRebuilt |
Raised after RebuildMappings() completes so InputService can reload descriptors. |
LayerActivated |
Raised when ActiveLayerMask changes. Subscribers reload per-row source data so the DataGrid reflects the active layer's rows. |
Slot-Level Picker Lists and Mapping Picker Filter (#322, discussion #302)
Slot-wide cross-device choice lists, plus the one search box and device-visibility set applied as the filter of the shared views every picker on the slot binds. A view filter changes what a dropdown offers, never what a row's binding holds, so saved selections survive filtering.
| Property |
Type |
Description |
SlotAvailableInputs |
ObservableCollection<InputChoice> |
Slot-level cross-device choice list, so the Gyro tab's Aim Engage picker and any other slot-wide picker binds without proxy-walking Mappings. Populated by InputService.PopulateAvailableInputs. |
SlotAvailableInputsView |
ICollectionView |
Grouped view over the list, keyed on InputChoice.DeviceLabel for the picker's GroupStyle header. |
SlotMacroTriggerChoices |
ObservableCollection<InputChoice> |
The subset of SlotAvailableInputs that converts to a MacroItem.TriggerInputEntry (raw buttons, POV directions, gamepad-layout axes, touchpad click, touchpad gestures), for the macro trigger dropdown (#177). |
SlotMacroTriggerChoicesView |
ICollectionView |
Grouped view over the macro-trigger list, same DeviceLabel grouping. |
MappingInputSearch |
string |
Find-as-you-type over the picker list. Session state, never persisted. Setting it re-applies the filter. |
PickerDeviceFilterEntries |
ObservableCollection<PickerDeviceFilterEntry> |
The device-filter popup's rows, one per device group currently in the picker. |
MappingPickerFilterActive |
bool |
Computed: any hidden device or non-empty search. Lets the funnel button read as engaged. |
| Method / Event |
Description |
RebuildPickerDeviceFilterEntries() |
Rebuilds the popup rows from the shared list's current device groups, preserving each group's shown / hidden state. Called after every picker repopulation. |
SetHiddenPickerDeviceKeys(IEnumerable<string>) |
Restores the persisted hidden set on settings load. |
GetHiddenPickerDeviceKeysJoined() |
The persisted form: hidden keys, semicolon-joined. Stored per slot in the settings root, not in profiles. |
PickerDeviceFilterChanged |
Raised when the hidden set changes so the settings service can mark dirty and persist. |
The search filters both sides: the dropdown's offered choices, and the mapping grid's own rows. A row matches when its target label or its selected source's display name carries the text. Rows are outputs, so the device-visibility set never hides them, only typed text does.
PickerDeviceFilterEntry is one popup row: Key (device guid, or "any" for the device-agnostic group), Label, and a two-way IsShown whose setter toggles the owner's hidden set.
Per-Device Test Routing (3.2)
Not a ViewModel property. InputManager.TestRumbleTargetGuid is a Guid[MaxPads] array holding, per slot, the InstanceGuid of the device currently visible in the per-device tabs (Impulse Triggers, Force Feedback, Adaptive Triggers, Lighting). InputService's test-send methods (SendTestImpulseTrigger and its siblings) set the slot's entry to the device GUID the pad page passed, and clear it back to Guid.Empty when the pulse ends. Test / Probe actions on those tabs gate on target == Empty || ud.InstanceGuid == target so only the selected physical pad fires, and UserEffectsDispatcher.TestRumbleTargetGuidProvider carries the same gate into the dispatcher branches that emit physical effects from those tabs (for example auto-routed impulse-to-AT vibration).
Touchpad State (3.2 canonical slot)
| Property |
Type |
Description |
TouchpadClickPressed |
bool |
True while the touchpad-click button is held (state.Buttons[16] = SDL_GAMEPAD_BUTTON_TOUCHPAD upstream). Drives the 2D/3D model click highlight and the per-press lightbar overlay synth. |
TouchpadFinger0X / TouchpadFinger0Y / TouchpadFinger0Down |
float / float / bool |
First finger position (0..1, normalized to pad width / height) and contact state on the source touchpad. |
TouchpadFinger1X / TouchpadFinger1Y / TouchpadFinger1Down |
float / float / bool |
Second finger position and contact state. |
All seven are written by UpdateFromTouchpadState(in TouchpadState), which mirrors the per-slot combined touchpad state so the 2D / 3D / web previews can render finger dots and the click highlight. Only meaningful on PlayStation slots, harmless elsewhere.
| Method |
Description |
RebuildMappings() |
Rebuilds Mappings from OutputType and Extended config. Dispatches to type-specific initializers. |
Touchpad Gestures (v3.3)
File: PadViewModel.Touchpad.cs
Per-(active device, pad-index) touchpad-tab settings. Same Load / Sync / guard rhythm as the gyro tuning partial: Load* reads PadSetting.TouchpadSettings[] into VM fields under the _loadingTouchpadGestures guard, setters push back through PushIfNotLoading, and InputService calls the sync method on the live polling rhythm so the gesture engine sees changes at once. Setter clamps in parentheses below.
Active pad pivot (devices with two pads, Steam Controller / Deck):
| Property |
Type |
Default |
Description |
SelectedTouchpadIndex |
int |
0 |
Which pad on the active device the tab edits (0..MaxTouchpadIndex-1). Changing it reloads VM fields from the matching TouchpadSettings entry. |
MaxTouchpadIndex |
int |
1 |
Touchpad count on the active device (0 when none). Private setter. |
HasMultipleTouchpads |
bool |
- |
Computed: MaxTouchpadIndex > 1. Shows the pad pivot. |
TouchpadIndexOptions |
IEnumerable<int> |
- |
0..MaxTouchpadIndex-1 for the pivot ComboBox. |
Detection card:
| Property |
Type |
Default |
Description |
TouchpadGesturesEnabled |
bool |
false |
Master switch for the tab. |
TouchpadGestureMode |
string |
"Both" |
"InBoxOnly" / "CustomOnly" / "Both". |
TouchpadInBoxSectionEnabled |
bool |
- |
Computed: master on and mode isn't CustomOnly. Grays the In-Box card otherwise (#177/#178). |
TouchpadCustomSectionEnabled |
bool |
- |
Computed: master on and mode isn't InBoxOnly. |
TouchpadCooldownMs |
int |
100 |
Minimum gap between recognized gestures (0–5000). |
In-box gestures card:
| Property |
Type |
Default |
Description |
TouchpadSwipeDistanceThreshold |
double |
0.15 |
Swipe distance, fraction of pad (0.01–1.0). |
TouchpadSwipeTimeWindowMs |
int |
500 |
Swipe time window (50–5000). |
TouchpadEnableFourWaySwipes |
bool |
false |
4-direction swipes. |
TouchpadEnableEightWaySwipes |
bool |
false |
8-direction swipes. |
TouchpadEnableRadialZones |
bool |
false |
Radial-menu zones. |
TouchpadRadialZoneCount |
int |
8 |
Zone count, snapped to one of 4 / 6 / 8 / 12. |
TouchpadRadialZoneCountOptions |
IReadOnlyList<int> |
{4,6,8,12} |
Static dropdown choices. |
TouchpadRadialCenterDeadzone |
double |
0.30 |
Center dead radius (0.0–0.9). |
TouchpadEnableTouchSpots |
bool |
false |
Fixed touch-spot regions. |
TouchpadEnableTaps |
bool |
false |
Tap / multi-tap recognition. |
TouchpadTapTimeWindowMs |
int |
350 |
Single-tap window (30–1000). |
TouchpadMultiTapGapMs |
int |
300 |
Gap between taps in a multi-tap (50–2000). |
TouchpadEnableLongPress |
bool |
false |
Long-press recognition. |
TouchpadLongPressTimeWindowMs |
int |
500 |
Long-press hold time (100–5000). |
TouchpadEnableTwoFingerSwipes |
bool |
false |
Two-finger swipes. |
TouchpadEnablePinchSpread |
bool |
false |
Pinch / spread. |
TouchpadEnableRotate |
bool |
false |
Two-finger rotate. |
TouchpadEnableThreeFingerGestures |
bool |
false |
Three-finger gestures. |
TouchpadEnableFourFingerGestures |
bool |
false |
Four-finger gestures. |
TouchpadEnableFiveFingerGestures |
bool |
false |
Five-finger gestures. |
TouchpadEnableShapeGestures |
bool |
false |
Shape-recognizer gestures. |
TouchpadGestureMatchThreshold |
double |
3.0 |
Shape-recognizer match threshold, lower = stricter (0.1–10.0). $Q scale. |
Joystick / D-pad output card:
| Property |
Type |
Default |
Description |
TouchpadEnableJoystickOutput |
bool |
false |
Drive a stick / D-pad from finger position. |
TouchpadJoystickMaxRadius |
double |
0.30 |
Finger travel mapped to full deflection (0.05–0.5). |
TouchpadJoystickInnerDeadzone |
double |
0.02 |
Center dead radius (0.0–0.10). |
TouchpadJoystickDPadMode |
string |
"FourWay" |
"Off" / "FourWay" / "EightWay". |
TouchpadJoystickDPadActivationThreshold |
double |
0.15 |
Travel before a D-pad direction fires (0.05–0.5). |
Mouse output card (touchpad-as-mouse tuning):
| Property |
Type |
Default |
Description |
TouchpadMouseSensitivityX |
double |
1.0 |
X sensitivity (0.05–10.0). |
TouchpadMouseSensitivityY |
double |
1.0 |
Y sensitivity (0.05–10.0). |
TouchpadMouseInvertX |
bool |
false |
Invert X. |
TouchpadMouseInvertY |
bool |
false |
Invert Y. |
TouchpadMouseMomentum |
bool |
false |
Keep the cursor gliding after the finger lifts. |
TouchpadMouseMomentumDecay |
double |
0.90 |
Per-tick momentum decay (0.80–1.00). Higher glides longer. |
TouchpadMomentumMaxSpeed |
double |
0 |
Fling launch ceiling in pad widths per second (0–30). 0 = no ceiling. |
TouchpadMomentumMinLift |
double |
0.286 |
Lift-velocity gate below which no fling starts (0–2). The old hardcoded value. |
TouchpadMomentumFlingGain |
double |
1.0 |
Launch-speed multiplier (0.1–5.0). Leaves drag speed alone. |
TouchpadMomentumStacking |
bool |
false |
Whether a new fling adds to an in-flight coast instead of replacing it. |
TouchpadPointerResponse |
string |
"Simple" |
"Simple" (flat multiplier) or "Trackpad" (the libinput pointer-acceleration port). Notifies TouchpadPointerResponseIsSimple / TouchpadPointerResponseIsTrackpad, which gate the two halves of the card. |
TouchpadTrackpadThreshold |
double |
130 |
Trackpad-curve speed threshold in mm/s (20–600). |
TouchpadTrackpadPadWidthMm |
double |
69 |
Physical pad width in mm the curve normalizes against (20–150). |
TouchpadMouseAcceleration |
double |
0 |
Simple-response acceleration exponent (0.0–5.0). 0 is linear. |
TouchpadMouseJitterReduction |
bool |
true |
Suppress sub-pixel jitter at rest. |
Recognizer tolerances:
| Property |
Type |
Default |
Description |
TouchpadTapMaxMotion |
double |
0.04 |
Travel a contact may drift and still count as a tap, as a fraction of the pad (0.01–0.30). |
TouchpadLongPressMaxMotion |
double |
0.05 |
Same guard for a long press (0.01–0.30). |
TouchpadTwoFingerSwipeAngularTolerance |
double |
25 |
How far apart, in degrees, two fingers may travel and still read as one swipe (5–90). |
TouchpadPinchThreshold |
double |
0.25 |
Spread change that fires a pinch (0.05–1.00). |
TouchpadRotateThresholdDegrees |
double |
20 |
Rotation that fires a two-finger rotate (5–90). |
Pointer region card (#9): the screen rectangle the "Touchpad N Pointer X/Y" absolute cursor sources map onto. The four values are authored together: touching any of them latches the region as user-authored, so a later default sweep leaves it alone.
| Property |
Type |
Default |
Description |
TouchpadPointerRegionSizeX |
double |
1.0 |
Region width as a fraction of the work area (0.05–3.0). 1.0 is Steam's 1:1 pad-to-screen map. |
TouchpadPointerRegionSizeY |
double |
1.0 |
Region height (0.05–3.0). |
TouchpadPointerRegionCenterX |
double |
0.5 |
Region center X in work-area fractions (0.0–1.0). |
TouchpadPointerRegionCenterY |
double |
0.5 |
Region center Y (0.0–1.0). |
Swipe haptics card (#219):
| Property |
Type |
Default |
Description |
TouchpadSwipeHapticsEnabled |
bool |
false |
Short haptic ticks as the finger travels across this pad. |
TouchpadSwipeHapticsIntensity |
double |
0.5 |
Tick intensity (0.0–1.0). |
Custom gestures card:
| Member |
Type |
Description |
CustomTouchpadGestures |
ObservableCollection<TouchpadCustomGestureItem> |
Profile-scoped custom gestures filtered by the active device's class. |
HasNoCustomTouchpadGestures |
bool |
Computed: the collection is empty. Drives the placeholder text. |
RecordTouchpadGestureCommand |
RelayCommand |
Raises RecordTouchpadGestureRequested with the active (device, pad) so the recorder dialog mirrors the right pad. |
DeleteTouchpadGestureCommand |
RelayCommand<TouchpadCustomGestureItem> |
Raises DeleteTouchpadGestureRequested for the row. |
| Event |
Args |
Description |
RecordTouchpadGestureRequested |
RecordTouchpadGestureArgs |
Open the recorder dialog for (device, pad). |
DeleteTouchpadGestureRequested |
TouchpadCustomGestureItem |
Delete a saved custom gesture. |
Reset commands: every setting row carries a Reset<Property>Command that restores its default above, plus per-card ResetTouchpadDetectionCardCommand, ResetTouchpadInBoxCardCommand, ResetTouchpadJoystickCardCommand, ResetTouchpadMouseCardCommand, ResetTouchpadPointerCardCommand, ResetTouchpadSwipeHapticsCardCommand, and ResetTouchpadSyntheticPressureCardCommand. Defaults mirror TouchpadGestureSettings.Default(), so Reset to Defaults round-trips the tab by construction.
| Method |
Description |
LoadTouchpadGestureSettingsForActiveDevice() |
Reads the per-(device, pad) entry into VM fields under the load guard so setters don't ping-pong back. |
SyncTouchpadGestureSettingsToActiveDevice() |
Writes VM fields into the entry, creating it on first touch. No-op while loading, which stops an external PropertyChanged caller from clobbering not-yet-loaded fields. |
RecomputeTouchpadCountForActiveDevice(int) |
Sets MaxTouchpadIndex from the active device. 0 hides the tab. |
RefreshCustomTouchpadGestures(IEnumerable<TouchpadCustomGesture>) |
Repopulates CustomTouchpadGestures after a profile load / switch. Pass null to clear. |
RecordTouchpadGestureArgs (device GUID, name, pad index) and TouchpadCustomGestureItem (display wrapper over a TouchpadCustomGesture with Name, FingerCount, Summary) are declared alongside the partial.
Mouse Gestures (#200)
File: PadViewModel.Mouse.cs
Per-(slot, device) mouse-gesture settings. Twin of the Touchpad partial's Load/Sync/guard pattern. Persistence rides setter-time writes into the live PadSetting plus MainWindow's PropertyChanged hook. The 30 Hz VM flush does not carry these fields.
| Property |
Type |
Default |
Description |
MouseGesturesEnabled |
bool |
false |
Mouse-gesture recognizer armed for the active mouse device. |
MouseGestureButtons |
int |
8 (1 << 3, X1) |
Bitmask of buttons arming the recognizer: bit 0 Left, bit 1 Middle, bit 2 Right, bit 3 X1, bit 4 X2, bit 5 Custom (#216, armed by the recorded cross-device input below instead of a mouse button). Masked to 0x3F. |
MouseGestureButtonLeft / Middle / Right / X1 / X2 / Custom |
bool |
- |
Per-bit wrappers over MouseGestureButtons for the checkboxes. |
MouseGestureFlickThreshold |
int |
150 |
Flick distance threshold in mouse counts. Clamped 10–5000. |
MouseGestureCooldownMs |
int |
100 |
Minimum gap between recognized gestures (ms). Clamped 0–5000. |
Custom activation input (#216). The recorded cross-device descriptor + owning device GUID that arm the Custom gesture session while held. Same pair shape, picker projection, and record flow as the Gyro tab's Aim Engage cluster, scoped per (slot, mouse device) with the rest of the card.
| Member |
Type |
Default |
Description |
MouseGestureCustomEngageButton |
string |
"" |
Recorded input descriptor. |
MouseGestureCustomEngageDeviceGuid |
string |
"" |
Owning device GUID for the recorded input. |
MouseGestureCustomEngageSelectedInput |
InputChoice |
null |
Picker projection over the (descriptor, device) pair. Setting it writes both. OnMouseGestureCustomEngageSelectedInputRefresh() re-raises it after the choice list repopulates. |
MouseGestureCustomEngageRecording |
bool |
false |
Record in progress. Swaps MouseGestureCustomEngageRecordButtonIcon / MouseGestureCustomEngageRecordButtonText. |
MouseGestureCustomEngageRecordCommand |
RelayCommand |
- |
Raises MouseGestureCustomEngageRecordRequested so MainWindow runs the freeform recorder. |
Reset commands: ResetMouseGesturesEnabledCommand, ResetMouseGestureButtonCommand, ResetMouseGestureFlickThresholdCommand, ResetMouseGestureCooldownCommand, ResetMouseGestureCustomEngageCommand (clears the recorded pair), ResetMouseGesturesCardCommand.
| Method |
Description |
LoadMouseGestureSettingsForActiveDevice() |
Loads the active mouse device's gesture settings into the VM. Missing settings load as defaults, so Reset to Defaults resets this tab by construction. |
SyncMouseGestureSettingsToActiveDevice() |
Writes the VM fields into the active mouse device's entry, creating it on first touch. No-op while loading. |
Force Feedback / Rumble
| Property |
Type |
Default |
Range |
Description |
ForceOverallGain |
int |
100 |
0–100 |
Overall FFB gain %. |
LeftMotorStrength |
int |
100 |
0–100 |
Left motor strength %. |
RightMotorStrength |
int |
100 |
0–100 |
Right motor strength %. |
SwapMotors |
bool |
false |
- |
Swap left/right motor assignment. |
LeftMotorDisplay |
double |
0 |
0–1 |
Live left motor level (post-scaling). |
RightMotorDisplay |
double |
0 |
0–1 |
Live right motor level. |
Audio Bass Rumble (per-device):
| Property |
Type |
Default |
Range |
Description |
AudioRumbleEnabled |
bool |
false |
- |
Audio-driven rumble from system bass frequencies. |
AudioRumbleSensitivity |
double |
4.0 |
1–20 |
Bass detection sensitivity multiplier. |
AudioRumbleCutoffHz |
double |
80.0 |
20–200 |
Low-pass cutoff (Hz) for bass extraction. |
AudioRumbleLeftMotor |
int |
100 |
0–100 |
Left motor strength for audio rumble. |
AudioRumbleRightMotor |
int |
100 |
0–100 |
Right motor strength for audio rumble. |
AudioRumbleLevelMeter |
double |
0 |
- |
Live audio bass level meter. |
Reset commands: ResetForceAllCommand, ResetOverallGainCommand, ResetLeftMotorCommand, ResetRightMotorCommand, ResetAudioRumbleAllCommand, ResetAudioSensitivityCommand, ResetAudioCutoffCommand, ResetAudioLeftMotorCommand, ResetAudioRightMotorCommand.
Constant Force (per-device, FFB-capable + scalar-rumble pads):
| Property |
Type |
Default |
Range |
Description |
ConstantForceEnabled |
bool |
false |
- |
Apply a continuous force vector until toggled off. Resumes after game/macro forces stop. |
ConstantForceX |
double |
0 |
−1.0–1.0 |
X component of the force vector. |
ConstantForceY |
double |
0 |
−1.0–1.0 |
Y component (+ = up in the UI grid). |
Audio DSP Chain (#347)
Audio-tab surface over the per-device crossfeed / EQ / limiter fields on DeviceSlotConfig (see the DeviceSlotConfig section below). Lives in PadViewModel.AudioDsp.cs. The cards are gated on SelectedDeviceHasDspChain.
| Member |
Type |
Description |
SelectedDeviceHasDspChain |
bool (computed, PadViewModel.cs) |
True when the selected device is a Sony pad whose audio rides an AudioPassthroughService sink, the only place the chain runs: DualSense family, or a DualShock 4 over Bluetooth. |
EqBands |
ObservableCollection<EqBandVm> |
Grid rows decoded from DeviceSlotConfig.AudioEqBands. Every row edit re-encodes the whole list back into the config (PushEqBands). |
RefreshEqBands() |
method |
Rebuilds the rows from the selected device's config on every device switch and clears the import status. |
AddEqBandCommand / RemoveEqBandCommand / ClearEqBandsCommand |
RelayCommand (RemoveEqBandCommand takes the EqBandVm row) |
Add a default band, remove one row, or clear the list. Each pushes the encoded list. |
ImportAutoEqFileCommand |
RelayCommand |
Open-file dialog for an AutoEq ParametricEQ.txt / FixedBandEQ.txt and apply its Filter lines plus preamp. |
ImportAutoEqCommand |
RelayCommand |
Same parse over the clipboard text. |
EqImportStatus / HasEqImportStatus |
string / bool |
What the last import did, shown under the buttons. A Graphic EQ file with no Filter lines says so instead of silently doing nothing. |
ResetCrossfeedCommand |
RelayCommand |
AudioCrossfeedLevel back to 0. |
ResetCrossfeedCutCommand / ResetCrossfeedFeedCommand |
RelayCommand |
Custom knobs back to libbs2b's own defaults, 700 Hz and 4.5 dB. |
ResetEqPreampCommand |
RelayCommand |
AudioEqPreampDb back to 0. |
ResetEqCommand |
RelayCommand |
EQ off, preamp 0, bands cleared. |
ResetLimiterCommand / ResetLimiterCeilingCommand |
RelayCommand |
Limiter back on, ceiling back to 98. |
EqBandVm (one editable row, ObservableObject): Enabled, Type (EqBandType: Peaking, LowShelf, HighShelf, HighPass, LowPass, Notch), FrequencyHz (clamped 10 to EqBand.MaxFrequencyHz()), GainDb (clamped −30..30), Q (clamped 0.05..20). Clamps match the engine's so the grid never accepts a band the DSP would silently reject.
Impulse Triggers Tab (3.2)
Per-pad-per-slot per-trigger-motor effects. Tab visible only when the selected device reports HasRumbleTriggers: SDL's SDL_PROP_JOYSTICK_CAP_TRIGGER_RUMBLE_BOOLEAN or a Microsoft impulse-trigger PID (XboxControllerIdentity.IsImpulseTriggerDevice). Xbox One and later pads.
| Property |
Type |
Default |
Range |
Description |
ImpulseOverallGain |
int |
100 |
0–100 |
Master scale for the trigger-motor pipeline. |
ImpulseLeftStrength |
int |
100 |
0–100 |
Left trigger motor scale. |
ImpulseRightStrength |
int |
100 |
0–100 |
Right trigger motor scale. |
ImpulseSwapTriggers |
bool |
false |
- |
Swap left/right trigger motor assignment. |
Constant Trigger Force:
| Property |
Type |
Default |
Range |
Description |
ConstantTriggerForceEnabled |
bool |
false |
- |
Override-with-resume continuous force on the trigger motors. |
ConstantTriggerForceLeft |
double |
0 |
0.0–1.0 |
Left trigger steady force. |
ConstantTriggerForceRight |
double |
0 |
0.0–1.0 |
Right trigger steady force. |
Audio Bass Trigger Rumble:
| Property |
Type |
Default |
Range |
Description |
AudioRumbleTriggersEnabled |
bool |
false |
- |
Drive trigger motors from system audio bass. Runs alongside the body-motor audio rumble. |
AudioRumbleTriggersSensitivity |
double |
4.0 |
1.0–20.0 |
Bass intensity multiplier (trigger channel). |
AudioRumbleTriggersCutoffHz |
double |
80.0 |
20–200 |
Low-pass cutoff (trigger channel). |
AudioRumbleLeftTrigger |
int |
100 |
0–100 |
Audio-driven left-trigger motor scale. |
AudioRumbleRightTrigger |
int |
100 |
0–100 |
Audio-driven right-trigger motor scale. |
AudioRumbleTriggersLevelMeter |
double |
0 |
- |
Live trigger-channel level. |
| Event |
Args |
Description |
TestLeftImpulseTriggerRequested |
EventArgs |
Per-device test pulse on the left trigger motor. |
TestRightImpulseTriggerRequested |
EventArgs |
Per-device test pulse on the right trigger motor. |
Gyro Engage Stick Gate (#120)
Per-direction stick gate for Easy-Aim gyro engagement. Both persist per device and reset to their defaults when the value is empty, so old profiles keep their original right-stick-radial behavior.
| Property |
Type |
Default |
Values |
Description |
GyroEngageStickSide |
string |
"Right" |
Right, Left, Either |
Which stick's deflection drives the Easy-Aim threshold gate. Either uses the larger of the two. Empty collapses to "Right". |
GyroEngageStickDirection |
string |
"Full" |
Full, X, Y, XNeg, XPos, YNeg, YPos |
Which component of the engage stick(s) drives the gate. Full is radial. Empty collapses to "Full". |
| Command |
Description |
ResetGyroEngageStickSideCommand |
Resets GyroEngageStickSide to "Right". |
ResetGyroEngageStickDirectionCommand |
Resets GyroEngageStickDirection to "Full". |
Motion Space and Grip (#392)
Both store the canonical English identifier, not the localized label, so PadForge.xml round-trips across a language change. Both are per (slot, device).
| Property |
Type |
Default |
Values |
Description |
GyroSpace |
string |
"Local" |
Local, Player, World |
Gyro reference frame. Empty collapses to "Local". |
GyroSpaceOptions |
IReadOnlyList<GyroLabeledOption> |
- |
- |
Display-label / stored-value pairs for the space dropdown. |
MotionGrip |
string |
"Pointing" |
Pointing, Sideways, WiiWheel, Upright |
How the controller is held. Empty collapses to "Pointing". |
MotionGripOptions |
IReadOnlyList<GyroLabeledOption> |
- |
- |
Display-label / stored-value pairs for the grip dropdown, in that order. |
IsLoadingPadSetting |
bool |
false |
- |
True while InputService.LoadPadSettingIntoViewModel mirrors a PadSetting into this VM. A plain field with no change notification: it is a scope marker, not a bound property. |
Reset commands: ResetGyroSpaceCommand (back to "Local"), ResetMotionGripCommand (back to "Pointing").
Changing the grip re-references the motion state of the one device the page is editing: its gravity estimate, shake envelope, and lean and tilt neutrals are in the old frame. MainWindow's MotionGrip PropertyChanged handler gates that on InputService.ShouldRecenterOnGripChange, which is !IsLoadingPadSetting. A grip write arriving through a PadSetting load is a mirror of stored state, not a change of hold, so a device selection or a profile switch must not drop every device's estimate on the slot. The recenter itself is InputService.RecenterMotionForSelectedDevice. The Gyro Recenter macro's pad-wide drop is a separate path (InputManager.GyroRecenterApply).
Gyro Tilt Envelope (#292)
Per-(slot, device) lens over the "Gyro Tilt X/Y" sources, the same shape as Flick Stick below. The save pipeline stamps these onto every tilt source (ApplyGyroTiltParamsToRow, the Motion Steering push pattern) and persists them in the PadSetting extended-mapping bag under GyroTilt* keys.
| Property |
Type |
Default |
Range |
Description |
GyroTiltRangeDeg |
double |
25 |
1–180 |
Physical tilt in degrees that reaches full deflection. 25 is the modal Steam-corpus maximum. The clamp admits the corpus outliers while the slider tops at 90. |
GyroTiltInnerDz |
double |
0 |
0–89 |
Inner tilt deadzone in degrees. |
Reset commands: ResetGyroTiltRangeCommand, ResetGyroTiltInnerCommand, and the card-level ResetGyroTiltAllCommand.
Flick Stick (#225)
Per-(slot, device) lens over the "Flick Stick ..." sources in the slot's KBM mapping set. The save pipeline stamps these onto every flick source (ApplyFlickStickParamsToRow, the Motion Steering push pattern) and persists them in the PadSetting extended-mapping bag (FlickStick* keys). Defaults mirror the MappingSource ParamFlick* defaults (JSM-grounded).
| Property |
Type |
Default |
Range |
Description |
FlickCountsPer360 |
double |
14400 |
100–100000 |
Mouse counts per full 360° camera turn (Steam "Dots Per 360°"). |
FlickTime |
double |
0.1 |
0.01–2.0 |
Flick easing duration in seconds for a full 180° flick. |
FlickThreshold |
double |
0.9 |
0.1–1.0 |
Stick deflection (raw magnitude) at which a flick engages. |
FlickSnapMode |
string |
"None" |
None / Forward / Half / Four / Sixths / Eight |
Snap interval, Tag-backed combo string (the GyroEngageStickSide pattern). Value set = SourceKindRuntime.FlickSnapIntervalRad's. |
FlickSnapStrength |
double |
1.0 |
0.0–1.0 |
Snap lerp strength. 1.0 = full snap to the interval. |
FlickForwardDeadzone |
double |
0 |
0–180 |
Forward angle deadzone in degrees: a flick within this of dead-ahead reads as 0°. |
FlickSmoothing |
double |
-1 |
−1.0 to 0.5 |
Rotation smoothing threshold in rad/tick. Negative = automatic tiered window, 0 = off, positive = explicit lower threshold. |
FlickOnEngage |
bool |
false |
- |
Fire a flick immediately when evaluation starts with the stick already past threshold (the shift-layer engage case). Off arms at the current angle and only tracks rotation. |
Reset commands: one per row (ResetFlickCountsPer360Command, ResetFlickTimeCommand, ResetFlickThresholdCommand, ResetFlickSnapModeCommand, ResetFlickSnapStrengthCommand, ResetFlickForwardDeadzoneCommand, ResetFlickSmoothingCommand, ResetFlickOnEngageCommand) plus the card-level ResetFlickStickCardCommand.
Deadzone Settings
Left Stick:
| Property |
Type |
Default |
Range |
Description |
LeftDeadZoneShape |
int |
2 (ScaledRadial) |
0–5 |
Deadzone shape index. |
LeftDeadZoneX |
double |
0 |
0–100 |
X deadzone %. |
LeftDeadZoneY |
double |
0 |
0–100 |
Y deadzone %. |
LeftAntiDeadZoneX |
double |
0 |
0–100 |
X anti-deadzone %. |
LeftAntiDeadZoneY |
double |
0 |
0–100 |
Y anti-deadzone %. |
LeftLinear |
double |
0 |
0–100 |
Linear interpolation factor. |
LeftMaxRangeX |
double |
100 |
1–100 |
X max range (positive). |
LeftMaxRangeY |
double |
100 |
1–100 |
Y max range (positive). |
LeftMaxRangeXNeg |
double |
100 |
1–100 |
X max range (negative). |
LeftMaxRangeYNeg |
double |
100 |
1–100 |
Y max range (negative). |
LeftCenterOffsetX |
double |
0 |
−100 to 100 |
X center offset %. |
LeftCenterOffsetY |
double |
0 |
−100 to 100 |
Y center offset %. |
Right Stick: Same pattern with Right prefix: RightDeadZoneShape, RightDeadZoneX, RightDeadZoneY, RightAntiDeadZoneX, RightAntiDeadZoneY, RightLinear, RightMaxRangeX, RightMaxRangeY, RightMaxRangeXNeg, RightMaxRangeYNeg, RightCenterOffsetX, RightCenterOffsetY.
Backward compat shims: LeftDeadZone (get=LeftDeadZoneX, set=both X+Y), RightDeadZone (same pattern).
Speed and momentum mirrors: LeftStickSensitivity / RightStickSensitivity (1.0, clamped 0.1–5.0) carry the flat speed multiplier the KBM pointer sticks use, and KbmMouseMomentum (false) / KbmMouseMomentumGlide (0.90, clamped 0.80–1.00) carry the stick trackball (#291) for the KBM mouse stick. SyncStickItemFromVm pushes them into the matching StickConfigItem.
Sensitivity Curves (serialized control point strings, "x,y;x,y;..." format):
| Property |
Default |
Description |
LeftSensitivityCurveX |
"0,0;1,1" |
Left stick X sensitivity curve. |
LeftSensitivityCurveY |
"0,0;1,1" |
Left stick Y sensitivity curve. |
RightSensitivityCurveX |
"0,0;1,1" |
Right stick X sensitivity curve. |
RightSensitivityCurveY |
"0,0;1,1" |
Right stick Y sensitivity curve. |
LeftTriggerSensitivityCurve |
"0,0;1,1" |
Left trigger sensitivity curve. |
RightTriggerSensitivityCurve |
"0,0;1,1" |
Right trigger sensitivity curve. |
Triggers:
| Property |
Type |
Default |
Range |
Description |
LeftTriggerDeadZone |
double |
0 |
0–100 |
Left trigger deadzone %. |
RightTriggerDeadZone |
double |
0 |
0–100 |
Right trigger deadzone %. |
LeftTriggerAntiDeadZone |
double |
0 |
0–100 |
Left trigger anti-deadzone %. |
RightTriggerAntiDeadZone |
double |
0 |
0–100 |
Right trigger anti-deadzone %. |
LeftTriggerMaxRange |
double |
100 |
1–100 |
Left trigger max range %. |
RightTriggerMaxRange |
double |
100 |
1–100 |
Right trigger max range %. |
Dynamic Stick/Trigger Config Items
Drive the ItemsControl-based Sticks and Triggers tabs. Gamepad presets: 2 sticks, 2 triggers. Custom Extended: N sticks, M triggers. KBM: 2 items (Mouse Movement, Scroll Wheel).
| Property |
Type |
Description |
StickConfigs |
ObservableCollection<StickConfigItem> |
Stick config items. Rebuilt by RebuildStickConfigs(). |
TriggerConfigs |
ObservableCollection<TriggerConfigItem> |
Trigger config items. Rebuilt by RebuildTriggerConfigs(). |
| Method |
Description |
RebuildStickConfigs() |
Rebuilds stick configs for current output type. |
RebuildTriggerConfigs() |
Rebuilds trigger configs. KBM has none. |
SyncStickItemFromVm(StickConfigItem) |
Pushes VM deadzone properties into a stick item. |
SyncTriggerItemFromVm(TriggerConfigItem) |
Pushes VM trigger properties into a trigger item. |
SyncAllConfigItemsFromVm() |
Syncs all items from VM. Called after settings load/paste. |
ResetAllSettings() |
Resets per-slot settings to defaults. Called on slot deletion. |
Macros
| Property |
Type |
Description |
Macros |
ObservableCollection<MacroItem> |
Macros for this slot. |
SelectedMacro |
MacroItem |
Selected macro. Notifies HasSelectedMacro, refreshes remove command. |
HasSelectedMacro |
bool |
Computed: SelectedMacro != null. |
| Command |
CanExecute |
Description |
AddMacroCommand |
always |
Adds a new macro named "Macro N", with the slot's derived ButtonStyle and RawProfileId, and selects it. |
RemoveMacroCommand |
HasSelectedMacro |
Removes the selected macro, then selects the last remaining one. |
DuplicateMacroCommand |
HasSelectedMacro |
Clones the selected macro within the slot (#112) by round-tripping it through the macro DTO, so it is a deep copy and the runtime-only execution state drops away. A macro mid-fire keeps firing; the clone starts cold. |
CopyMacroCommand |
HasSelectedMacro |
Raises CopyMacroRequested. MainWindow owns the clipboard. |
PasteMacroCommand |
always |
Raises PasteMacroRequested. |
CopyMacroFromCommand |
always |
Raises CopyMacroFromRequested: pull macros from another virtual controller, mirroring the Mappings-tab Copy From. MainWindow shows the picker and does the copy. |
Action-type picker. The editor's type dropdown binds MacroTypeCatalog.View, a grouped ICollectionView over MacroTypeCatalog.Choices keyed on MacroTypeChoice.Category. Each MacroTypeChoice carries the MacroActionType, a localized Label, the localized Category header, and an optional Tooltip. The picker was fifty-six flat entries in enum-history order; the catalog orders them by what they act on, under eleven headers: Virtual Buttons, Virtual Axes & Wheel, Keyboard & Text, Mouse, Timing & Flow, Rumble, Lightbar & LEDs, Sound & Volume, Motion & Pointer, Layers & Overlays, and System & Apps. A census test pins that every enum member appears exactly once, so a new action type cannot ship without choosing its shelf. The catalog holds one list instance for its lifetime and refills it in place on Strings.CultureChanged, then refreshes the view: a fresh list per culture would strand the x:Static-bound view on the old one.
Bass Shakers (#236)
The per-slot Bass Shakers tab surface, MappingSet-backed like Menus and persisted through the same dirty callback. RumbleAudioTabVisible gates the tab on slot type: Xbox, PlayStation, and Nintendo always, plus Extended slots whose surface carries force feedback (Customize on: the ForceFeedbackEnabled checkbox decides; Customize off: the catalog profile descriptor must carry a PID FFB block). The card binds RumbleAudioEnabled, RumbleAudioEndpointId (output device picker), RumbleAudioChannelMode (Mono / Controller Stereo), RumbleAudioMasterGain, and RumbleAudioVoices, an ObservableCollection of the four per-channel voice rows (Low Motor, High Motor, Left Trigger, Right Trigger: enable, 20–120 Hz frequency, gain, per-voice Test). The frequency sweep and Stop live beside the voices. Everything writes into MappingSet.RumbleAudio (RumbleAudioConfig), so the routing travels with profiles and Copy / Paste.
Output tab: Slot SOCD (#245) and Keep Controller Awake
Both cards live on the Output tab (index 17). OutputTabVisible is a slot-type gate: every type except MIDI and VR, the two with no output-behavior surface at all.
Controller-button SOCD is distinct from KbmSlotConfig's key SOCD (#205). SocdCardVisible and KbmSocdCardVisible are mutually exclusive by slot type. The card binds SocdMode over MappingSet.SocdMode (AvailableSlotSocdModes, in dropdown order: Off / Last Wins / Neutral / First Wins), and SocdPairItems (SlotSocdPairItem) edits the opposing button pairs from SocdButtonOptions. SocdUsesRawIndices says which pair grammar the slot stores: Extended and Nintendo slots write flat raw indices ("12:13"), Xbox and PlayStation slots write the WriteBoolTarget names, mirroring the engine's own raw-surface gate. The engine applies the cleaning to the combined output right before submit, on both the Gamepad bitmap and the raw-HID button words.
Keep Controller Awake holds a stick off-center so a console-style pad never idles out. Like the Bass Shakers config, it lives on the slot's MappingSet and every setter fires ConfigItemDirtyCallback.
| Property |
Type |
Default |
Description |
KeepAwakeCardVisible |
bool |
- |
Computed: Xbox and PlayStation slots only. |
KeepAwakeEnabled |
bool |
false |
Card master switch, over MappingSet.KeepAwakeEnabled. |
KeepAwakeMotion |
bool |
false |
Sweep the hold instead of parking it, over MappingSet.KeepAwakeMotion. For titles that gate vibration on the stick moving rather than on where it sits. |
KeepAwakeAxis |
string |
"LX" |
Held axis, locale-stable: LX, LY, RX, RY. The default is stored as an empty string, so the getter substitutes "LX". |
AvailableKeepAwakeAxes |
IReadOnlyList<GyroLabeledOption> |
- |
Axis picker options, reusing the stick-axis strings the mapping grid already localizes. |
KeepAwakeDeflection |
int |
25 |
Held deflection percent, clamped 1–90. A persisted 0 means unset and reads as the engine default 25, so the card always shows the effective number. |
| Command |
Description |
ResetKeepAwakeEnabledCommand |
Card switch back off. |
ResetKeepAwakeMotionCommand |
Sweep back off. |
ResetKeepAwakeAxisCommand |
Back to "LX". |
ResetKeepAwakeDeflectionCommand |
Back to 25. |
ResetKeepAwakeCardCommand |
Disabled, axis and deflection back to their unset defaults, then ReloadKeepAwake(). |
| Method |
Description |
ReloadKeepAwake() |
Re-raises the whole card off the live MappingSet. Called on profile apply and output-type change. |
Radial / touch menus for this slot. Slot-level like Macros: the collection wraps the LIVE MenuDefinitionEntry list on the slot's MappingSet (write-through), and every edit fires ConfigItemDirtyCallback so the autosave path persists the set. Rows are MenuEditorItem instances.
| Property |
Type |
Description |
Menus |
ObservableCollection<MenuEditorItem> |
Menus configured for this pad slot. |
SelectedMenu |
MenuEditorItem |
Selected menu. Notifies HasSelectedMenu, refreshes the remove / duplicate commands. |
HasSelectedMenu |
bool |
Computed: SelectedMenu != null. |
| Command |
CanExecute |
Description |
AddMenuCommand |
always |
Adds a new entry to the slot set with the next free MenuId and an auto-numbered name. |
RemoveMenuCommand |
HasSelectedMenu |
Removes the selected menu from the slot set. |
DuplicateMenuCommand |
HasSelectedMenu |
Clones the selected entry under the next free MenuId with a "(Copy)"-suffixed name. |
MenuHostRecordCommand |
always |
Calls SelectedMenu.PrepareRecord(Host), then raises MenuHostRecordRequested. |
MenuRecordTargetCommand |
always |
RelayCommand<string>. The same freeform recorder aimed by parameter for the Custom opener's steer axes and the Click input: "CustomX", "CustomY", "Click". |
| Callback |
Description |
MenusStructureChanged |
Action, wired by MainWindow to InputService.RefreshAvailableInputsForSlot. Fired on a structural edit (add / remove / duplicate, kind, cell count, center cell, enabled) that changes which "Menu N Item K" descriptors exist, so the mapping and macro pickers stop offering stale entries. Label and name typing must not fire it. |
| Event |
Description |
MenuHostRecordRequested |
Record button on the menu host picker. MainWindow runs the freeform recorder and folds the recorded descriptor onto a host choice (MenuEditorItem.TryApplyRecordedHost), the Aim Engage record shape. |
| Method |
Description |
ReloadMenus() |
Rebuilds Menus from the slot's live MappingSet and selects the first entry. Called from RebuildMappings(), so profile applies, Workshop imports, output-type changes, and Reset to Defaults all refresh the tab. |
Slot context stamped onto every row. ApplyMenuButtonStyle runs on each item at reload, add, duplicate, and whenever the slot's macro button style is re-derived. It sets ButtonStyle from MacroButtonNames.DeriveStyle(OutputType), RawButtonCount (the Extended / Nintendo ExtendedConfig.ButtonCount, else 11), RawProfileId, and SupportsControllerButtons, true on Xbox, PlayStation, Nintendo and Extended only. A MIDI or Keyboard+Mouse output cannot press a controller button, so its cells omit the choice rather than offering it with a warning. It also hands the row four providers: DescriptorDisplayProvider, InputChoicesProvider (the slot's SlotAvailableInputs), RowBoundProvider, and, for the #390 macro cells, MacroNamesProvider, which lists this slot's macro names live.
Macro renames follow their cells. MacroItem.Renamed is a static hook each pad VM subscribes to once. A rename on this slot retags every menu cell that referenced the old name, matched case-insensitively, which is the comparer the menu runtime itself uses.
Active Config Tab
| Property |
Type |
Default |
Description |
SelectedConfigTab |
int |
0 |
Always-visible: 0=Preview (the TabController RadioButton, Tag="0", binds Pad_Tab_Preview = "Preview"), 1=Macros, 2=Mappings, 3=Sticks, 4=Triggers, 5=Force Feedback. Visibility-gated on the active source device's capability flags: 6=Adaptive Triggers (HasAdaptiveTriggers), 7=Lighting (HasLightbar), 8=Gyro (HasGyro), 9=Impulse Triggers (HasRumbleTriggers), 10=Touchpad (HasTouchpad), 11=Wheel, 12=Audio (AudioTabIndex), 13=Pointer (IR-capable Wii Remote, #146), 14=Mouse (source device is a mouse, #200). Slot-scope like tabs 0–2: 15=Menus (#9, always visible), 16=Bass Shakers (BassShakersTabIndex, gated on RumbleAudioTabVisible), 17=Output (OutputTabIndex, gated on OutputTabVisible). Tag values match the RadioButton.Tag strings on the PadPage tab strip. Entering 12 re-derives the sound-macro list and the mirror endpoints. Entering 16 re-enumerates render endpoints and re-seeds the voice rows. A gate that closes under the current selection evicts it back to tab 0. |
Commands
| Command |
CanExecute |
Description |
TestRumbleCommand |
IsDeviceOnline |
Raises TestRumbleRequested. |
ClearMappingsCommand |
always |
Clears all mapping source descriptors. |
CopySettingsCommand |
HasSelectedDevice |
Raises CopySettingsRequested. |
PasteSettingsCommand |
HasSelectedDevice |
Raises PasteSettingsRequested. |
CopyFromCommand |
HasSelectedDevice |
Raises CopyFromRequested. |
MapAllCommand |
HasSelectedDevice && !IsMapAllActive && SelectedMappedDevice.IsOnline |
Starts sequential "Map All" recording. |
StopMapAllCommand |
IsMapAllActive |
Raises MapAllCancelRequested, ending a sweep in progress. The Map All button swaps its own caption and tooltip through MapAllButtonText / MapAllButtonTooltip while a sweep runs. |
Map All System
| Property |
Type |
Description |
IsMapAllActive |
bool |
Map All recording in progress. |
MapAllCurrentIndex |
int |
Current mapping row index during Map All. |
MapAllCurrentTarget |
string |
Target setting name being recorded. |
MapAllPromptText |
string |
Descriptive text shown on Controller tab during Map All (e.g., "Press: A (1/21)"). |
CurrentRecordingTarget |
string |
TargetSettingName being recorded. Drives controller-tab flashing. |
MapAllRecordingNeg |
bool |
Recording negative direction of a bidirectional axis. |
| Event |
Args |
Description |
TestRumbleRequested |
EventArgs |
Test rumble requested. |
TestLeftMotorRequested |
EventArgs |
Test left motor only. Fired via FireTestLeftMotor(). |
TestRightMotorRequested |
EventArgs |
Test right motor only. Fired via FireTestRightMotor(). |
CopySettingsRequested |
EventArgs |
Copy settings to clipboard. |
PasteSettingsRequested |
EventArgs |
Paste settings from clipboard. |
CopyFromRequested |
EventArgs |
Copy from another device. |
MapAllRecordRequested |
MappingItem |
Request recording for the current Map All item. |
MapAllCancelRequested |
EventArgs |
Cancel an in-progress Map All recording. |
State Update Methods
| Method |
Description |
UpdateFromEngineState(Gamepad, Vibration, Vibration selectedDeviceVibration = null) |
Updates combined slot output at 30 Hz. The optional third argument drives the selected device's own motor bars. |
UpdateDeviceState(Gamepad) |
Updates per-device stick/trigger values for tab previews. |
UpdateFromTouchpadState(in TouchpadState) |
Mirrors the combined touchpad state onto the TouchpadFinger* / TouchpadClickPressed preview properties. |
UpdateFromRawHidState(RawHidState) |
Publishes the combined raw-HID output for the Extended schematic. Skips the notification when axes, hardware axes, buttons, and POVs all match the last snapshot, so an idle slot does not re-arm a repaint at 30 Hz. On a slot whose preview rides the raw surface (PreviewRidesRawSurface: any Nintendo slot, and an Extended slot on a Valve lettered profile) it also projects the raw state onto the Gamepad-shaped preview properties, and UpdateFromEngineState skips writing them so the two paths cannot fight. |
UpdateFromMidiRawState(MidiRawState) |
Updates MIDI preview snapshot. |
OnMapAllItemCompleted() |
Advances Map All to next item after 500 ms delay. |
StopMapAll() |
Stops Map All, clears state, raises MapAllCancelRequested. |
RefreshCommands() |
Refreshes CanExecute for all commands. |
Output Snapshots (for custom previews)
| Property |
Type |
Description |
RawHidOutputSnapshot |
RawHidState |
Latest raw-HID state for the Extended / Nintendo schematic. Updated at 30 Hz by UpdateFromRawHidState, which raises PropertyChanged for it. Private setter. |
KbmOutputSnapshot |
KbmRawState |
Latest KBM raw state. Updated at 30 Hz. Plain auto-property, no change notification: the view polls it. |
VrOutputSnapshot |
VrRawState |
Latest VR raw state for the VR preview (#49). Same plain auto-property contract as the KBM twin. |
MidiOutputSnapshot |
MidiRawState |
Latest MIDI raw state snapshot. |
MappingItem
File: MappingItem.cs
Single mapping row linking a physical input to an output target in the Pad page mapping grid.
Target (Output)
| Property |
Type |
Description |
TargetLabel |
string |
Display label (e.g., "A", "Left Stick X", "CC 1"). Read-only. |
TargetSettingName |
string |
PadSetting property name (e.g., "ButtonA", "LeftThumbAxisX"). Read-only. |
Category |
MappingCategory |
Grouping: Buttons, DPad, Triggers, LeftStick, RightStick, Touchpad, Motion. |
NegSettingName |
string |
Negative-direction PadSetting property. Null for non-axis targets. |
HasNegDirection |
bool |
Computed: NegSettingName != null. |
| Property |
Type |
Default |
Description |
SourceDescriptor |
string |
"" |
Physical input descriptor. Format: "{MapType} {Index}", "IH{MapType} {Index}", or "POV {Index} {Direction}". Empty = unmapped. Notifies SourceDisplayText and IsMapped. |
NegSourceDescriptor |
string |
"" |
Negative-direction descriptor for stick axes. |
SourceDisplayText |
string |
- |
Computed: readable text. Bidirectional axes show "neg / pos". Falls back to "Not mapped". |
IsMapped |
bool |
- |
Computed: true if either SourceDescriptor or NegSourceDescriptor is non-empty. |
| Method |
Description |
SetResolvedSourceText(string) |
Sets display text (e.g., "A" instead of "Button 65"). Called by InputService. |
SetResolvedNegText(string) |
Sets resolved text for negative direction. |
LoadDescriptor(string) |
Sets source descriptor, syncs IsInverted/IsHalfAxis from prefix flags. |
LoadNegDescriptor(string) |
Loads negative-direction descriptor. |
SyncSelectedInputFromDescriptor() |
Syncs SelectedInput to current SourceDescriptor without re-triggering update. |
| Property |
Type |
Description |
AvailableInputs |
ObservableCollection<InputChoice> |
Source dropdown choices, spanning every device assigned to the slot. Populated by InputService once per VC slot, not per device-dropdown change. Since #322 every row on a slot points at the ONE shared list (UseSharedAvailableInputs), so a keyboard-and-mouse tab switch no longer rebuilds a private copy and a grouped view per row. |
AvailableInputsView |
ICollectionView |
The grouped view the XAML ComboBox binds to. GroupDescriptions carries one PropertyGroupDescription on InputChoice.DeviceLabel, so the picker renders one dropdown with device-name headers. The default view is one object per collection, so on the shared list every row lands on the same view and the grouping is configured once. |
SelectedInput |
InputChoice |
Selected dropdown input. Updates SourceDescriptor. Empty sentinel triggers ClearCommand. Suppression flag prevents re-entrancy, and is held across the shared-list rebuild (BeginSharedListRebuild / EndSharedListRebuild) so a live ComboBox cannot write its own selection back through the TwoWay binding. |
| Event |
Description |
InputSelectedFromDropdown |
Input selected from dropdown (for display text resolution). |
Recording
| Property |
Type |
Default |
Description |
IsRecording |
bool |
false |
Recording mode active. Notifies RecordButtonText and RecordButtonIcon. |
RecordButtonText |
string |
- |
Computed: "Record" or "Recording...". |
Live Value Display
| Property |
Type |
Description |
CurrentValueText |
string |
Source input raw value. Updated at 30 Hz when visible. |
Per-Mapping Deadzone
| Property |
Type |
Default |
Description |
MappingDeadZone |
int |
50 |
Axis activation threshold, clamped 1–100%. Input below this percentage is ignored for this mapping row. 0 is disallowed because it used to read as "unset" and silently reverted to 50. |
IsDeadZoneApplicable |
bool |
- |
Computed: true for axis-based mappings where a per-mapping deadzone is meaningful. |
| Command |
Description |
ResetDeadZoneCommand |
Resets MappingDeadZone to 50. |
PadViewModel.ClearAllMappings resets all mapping deadzones to 50.
Options
| Property |
Type |
Default |
Description |
IsInverted |
bool |
false |
Axis inverted. Calls RebuildDescriptor() to toggle "I" prefix. |
IsHalfAxis |
bool |
false |
Upper half of axis range only. Calls RebuildDescriptor() to toggle "H" prefix. |
IsBidirectional |
bool |
false |
With IsHalfAxis, fires on absolute deflection past the deadzone, either side of center. Persisted through PadSetting.MappingBidirectional, not a descriptor prefix, because the I/H prefixes are already spoken for. |
IsInvertApplicable |
bool |
- |
Computed: !(IsHalfAxis && IsBidirectional). Mirroring around center already covers both directions. |
InvertOutput |
bool |
false |
Flip Output: invert what the row writes, for the half-axis reads where Invert is consumed as the half selector. Round-trips to MappingSource.InvertOutput. |
IsInvertOutputApplicable |
bool |
- |
Computed by the engine's own InvertConsumedByHalfAxisRead predicate against the prefix-stripped primary. Gates the Flip Output checkbox. |
ParamAccel |
double |
0 |
Per-row acceleration exponent, clamped 0.0–5.0. Has ResetParamAccelCommand. Gated by IsParamAccelApplicable, which excludes the gravity-tilt family: those are position reads whose engine path applies only the curve / range channel (#292). |
GyroSensitivity |
double |
1.0 |
Primary-source gyro multiplier, clamped 0.1–10. Gated by IsGyroSource, which excludes the gravity-tilt family. |
IsGyroLeanSource |
bool |
- |
Computed: the primary is a gravity-lean read ("Gyro Lean X" / "Gyro Lean Y", #292). Gates the lean row's dial, which binds Sensitivity. |
MouseCursorSensitivity |
double |
1.0 |
Primary-source cursor sensitivity, clamped 0.1–5. Gated by IsMouseCursorSource. |
IrPointerSensitivity |
double |
1.0 |
Primary-source Wii IR-pointer sensitivity, clamped 0.1–5. Gated by IsIrPointerSource. |
Sensitivity |
double |
1.0 |
Clamped 0.1–5. Its only mapping-grid surface is the Gyro Lean dial, gated by IsGyroLeanSource. The generic per-source slider was removed on 2026-07-27 and the knob moved to the Sticks tab as a per-stick multiplier, so IsGenericSensitivitySource exists but no XAML binds it. |
Commands
| Command |
Description |
ToggleRecordCommand |
Toggles recording. Raises StartRecordingRequested or StopRecordingRequested. |
ClearCommand |
Clears the source and neg descriptor, resets IsInverted / IsHalfAxis / IsBidirectional, puts MappingDeadZone back to 50, blanks PrimarySourceDeviceGuid and PrimarySourceDeviceLabel, wipes the primary kind holder's ParamUp / ParamDown / ParamModifier and forces its Kind to "Direct", then re-syncs the selected input from the descriptor. |
ResetGyroSensitivityCommand, ResetMouseCursorSensitivityCommand, ResetIrPointerSensitivityCommand, ResetSensitivityCommand, ResetParamAccelCommand |
Per-control resets for the primary-source knobs above. |
| Event |
Description |
StartRecordingRequested |
Start recording. |
StopRecordingRequested |
Recording should stop. |
A row's primary source stays on SourceDescriptor. Additional sources live in ExtraSources, each a MappingSourceItem. CombineMode applies when the row has more than one source. The engine's CombineHelper / MappingExpression consumes it in Step 3.
| Property |
Type |
Default |
Description |
ExtraSources |
ObservableCollection<MappingSourceItem> |
empty |
Sources beyond the primary. |
IsMultiSource |
bool |
- |
Computed: ExtraSources.Count > 0 \|\| !IsPrimaryDirect, where IsPrimaryDirect is PrimaryKindSource.Kind == "Direct". Not the same test as IsTrivialDirect, which additionally requires no neg descriptor, no extras, no invert / half / bidirectional, and no custom formula. |
VariableCount |
int |
- |
Computed: 1 + ExtraSources.Count. Drives the formula chip palette's visibility. The count of letters a Custom formula may actually reference is PositionalSourceCount, which the validator uses and which runs smaller: its walk skips the bipolar Neg pair and any InvertOnHold source with no placeholder. |
CombineMode |
string |
"" |
Per-row combine mode. Empty = per-target-type default (MaxAbs for axes, OR for buttons). Named modes: MaxAbs, Sum, Average, OR, AND, XOR, StickTrim, Custom. |
CombineExpression |
string |
"" |
Custom formula, only meaningful when CombineMode == "Custom". |
IsCustomCombine |
bool |
- |
Computed: CombineMode == "Custom". |
ShouldShowCustomExpression |
bool |
- |
Computed: IsMultiSource && IsCustomCombine. |
IsBipolarAxisTarget |
bool |
- |
Computed: target is a bipolar stick axis (LeftThumbAxisX/Y, RightThumbAxisX/Y). Drives per-source direction-badge visibility. |
IsTouchpadAxisTarget |
bool |
- |
Computed: target is a touchpad X/Y axis (TouchpadX1/Y1/X2/Y2). |
PrimarySourceDeviceGuid |
string |
"" |
DeviceGuid of the primary source on the per-VC MappingSet row. Empty = first available device on this VC. |
PrimarySourceDeviceLabel |
string |
"" |
Friendly device name for the primary source. |
NoInherit |
bool |
false |
Shift-layer "do not inherit from Base" flag. Round-trips to MappingRow.NoInherit. Visible only when authoring a non-Base layer. |
ShouldShowEmptyDirectionHint |
bool |
- |
Computed: a bipolar-axis row with exactly one non-inverted button-class primary source, nudging the user to map the opposite direction. |
| Command |
Description |
AddExtraSourceCommand |
Appends a blank MappingSourceItem, seeding CombineMode to the per-target default. |
RemoveExtraSourceCommand |
RelayCommand<MappingSourceItem>. Removes the passed source. |
AddOppositeDirectionCommand |
Adds an extra source mirroring the primary descriptor with Invert=true, filling a single-button bipolar-axis row's negative direction in one click. |
Stick Trim (#155)
Trigger-only combine mode that lets a "winding" stick axis trim a trigger output up/down.
| Property |
Type |
Default |
Description |
IsStickTrimCombine |
bool |
- |
Computed: CombineMode == "StickTrim". |
IsTriggerTarget |
bool |
- |
Computed: target is LeftTrigger / RightTrigger or an Extended axis created in the Triggers category. The only class the engine's StickTrim combine intercepts. |
ShouldShowTrimSettings |
bool |
- |
Computed: IsMultiSource && IsStickTrimCombine && IsTriggerTarget. Gates the trim strip. |
TrimDeadzone |
int |
25 |
Trim-axis deflection below this percentage is ignored (steering wobble guard). Clamped 0–95. |
TrimRate |
int |
100 |
Full-deflection adjust speed, percent of the trigger range per second. Clamped 1–1000. |
TrimResetOnRelease |
bool |
true |
Releasing the gate snaps the stored level back to 100% when true. False keeps it. |
Reset commands: ResetTrimDeadzoneCommand, ResetTrimRateCommand, ResetTrimResetOnReleaseCommand.
File: MappingItem.cs
Input choice in the source dropdown.
| Property |
Type |
Description |
Descriptor |
string |
Mapping descriptor (e.g., "Button 0", "Axis 1", "POV 0 Up"). Empty string = "clear" sentinel. |
DisplayName |
string |
Human-readable display name (e.g., "A", "Left Stick X"). ToString() returns it. |
DeviceGuid |
string |
Owning device for the cross-device picker. Empty = the "(Any device)" group. |
DeviceLabel |
string |
Friendly device name for that group heading. |
MappingCategory (enum)
Values: Buttons, DPad, Triggers, LeftStick, RightStick, Touchpad, Motion. Motion covers the bundled motion-passthrough rows (MotionGyro / MotionAccel targets), auto-created on Sony-class slots for every assigned gyro or accel-capable device and rendered as one combined Motion row when both sub-channels come from the same source. Touchpad rows are the one category IsRecordable excludes.
MappingSourceItem
File: MappingSourceItem.cs
One source row within a multi-source MappingItem (#61). Represents a single Engine.Data.MappingSource, bound by the RowDetailsTemplate inside the Mappings DataGrid.
Kind
| Property |
Type |
Default |
Description |
Kind |
string |
"Direct" |
Source kind: Direct, Incremental, InvertOnHold, Ramped. Notifies the Is*Kind computed flags. |
IsIncrementalKind / IsInvertOnHoldKind / IsRampedKind |
bool |
- |
Computed per-kind flags. |
UsesUpDownKeys |
bool |
- |
Computed: Incremental or Ramped (authored via an Up/Down key pair). |
IsKindDescriptorless |
bool |
- |
Computed: kinds where the main Descriptor/flags are unused (Incremental, InvertOnHold, Ramped). |
HasAnyBoundFeed |
bool |
- |
Computed: whether any input feeds this source, per kind. Mirrors the engine's SourceEvaluator dispatch. |
KindOptions (static) is the culture-cached list of KindChoice { Value, Name } for the Kind dropdown, keeping backend identifiers out of the UI.
Source
| Property |
Type |
Default |
Description |
DeviceGuid |
string |
"" |
Device this source reads from. Empty = the picker's "(Any device)" group. |
DeviceLabel |
string |
"" |
Friendly device name, shown inline below the per-source picker. |
DisplayDeviceLabel |
string |
- |
Computed device label honoring the "(Any device)" contract: a concrete-guid source prefers SelectedInput.DeviceLabel (recovers the friendly name when the stored label wasn't hydrated), an empty-guid source keeps its own label and never borrows a concrete device from a descriptor-only picker fallback. Read by the grid subtitle, the annotation overlays, and the pipeline-chip tooltip. |
Descriptor |
string |
"" |
Input descriptor. Notifies the source-class flags below. |
SelectedInput |
InputChoice |
null |
Cross-device picker selection. Setting it writes DeviceGuid, DeviceLabel, and Descriptor in one shot. |
IsGyroSource / IsMouseCursorSource / IsIrPointerSource / IsMouseMotionSource |
bool |
- |
Computed from the descriptor prefix. Gate the matching per-source sensitivity sliders. IsGyroSource excludes the gravity-tilt family (#292). |
IsGyroLeanSource |
bool |
- |
Computed: the gravity-lean pair ("Gyro Lean X" / "Gyro Lean Y", #292). Gates the lean row's dial, which binds Sensitivity, the field ReadGyroLean scales by. |
IsParamAccelApplicable |
bool |
- |
Computed: gates the per-source Acceleration slider. The continuous-source family minus the gravity-tilt pairs, whose engine path never applies ParamAccel (#292). |
IsButtonClassDescriptor |
bool |
- |
Computed: the descriptor yields a bool. Gamepad aliases resolve first, then button, POV, touchpad-click, NFC tag (#241), and voice phrase (#317). |
DirectionBadge |
string |
- |
Computed: "→ +" / "← −" for button-class sources, per Invert. Empty otherwise. |
Options and per-source deadzone
| Property |
Type |
Default |
Description |
Invert |
bool |
false |
Axis / direction inverted. |
HalfAxis |
bool |
false |
Upper half of the range only. |
IsHalfAxisApplicable |
bool |
- |
Computed: Half applies to continuous-range sources (Axis, Slider, Touchpad X/Y/Pressure, Gyro, Mouse Motion). |
Bidirectional |
bool |
false |
With HalfAxis, fires on absolute deflection past the deadzone (either side of center). |
DeadZone |
int |
50 |
Per-source axis-to-button threshold. Clamped 1–100 (0 is disallowed because it read as "unset"). |
IsDeadZoneApplicable |
bool |
- |
Computed: the source is an axis/slider (or an engine-owned continuous family) AND the parent target is a discrete output. |
ParentTargetIsDiscrete |
bool |
false |
Set by the parent MappingItem so IsDeadZoneApplicable knows the target class. |
Kind parameters
| Property |
Type |
Default |
Description |
ParamUp / ParamDown |
string |
"" |
Up / Down key descriptors for Incremental and Ramped. |
ParamRate |
double |
0.5 |
Incremental step rate. |
ParamSticky |
bool |
true |
Incremental sticky hold. |
ParamMin / ParamMax |
double |
0 / 1 |
Incremental output bounds. |
ParamModifier |
string |
"" |
InvertOnHold modifier key descriptor. |
ParamAttackTime |
double |
0.30 |
Ramped attack seconds (#111). Clamped 0–5. |
ParamReleaseTime |
double |
0.30 |
Ramped release seconds (#111). Clamped 0–5. |
ParamAutocenter |
bool |
true |
Ramped: release ramps back to zero (#111). |
ParamReverseMultiplier |
double |
4.0 |
Ramped reverse speed-up (#111). Clamped 1–10. |
GyroSensitivity |
double |
1.0 |
Per-source gyro multiplier (Gyro descriptors). Clamped 0.1–10. |
MouseCursorSensitivity |
double |
1.0 |
Per-source cursor sensitivity (#107, Mouse Position). Clamped 0.1–5. |
IrPointerSensitivity |
double |
1.0 |
Per-source Wii IR-pointer sensitivity (#146). Clamped 0.1–5. |
Per-source recording
| Property |
Type |
Default |
Description |
IsRecording |
bool |
false |
Recording mode active. Notifies the record text/icon and the per-Param recording flags. |
RecordButtonText / RecordButtonIcon |
string |
- |
Computed record-button label / Segoe MDL2 glyph. |
IsRecordingParamUp / Down / Modifier |
bool |
- |
Computed: which Param field's record is armed, so only that button swaps to its Stop glyph. |
| Command |
Description |
ToggleRecordCommand |
Toggles primary-source recording (raises StartRecordingRequested / StopRecordingRequested). |
RecordParamUpCommand / RecordParamDownCommand / RecordParamModifierCommand |
Toggle recording for a specific Param field (raise StartParamRecordingRequested with a ParamRecordTarget). |
ClearCommand |
Clears the descriptor, DeviceGuid, DeviceLabel, and the backing selected-input field, and resets the flags and deadzone to their defaults, keeping the row in ExtraSources. It leaves Kind, the Param* fields, the sensitivities, ParamAccel, and InvertOutput alone. |
ResetDeadZoneCommand, ResetGyroSensitivityCommand, ResetMouseCursorSensitivityCommand, ResetIrPointerSensitivityCommand |
Per-control resets. |
| Method |
Description |
ToDomain() |
Builds a domain Engine.Data.MappingSource from the VM. |
FromDomain(MappingSource) |
Static: populates a new VM from a domain source. |
SyncSelectedInputFromState(IEnumerable<InputChoice>) |
Syncs the picker selection from the current (DeviceGuid, Descriptor) pair. |
ParamRecordTarget (enum): Up, Down, Modifier.
ShiftLayerInfo
File: ShiftLayerInfo.cs
VM wrapper around a single Engine.Data.ShiftActivator. Populates the nested tab strip on the Mappings tab (PadViewModel.LayerTabs) and the per-layer context-menu commands.
| Property |
Type |
Default |
Description |
LayerMask |
string |
"Base" |
Engine-side layer identity (matches MappingRow.LayerMask). |
LayerName |
string |
"Base" |
User-visible display name. |
Color |
string |
"" |
v2 per-layer color hint, "#AARRGGBB" or empty. |
IsActive |
bool |
false |
True when this layer's tab is selected (the one being authored). |
IsBase |
bool |
- |
Computed: LayerMask == "Base". Two consumers: the tab chip's color-dot visibility, and the SHIFT pipeline chip's idle summary, which lists only non-Base layer names. It does not gate the tab context menu, whose Base behavior lives in the handlers. Base does show Rename (edits the base appearance), Configure (its flyout and tab appearance), and Delete (the legacy-Base heal path). |
MacroItem
File: MacroItem.cs
A trigger combination of inputs that produces a sequence of output actions. Evaluated in Step 4b, after Step 4 combines the slot output and before Step 5 writes the virtual devices.
Identity
| Property |
Type |
Default |
Description |
Name |
string |
Macro_NewMacro |
User-facing name, seeded from the localized resource (English "New Macro"). The setter is not plain: any post-construction change fires the static Renamed event, which is how #390 retags menu cells that referenced the old name. |
IsEnabled |
bool |
true |
Macro active. |
| Property |
Type |
Default |
Description |
TriggerButtons |
ushort |
0 |
Gamepad button bitmask (e.g., Gamepad.A \| Gamepad.B). All must be pressed. |
TriggerCustomButtonWords |
uint[] |
new uint[4] |
Wide bitmask for custom Extended slots (128 buttons, 4x32-bit). Not serialized. |
TriggerCustomButtons |
string |
null |
Serializable hex form (e.g., "00000003,00000000,00000000,00000000"). |
UsesCustomTrigger |
bool |
- |
Computed: any custom trigger button set. |
TriggerSource |
MacroTriggerSource |
InputDevice |
Record from physical device or virtual controller output. |
TriggerDisplayText |
string |
- |
Computed: readable trigger combo with device name. |
ButtonStyle |
MacroButtonStyle |
Xbox360 |
Display name style. Not serialized. |
CustomButtonCount |
int |
11 |
Custom Extended button count. Not serialized. |
| Property |
Type |
Default |
Description |
TriggerDeviceGuid |
Guid |
Guid.Empty |
Trigger source device. Empty = legacy Xbox bitmask. |
TriggerRawButtons |
int[] |
[] |
Raw button indices. All must be pressed. |
UsesRawTrigger |
bool |
- |
Computed: any TriggerInputs entry has RawButton >= 0, or the legacy pair TriggerDeviceGuid != Empty && TriggerRawButtons.Length > 0. The legacy half alone reads false for every entry-list macro. |
Trigger Condition (POV Hat)
| Property |
Type |
Default |
Description |
TriggerPovs |
string[] |
[] |
POV hat triggers as "povIndex:centidegrees" (e.g., "0:0" for POV 0 Up). |
UsesPovTrigger |
bool |
- |
Computed: any TriggerInputs entry carries a non-empty Pov, or TriggerPovs.Length > 0. |
Trigger Condition (Axis)
| Property |
Type |
Default |
Description |
TriggerAxisTargets |
MacroAxisTarget[] |
[] |
Axes that must all exceed threshold. Not serialized. |
TriggerAxisTargetList |
string |
null |
Serializable comma-separated form. |
TriggerAxisThreshold |
int |
50 |
Threshold % (1–100). |
UsesAxisTrigger |
bool |
- |
Computed: TriggerAxisTargets.Length > 0, or any TriggerInputs entry has AxisTarget != MacroAxisTarget.None. |
TriggerAxisDirections |
MacroAxisDirection[] |
[] |
Direction filter per axis. Parallel array. Not serialized. |
TriggerAxisDirectionList |
string |
null |
Serializable comma-separated form. |
TriggerAxisDirectionIndex |
int |
- |
UI index for direction (0=Any, 1=Positive, 2=Negative). Sets all uniformly. |
Trigger Condition (Descriptor / Device-Free) (#9)
Descriptor entries carry the trigger shapes that have no raw-entry form: engine-read descriptors ("Gyro Pitch", "Touchpad 0 Finger 0 Down", "Menu {id} Item {k}") evaluated through SourceCoercion's button read with the same per-(device, slot) tuning a mapping row gets. Entry-list only, picked from the trigger dropdown, never recorded.
| Member |
Type |
Default |
Description |
TriggerInputEntry.SourceDescriptor |
string |
null |
The engine-read descriptor on a trigger-entry row. Serialized through the pipe-joined TriggerInputs spec. The plain form is in:{guid}:sd:{descriptor} (same tail escaping as gestures). When the entry carries a stamp (HalfAxis, Invert, Bidirectional, or DescriptorDeadZone > 0) it writes the extended form instead: in:{guid}:sdh:{half}:{invert}:{bidi}:{deadzone}:{descriptor}. |
TriggerInputEntry.DescriptorSource |
MappingSource |
- |
Cached wrapper for the descriptor so the 1 kHz trigger evaluation never allocates. Null when the entry isn't a descriptor entry. |
UsesDescriptorTrigger |
bool |
- |
Computed: any trigger entry carries a SourceDescriptor. |
An entry with DeviceGuid == Guid.Empty is device-free: the evaluator resolves it against whatever device sits on the macro's slot, and TriggerDisplayText renders it under the localized "(Any device)" sentinel (the same group the mapping picker uses).
Trigger Options
| Property |
Type |
Default |
Description |
TriggerMode |
MacroTriggerMode |
OnPress |
Twelve modes, ordinals pinned: OnPress (0), OnRelease (1), WhileHeld (2), Always (3), CustomExpression (4, v3.2), HoldForMs (5, On Long Press), DoublePress (6), TriplePress (7), SinglePress (8), Toggle (9), Turbo (10), ShortPress (11, On Short Press, #253). |
TriggerHoldMs |
int |
500 |
Hold threshold shared by HoldForMs and ShortPress, the tap-vs-hold pair (#9 wave 1b). Clamped 50-10000. ResetTriggerHoldMsCommand puts it back to 500. |
TriggerDoublePressMs |
int |
442 |
Multi-press window for the Single / Double / Triple modes. Clamped 50-5000. The default is the double-tap window Valve's own shipped controller_base templates author (basicui.vdf / basicui_neptune.vdf, "double_tap_time" "442"). ResetTriggerDoublePressMsCommand puts it back to 442. |
LayerMask |
string |
"" |
Per-macro shift-layer scope (#254). Empty fires on any layer. "Base" opens only while Base is effectively engaged, honoring the engaged layer's InheritUnmapped exactly like a Base row does. Any other value opens only while that layer is engaged, scoped to the macro's own slot. When the macro's own slot does not declare the mask at all, any slot's engagement still opens it. A null write is ignored: the editor's Layer ComboBox coerces SelectedValue to null when the selected choice disappears, which silently downgraded a scoped macro to "any layer". HasLayerScope and ShowsLayerRow both read LayerMask non-empty. |
IsNotAlwaysMode |
bool |
- |
Computed: TriggerMode != Always. Controls trigger UI visibility. |
ConsumeTriggerButtons |
bool |
true |
Remove trigger buttons from Gamepad state on fire. |
Recording
| Property |
Type |
Default |
Description |
IsRecordingTrigger |
bool |
false |
Recording trigger combo. Notifies RecordTriggerButtonText. |
RecordTriggerButtonText |
string |
- |
Computed: "Stop" or "Record Trigger". |
RecordingLiveText |
string |
"" |
Live display of buttons pressed during recording. Not serialized. |
Actions
| Property |
Type |
Description |
Actions |
ObservableCollection<MacroAction> |
Ordered sequence of actions to execute. |
SelectedAction |
MacroAction |
Selected action. Refreshes remove command. |
Repeat Settings
| Property |
Type |
Default |
Description |
RepeatMode |
MacroRepeatMode |
Once |
How actions repeat: Once, FixedCount, UntilRelease. |
RepeatCount |
int |
1 |
Repeat count for FixedCount mode. Min 1. |
RepeatDelayMs |
int |
100 |
Delay between repeats (ms). Min 0. |
Runtime State (not serialized)
| Property |
Type |
Description |
IsExecuting |
bool |
Executing action sequence. |
CurrentActionIndex |
int |
Position in action sequence. |
RemainingRepeats |
int |
Remaining repeat count. |
ActionStartTime |
DateTime |
When current action/delay started. |
WasTriggerActive |
bool |
Trigger active on previous frame. |
Commands
| Command |
Description |
RecordTriggerCommand |
Toggles IsRecordingTrigger and raises RecordTriggerRequested. |
AddActionCommand |
Adds a new MacroAction with default type ButtonPress. |
RemoveActionCommand |
Removes the selected action, then selects the last remaining one. CanExecute: SelectedAction != null. |
| Event |
Description |
RecordTriggerRequested |
Trigger recording toggled. |
MacroAction
File: MacroItem.cs (a separate top-level class in the same file, not nested inside MacroItem)
Single action in a macro's sequence.
Core Properties
| Property |
Type |
Default |
Description |
Type |
MacroActionType |
ButtonPress |
Action type. Notifies all Is*Type properties. |
DurationMs |
int |
50 |
Hold/delay duration (ms). Min 0. |
DisplayText |
string |
- |
Computed: readable action summary. |
Type-Check Properties (all computed, not serialized)
One Is*Type flag per action family, plus the rolled-up families the editor gates whole panels on (IsAnyRumbleType, IsAnyAxisValueType, IsAnyVcButtonType, IsAnyKeyType, and the rest). A representative set: IsButtonType, IsKeyType, IsDurationType, IsAxisType, IsSystemVolumeType, IsAppVolumeType, IsMouseMoveType, IsMouseButtonType, IsContinuousAxisType, IsDeviceAxisSource, IsOutputAxisSource.
| Property |
Type |
Default |
Description |
ButtonFlags |
ushort |
0 |
Xbox bitmask for gamepad presets. |
ButtonStyle |
MacroButtonStyle |
Xbox360 |
Display name style. Not serialized. |
CustomButtonCount |
int |
11 |
Numbered style button count. Not serialized. |
CustomButtonWords |
uint[] |
new uint[4] |
Wide bitmask for Extended slots (128 buttons). Not serialized. |
CustomButtons |
string |
null |
Serializable hex form. |
ButtonOptions |
IReadOnlyList<GamepadButtonOption> |
- |
Computed: checkbox-bindable list. Lazy-built from style and count. |
| Method |
Description |
SetCustomButton(int, bool) |
Sets/clears a custom Extended button by 0-based index. |
IsCustomButtonPressed(int) |
Returns whether a custom button is pressed. |
HasCustomButtons |
Computed: any custom button is set. |
Key Action Properties
| Property |
Type |
Default |
Description |
KeyCode |
int |
0 |
Win32 VK_ code. Notifies SelectedVirtualKey. |
SelectedVirtualKey |
VirtualKey |
None |
Enum wrapper for ComboBox. Not serialized. |
KeyString |
string |
"" |
Multi-key combo in x360ce format ("{Control}{Alt}{Delete}"). |
ParsedKeyCodes |
int[] |
- |
Computed: VK codes from KeyString, falling back to KeyCode. |
SelectedKeyToAdd |
VirtualKey |
None |
Key picker binding. Auto-appends to KeyString and resets. |
| Command |
Description |
ClearKeyStringCommand |
Clears the KeyString. |
| Static |
Description |
VirtualKeyValues |
List<KeyDisplayItem> with localized display names. Rebuilt on culture change. |
ParseKeyString(string) |
Parses "{Key1}{Key2}..." into int[] of VK codes. |
Axis Action Properties
| Property |
Type |
Default |
Description |
AxisValue |
short |
0 |
AxisSet: signed value (−32768 to 32767). |
AxisTarget |
MacroAxisTarget |
None |
Which axis to set/read. |
InvertAxis |
bool |
false |
Invert axis value. |
Volume Action Properties
| Property |
Type |
Default |
Description |
ShowVolumeOsd |
bool |
true |
Trigger Windows volume OSD. |
ProcessName |
string |
"" |
AppVolume: target process name. |
AudioProcessNames |
ObservableCollection<string> |
empty |
Processes with active audio sessions (suggestions). |
VolumeLimit |
int |
100 |
Max volume % (1–100). |
| Command |
Description |
RefreshAudioProcessesCommand |
Refreshes active audio session process list. |
Mouse Action Properties
| Property |
Type |
Default |
Description |
MouseSensitivity |
float |
10f |
Pixels/scroll units per frame at full deflection (1–100). |
MouseButton |
MacroMouseButton |
Left |
Mouse button for press/release. |
MouseAccumulator |
float |
0 |
Internal [XmlIgnore] field, not a property: the fractional pixel accumulator that keeps sub-pixel precision across ticks. |
Axis Source Properties
| Property |
Type |
Default |
Description |
AxisSource |
MacroAxisSource |
OutputController |
Axis value source. |
SourceDeviceGuid |
Guid |
Guid.Empty |
InputDevice source: physical device GUID. |
SourceDeviceAxisIndex |
int |
-1 |
InputDevice source: axis index in InputState.Axis[]. |
Pressure-Scaled Turbo (#290, #393)
Applies to the repeat-interval action family (RepeatKeyWhileHeld, RepeatVcButtonWhileHeld, the toggles pulsing under PulseWhileLatched, and the rest of the IsRepeatIntervalType set). While the gate is on, the repeat rate follows a physical analog source (DualShock 3 button pressure, any trigger) instead of the fixed IntervalMs. IntervalMs becomes the fast end (full press) and SlowIntervalMs the slow end, interpolated in rate space so a light press is a defined slow rate.
Pressure direction comes from the trigger (#393). The read used to be the raw axis position, raw / 65535, so on a stick's lower half a harder push read as falling pressure and a gentle push repeated faster than a hard one. A macro that fires on a stick half already says which half, so the engine takes the direction from there rather than adding a knob. InputManager.ResolveTurboPressure01 resolves the 0..1 pressure in three steps:
- A
TriggerInputs entry on the pressure source's axis, on the same device or device-free (Guid.Empty, the picker's "(Any device)" form). A half-axis entry reads deflection from center into the half it selects: Invert takes the lower half, Bidirectional takes both sides. A full-axis entry is trigger-style and reads raw / 65535, flipped by Invert.
- Otherwise a legacy
TriggerAxisTargets slot target on the corresponding gamepad axis: Positive and Negative read deflection from center into that half, Any reads raw / 65535.
- Otherwise
raw / 65535, byte-identical to the #290 read. A pressure source that is not the macro's trigger keeps exactly what it shipped with.
Every direction now ramps 0 at rest to 1 at full push, which is what the ramp endpoints, the curve, and the rate blend were written against. The volume and mouse actions keep ReadAxisFromDevice, where absolute position is the meaning.
| Property |
Type |
Default |
Description |
PressureScaledRate |
bool |
false |
Master gate. Off keeps every existing macro on the legacy fixed-rate path. |
SlowIntervalMs |
int |
500 |
Light-press period in ms. Clamped 10–2000 here, and floored at IntervalMs at evaluation time so slow can never outrun fast. |
TurboRateCurve |
string |
"Linear" |
Curve applied to the 0..1 pressure before the rate interpolation. Same vocabulary as the mapping rows' output curves: Linear, Aggressive, Relaxed, Wide, ExtraWide. Empty collapses to Linear. |
TurboRateCurveOptions |
IReadOnlyList<GyroLabeledOption> |
- |
Live-language curve choices for the dropdown. |
PressureStartPercent |
int |
0 |
Pressure at which the ramp leaves the slow rate. Below it the rate stays at SlowIntervalMs. Clamped 0–99. The remap runs before the curve. |
PressureEndPercent |
int |
100 |
Pressure at which the ramp reaches the full-press rate. Clamped 1–100. The engine floors the span at one percent. |
ShowsPressureTurboRows |
bool |
- |
Computed: IsRepeatIntervalType && PressureScaledRate. Gates the card's extra rows. |
Reset commands: ResetIntervalMsCommand (100 ms), ResetSlowIntervalMsCommand (500 ms), ResetPressureStartCommand (0), ResetPressureEndCommand (100).
Disconnect Action Properties (#162)
For MacroActionType.DisconnectController. The target is set on the action, not the trigger, so a macro can turn off device X from a chord on device Y.
| Property |
Type |
Default |
Description |
DisconnectTarget |
MacroDisconnectTarget |
TriggeringDevice |
Which device(s) the disconnect targets. Notifies DisplayText and IsDisconnectSpecificDevice. |
DisconnectDeviceGuid |
Guid |
Guid.Empty |
Victim device for SpecificDevice mode. Notifies DisplayText. |
IsDisconnectSpecificDevice |
bool |
- |
Computed: IsDisconnectControllerType && DisconnectTarget == SpecificDevice. Drives the specific-device picker's visibility. |
DisconnectDeviceOptions |
List<MacroDisconnectDeviceOption> |
- |
Computed on read ([XmlIgnore]). Every known device on a Bluetooth path (BluetoothLinkHelper.IsDisconnectTarget). If the saved DisconnectDeviceGuid is offline or forgotten it is appended as a synthetic option, so the selection stays visible in the picker. Populates the Specific-device picker. |
Supporting Enums (MacroItem.cs)
MacroTriggerMode (ordinals pinned): OnPress (0), OnRelease (1), WhileHeld (2), Always (3), CustomExpression (4, v3.2), HoldForMs (5, On Long Press), DoublePress (6), TriplePress (7), SinglePress (8, deferred single), Toggle (9), Turbo (10), ShortPress (11, On Short Press, #253. Shares TriggerHoldMs with HoldForMs to compose tap-vs-hold on one button)
MacroTriggerSource: InputDevice, OutputController
MacroRepeatMode: Once, FixedCount, UntilRelease
MacroActionType (append-only, since the clipboard serializes these as ints, so members are never reordered): ButtonPress, ButtonRelease, KeyPress, KeyRelease, Delay, AxisSet, SystemVolume, AppVolume, MouseMove, MouseButtonPress, MouseButtonRelease, MouseScroll, ToggleTouchpadOverlay (v3.2), LightbarColor / LightbarColorClear / LightbarModeSet / LightbarModeCycle (v3.1+), SetGyroEngaged (#120), Rumble / RumbleStop (v3.1+), RumbleTrigger / RumbleTriggerStop (#102), PlaySound / SoundStop (#83), MouseRecenter (#108), MouseFixPosition (#109), MouseLimitRegion (#110), DisconnectController (#162), RunProgram (launch an external program/file), TextBlock (#201, Unicode text injection), PointerModeCycle / PointerModeSet (#203, Wii pointer mode), GuideLedBrightness (#209), MoveMouseToScreenPosition (33, #9), RepeatKeyWhileHeld (34), RepeatVcButtonWhileHeld (35), ToggleVcButton (36), ToggleKey (37), GyroRecenter (38), AxisHold (39), MouseWheelTap (40), MouseNudge (41), CycleTapList (42), ToggleMouseButton (43), ToggleVcAxis (44), RepeatVcAxisWhileHeld (45), ToggleWheel (46), AxisAdd (47, #237), ComboBreak (48, #237), AxisSetLatched (49, #251), AxisLatchRelease (50, #251), AxisScale (51, #251), HeadphoneVolumeUp (52), HeadphoneVolumeDown (53), VoiceListenWhileHeld (54, #317), SwitchLayer (55, #377). The volume pair steps DeviceSlotConfig.HeadphoneVolume by 10% and clamps at the ends, persisting like any other Audio-tab edit. SwitchLayer is one-shot: it writes the shift runtime's CustomLayer override under the Latch activator's own lock-and-version discipline, so the layer stays engaged until another switch, a Latch or Cycle transition, or a profile switch. Its parameter is MacroAction.SwitchLayerMask (default "Base") and its gate is IsSwitchLayerType. The append-only rule is stated in code beside the enum.
The editor's type picker is grouped, not a flat list of these members. See MacroTypeCatalog under PadViewModel's Macros section.
MacroDisconnectTarget (#162): TriggeringDevice (0), SpecificDevice (1), SlotDevices (2), AllDevices (3). Picks the disconnect victim: the trigger's device(s), one picked device, every Bluetooth device on the pad's slot, or every Bluetooth device PadForge knows.
MacroLightbarHoldMode: Reactive, Sticky (v3.1)
MacroLightbarColorSource: Fixed, RandomHue, PaletteStep (v3.1)
MacroRumbleHoldMode: Reactive, Sticky (v3.1)
MacroMouseButton: Left, Right, Middle, X1, X2
MacroAxisTarget: None, LeftStickX, LeftStickY, RightStickX, RightStickY, LeftTrigger, RightTrigger
MacroAxisDirection: Any, Positive, Negative
MacroAxisSource: OutputController, InputDevice
MacroButtonStyle: Xbox360, DualShock4, Numbered
File: MacroItem.cs
Checkbox for a single gamepad button. Reads/writes bits from the parent MacroAction's ButtonFlags (or CustomButtonWords for Extended slots).
| Property |
Type |
Description |
Label |
string |
Button display name. |
Flag |
ushort |
Xbox/PlayStation bitmask flag (0 for custom). |
CustomIndex |
int |
Extended button index (0-based). −1 = use Flag. |
IsChecked |
bool |
Button selected. Reads/writes parent's flags. |
| Method |
Description |
Refresh() |
Re-evaluates IsChecked after external state change. |
KeyDisplayItem
File: MacroItem.cs
Wraps a VirtualKey with a localized display name for ComboBox binding.
| Property |
Type |
Description |
Key |
VirtualKey |
The virtual key value. |
DisplayName |
string |
Localized display name. |
MacroDisconnectDeviceOption
File: MacroItem.cs
One row in the DisconnectController action's Specific-device picker (#162): a known Bluetooth-pathed device by GUID and display name.
| Property |
Type |
Description |
Guid |
Guid |
Device instance GUID. |
Name |
string |
Resolved device name. |
File: MenuEditorItem.cs
Editor VM for one radial / touch menu (#9), backing a row of PadViewModel.Menus. Wraps the LIVE MenuDefinitionEntry stored on the slot's MappingSet (write-through, like the mapping grid edits its live rows). Every mutation raises Changed, which PadViewModel forwards to ConfigItemDirtyCallback so the settings go dirty. Cells materialize lazily: the editor shows every cell position for the current shape, and a cell's entry exists in MenuDefinitionEntry.Items only once it carries a label or a binding.
Slot Context
Set on every row by PadViewModel.ApplyMenuButtonStyle. All four providers are internal delegates, not bound properties.
| Member |
Type |
Description |
ButtonStyle |
MacroButtonStyle |
Button lettering, derived from the slot's output type. Same derivation the macro editor uses. |
RawButtonCount |
int |
Raw button count for Extended and Nintendo slots, from ExtendedConfig.ButtonCount. Defaults to 11. |
RawProfileId |
string |
The slot's HIDMaestro profile slug, so lettered wires label their buttons correctly. |
SupportsControllerButtons |
bool |
Xbox, PlayStation, Nintendo, and Extended only. A MIDI or Keyboard+Mouse output cannot press a controller button, so its cells omit the choice rather than offering it with a warning. |
DescriptorDisplayProvider |
Func<string,string> |
Resolves a descriptor to its display name for the Custom host's subtitles. |
InputChoicesProvider |
Func<IEnumerable<InputChoice>> |
The slot's SlotAvailableInputs, for the Custom host's three pickers. |
RowBoundProvider |
Func<int,int,bool> |
Whether a cell is already bound through a mapping row, which is what the read-only RowBound binding kind reports. |
MacroNamesProvider |
Func<IReadOnlyList<string>> |
The slot's macro names, live, for the #390 macro cells. |
Identity and shape
| Property |
Type |
Description |
Name |
string |
User-facing name. |
Enabled |
bool |
Menu active. |
KindIndex |
int |
0 = Radial, 1 = Grid (combo index = enum value). Switching to Grid forces HasCenter = false, both rebuild Cells. |
IsRadial |
bool |
Computed: Kind == MenuKind.Radial. Gates the Center Cell checkbox. |
KindOptions |
IReadOnlyList<MenuIntOption> |
Style combo items (Menu_Style_* resx). Instance accessor over a static backing field on purpose: WPF {Binding} resolves against the DataContext instance and never finds static properties. |
CellCount |
int |
Cell count, clamped 1–20. Rebuilds Cells. |
HasCenter |
bool |
Radial only: adds the center cell (index 0, Steam's "Radial Menu Center Button"), selected while inside the deadzone. Rebuilds Cells. |
FireTypeIndex |
int |
Fire mode, clamped 0–3 over FireOptions (Menu_Fire_* resx): 0 On Click, 1 On Click Release, 2 On Touch Release, 3 While Hovered. SelectedFireDescription is the selected option's longer explanation. |
EngageDeadzonePercent |
int |
Engage deadzone, clamped 1–95. |
Host surface
| Property |
Type |
Description |
HostOptions |
IReadOnlyList<MenuHostOption> |
The five built-in host surfaces: Gamepad LeftStick, Gamepad RightStick, Touchpad 0, Touchpad 1 (labels from Menu_Host_* resx, touchpads displayed 1-based as "Touchpad 1" / "Touchpad 2"), and Custom for the devices the named surfaces cannot describe (joysticks, wheels, anything not detected as a gamepad). An authored descriptor outside that grammar is appended as its own entry so the selection never silently lies. Never gated on assignment or online state, because imported profiles land on slots with nothing assigned yet. |
SelectedHost |
MenuHostOption |
Selected host. Defaults to the right stick when the stored descriptor matches nothing. Selecting a non-touchpad host resets HostHalf to 0. |
HostIsTouchpad |
bool |
Computed from SelectedHost. Shows the Pad Half row. |
IsCustomHost |
bool |
Computed: the Custom host is selected, which swaps in the CustomX / CustomY / Click pickers (CustomXChoices, CustomXSelected, CustomXSubtitle, and their Y and click twins) and their own record buttons. |
HostHalfOptions / HostHalfIndex |
list / int |
Pad Half picker for touchpad hosts: 0 Whole Pad, 1 Left Half, 2 Right Half. Clamped 0–2. |
HostRecording |
bool |
Freeform-recorder state. |
HostRecordIcon / CustomXRecordIcon / CustomYRecordIcon / ClickRecordIcon |
string |
Record-button glyphs: Stop (E71A) on the target currently recording, Record (E7C8) otherwise, mirroring the Aim Engage record button. |
PendingRecordTarget |
MenuRecordTarget |
Which of Host, CustomX, CustomY, Click the next recorded descriptor lands on. Private setter. PrepareRecord(MenuRecordTarget) sets it. |
| Method |
Description |
TryApplyRecordedHost(string) |
Folds a freeform-recorded descriptor onto a host choice: stick axis and click reads pick the stick, touchpad-family reads pick the pad. Returns false when the recorded input has no host surface (buttons, gyro, keys). |
TryApplyRecorded(MenuRecordTarget, string) |
The same fold aimed at any of the four record targets. |
PrepareRecord(MenuRecordTarget) |
Aims the next recording at one target. |
Overlay geometry
| Property |
Type |
Description |
ShowLabels |
bool |
Draw cell labels on the overlay. |
PosXPercent / PosYPercent |
int |
Overlay center as work-area percents, clamped 0–100 (50/50 = centered). |
ScalePercent |
int |
Overlay scale, clamped 10–400. |
OpacityPercent |
int |
Overlay opacity, clamped 5–100. |
Cells
| Member |
Type |
Description |
Cells |
ObservableCollection<MenuCellItem> |
One row per visible cell position: grid 0..N−1, radial (optional center 0 +) ring 1..N. |
RebuildCells() |
internal |
Rebuilds the rows for the current shape. Existing item entries keep their data. Out-of-range entries (a shrunken ring's tail, a removed center) are pruned from the definition so exports and the overlay agree with the editor. |
EnsureItem(int) / DropItemIfEmpty(...) |
internal |
Write-through for a cell edit: materializes the MenuItemDefinition on first use, drops it again when the label and both bindings are cleared. |
Per-row resets (canon: every setting row has one): ResetHostCommand (right stick, whole pad, custom descriptors cleared), ResetHostHalfCommand (whole pad), ResetStyleCommand (Radial), ResetFireCommand (On Click), ResetCellsCommand (4 cells, no center), ResetGeometryCommand (50/50, scale 100%, opacity 90%, labels on), ResetDeadzoneCommand (25), plus ResetCustomXCommand / ResetCustomYCommand / ResetClickCommand for the Custom host's three inputs.
File: MenuEditorItem.cs
One cell row in the menu editor: a label, an optional icon, and one direct binding. The backing MenuItemDefinition reference is null until the cell has content.
| Property |
Type |
Description |
Index / IsCenter |
int / bool |
Cell position. Header renders "Center" or the localized "Cell {n}". |
Label |
string |
Cell label. Setting it materializes the item entry, clearing it drops an otherwise-empty entry. |
HasIcon / IconName / IconImage / ShowIconGlyph |
bool / string / ImageSource / bool |
Cell icon: the stored reference, the resolved image, and the glyph fallback used when the reference resolves to no image. Written through the internal SetIcon(string), which the view-layer picker calls. An empty reference clears the icon and prunes an otherwise-empty item. |
BindingKind |
int |
Derived from which field is set: 0 none, 1 keyboard key, 2 VC button (Xbox mask on Xbox and PlayStation slots, 1-based raw button number on Extended slots, per the owner's button style), 3 RowBound (no direct binding but bound through mapping rows, a read-only sentinel, and the cell still fires as a menu-item source), 4 Macro (#390). Selecting a kind seeds a default and zeroes the others. |
BindingKindOptions |
IReadOnlyList<MenuIntOption> |
Built per read, dynamic per slot type. None and Keyboard Key everywhere. Controller Button only where the slot's output can press one, or as a marked "unsupported" entry when a slot-type switch left a stale button binding, so the selection never lies. Macro only while the slot has macros, or when the cell already carries a name. RowBound appears only while that is the current kind. |
ShowKeyPicker / ShowButtonPicker / ShowMacroPicker |
bool |
Computed from BindingKind (1 / 2 / 4). |
SelectedKeyVk |
int |
Virtual-key code. KeyOptions offers the full VirtualKey vocabulary the macro editor's Key Press offers, with the same localized labels. VirtualKey.None is dropped: a cell with no key is an empty cell, cleared by its reset button, never by a "None" pick. |
SelectedButtonFlag |
int |
Xbox-family button flag, labels mirrored from the macro editor's MacroButtonNames table. |
SelectedMacroName |
string |
The macro this cell fires, by name (#390). |
MacroOptions |
IReadOnlyList<MenuHostOption> |
The slot's macro names from MacroNamesProvider. A name the slot no longer declares stays in the list under a "missing" label rather than vanishing, and the runtime treats the stale name as an inert no-op. |
ResetCellCommand |
RelayCommand |
Clears label and binding, which drops the item entry. |
Cell Icons and Icon Packages (#390)
A cell's Icon is one reference string, resolved by PadForge.Common.MenuIconResolver.Resolve into a frozen ImageSource. Three shapes, tried in that order, with misses cached as null so a menu rebuild never re-probes the disk for a known-absent file:
pficon://Package/entry, an entry inside a registered icon package.
- A loose image path, exe-relative or absolute. The shape test wants a directory separator or a drive colon plus one of the pack image extensions. Existence is the loader's problem.
- A bare Steam binding-icon name, resolved under the Steam install.
PadForge.Common.IconPackageManager owns the packages. A package is a .pficons file: a zip carrying a manifest.json name and the images. Recognized image formats are .png, .jpg, .jpeg, .bmp, .gif, one list shared by the pack probe, the entry lister, and the editor's loose-file gate, so every surface agrees on what an icon is.
| Member |
Description |
Scheme / FileExtension |
"pficon://" and ".pficons". |
Packages |
The registry: PackageRef rows of Name and Path. The stored path is relative to the exe directory when the pack lives under it, absolute otherwise. |
RegistryChanged |
Raised on any registry change. MenuIconResolver subscribes and drops its whole cache, since a registration, rename, or removal changes what pficon:// references resolve to and misses are cached. |
Register(string filePath) / Register(string, out string probedName) |
Adds a pack, returning its registered name. |
Unregister(string name) |
Removes a pack. |
LoadRegistry(...) / SaveRegistry() |
Settings round-trip. |
IsPackageRef / MakeRef / TryParseRef / DisplayName |
Reference-string grammar. |
ResolvePackageFile / ResolvePath / MakeStoredPath |
Path resolution against the exe directory. |
TryReadIcon(string iconRef) |
The bytes behind one reference. |
ListIcons(string packageName) / ListIconsInFile(string filePath) |
Entry names, for the picker. |
ExportPackage(string destFilePath, string displayName, IEnumerable<string> imageFiles) |
Builds a pack from loose images. Entry names are the source file names, with a " (2)" suffix on same-named files from different folders. It writes to a temp file and moves it into place, so a mid-write failure never leaves a truncated pack at the destination. |
File: MenuEditorItem.cs
MenuHostOption is one pickable host surface (Descriptor, Label, IsTouchpad), and it is also reused for the macro picker's rows. MenuIntOption is a generic labeled int option used by the style, half, fire, binding-kind, and button combos (Value, Label, Description). The key picker reuses SocdKeyOption from KbmSlotConfig instead. Both option types override ToString() with the label.
ProfileShortcutViewModel
File: ProfileShortcutViewModel.cs
Wraps a GlobalMacroData instance for data binding in the ProfilesPage shortcuts card. One ViewModel per shortcut row.
Constructor
public ProfileShortcutViewModel(
GlobalMacroData data,
Action<ProfileShortcutViewModel> deleteCallback,
Action<ProfileShortcutViewModel> saveCallback)
deleteCallback removes the row. saveCallback triggers a settings save via SettingsService.MarkDirty().
Switch Mode
| Property |
Type |
Description |
SwitchMode |
SwitchProfileMode |
Two-way bound to mode ComboBox. Setter writes through to Data.SwitchMode and raises IsSpecificMode. |
IsSpecificMode |
bool (computed) |
true when SwitchMode == Specific. Controls visibility of the target profile dropdown. |
SwitchModes |
ObservableCollection<SwitchProfileModeItem> |
Dropdown items: Next, Previous, Specific, ToggleWindow, ToggleVCsDisabled (v3.2). |
SwitchProfileModeItem is a class extending ObservableObject with a read-only Mode and a mutable DisplayName that updates in place on culture change. ToString() returns DisplayName so ComboBox display refreshes on language switch without rebuilding the list.
Target Profile (Specific mode)
Selection binds to a stable ID, not a display name. The previous name-keyed properties were removed: rebuilding a fresh ObservableCollection<string> on every culture refresh replaced the ComboBox ItemsSource instance, which cleared SelectedItem and wrote null back through the binding, wiping Data.TargetProfileId / TriggerDeviceGuid / SwitchMode. The persistent-collection + stable-ID-selection shape fixes that.
| Member |
Type |
Description |
TargetProfileId |
string |
Two-way selection target. Empty string is the localized "Default" sentinel. Any other value is a ProfileEntry.Id. Getter reads Data.TargetProfileId ?? "". Setter writes through (empty maps to null) and invokes the save callback. |
ProfileChoices |
ObservableCollection<ProfileChoice> |
Persistent dropdown items: the "Default" sentinel plus one per SettingsManager.Profiles entry. |
RebuildProfileChoices() |
method |
Rebuilds ProfileChoices in place from SettingsManager.Profiles. The page's DropDownOpened handler calls it so newly-saved / deleted profiles surface without tearing down the shortcut row. |
Trigger Device
| Member |
Type |
Description |
TriggerDeviceGuid |
Guid |
Two-way selection target. Guid.Empty is the localized "Any device" sentinel. Getter/setter proxy Data.TriggerDeviceGuid and invoke the save callback on change. |
DeviceChoices |
ObservableCollection<DeviceChoice> |
Persistent dropdown items: the "Any device" sentinel plus one per online, named SettingsManager.UserDevices entry. |
RebuildDeviceChoices() |
method |
Rebuilds DeviceChoices in place from SettingsManager.UserDevices. Called from the page's DropDownOpened handler so newly-connected / disconnected devices surface without a row teardown. |
ProfileChoice and DeviceChoice are the same mutable-wrapper pattern as SwitchProfileModeItem: a stable key (ProfileId / DeviceGuid) plus a DisplayName that updates in place on culture change, with ToString() returning DisplayName. Selection is unaffected by a language switch because no item is added or removed.
| Property |
Type |
Description |
ButtonComboDisplay |
string (computed) |
Human-readable trigger combo. Joins entries with " + ", resolving each to a friendly name (e.g., "A (Xbox Controller) + LT+ (Xbox Controller)"). During recording, appends countdown. |
Name resolution helpers:
- ResolveButtonName(int index, Guid deviceGuid). Gamepad-type devices use standard names (A, B, X, Y, LB, RB, Back, Start, LS, RS, Guide) for indices 0–10. Keyboard devices resolve via VirtualKey enum. Others fall back to "Button N".
- ResolveAxisName(int index, Guid deviceGuid, AxisTriggerDirection direction). Gamepad axes use LX/LY/LT/RX/RY/RT names with +/– suffix. Others use "Axis N+/–".
- ResolveDeviceName(Guid deviceGuid). Returns ResolvedName from UserDevices, or null for Guid.Empty.
Recording State
| Property |
Type |
Description |
IsRecording |
bool |
true during Learn mode. Raises LearnButtonText and LearnButtonIcon. |
RecordingCountdown |
int |
Seconds remaining. Raises ButtonComboDisplay on change. |
LearnButtonText |
string (computed) |
"Recording..." (Profiles_ShortcutLearning) or "Record" (Profiles_ShortcutLearn) |
LearnButtonIcon |
string (computed) |
\uE71A (Stop) or \uE7C8 (Record) |
| Method |
Description |
SetLearnedButtons(TriggerButtonEntry[] entries) |
Saves captured entries to Data.TriggerEntries, clears recording state, invokes save callback. |
CancelRecording() |
Clears IsRecording without saving. |
NotifyComboChanged() |
Raises ButtonComboDisplay for live display during recording. |
Commands
| Command |
Action |
DeleteCommand |
Invokes _deleteCallback. Removes this shortcut from the list. |
ClearCommand |
Sets Data.TriggerEntries = null, updates display, saves. |
StickConfigItem
File: StickConfigItem.cs
One thumbstick section in the Sticks tab. Gamepad presets: 0 = Left, 1 = Right. Custom Extended: 0..N per ThumbstickCount.
Identity
| Property |
Type |
Description |
Title |
string |
Display title (e.g., "Left Thumbstick", "Stick 1"). Read-only. |
Index |
int |
Stick index (0-based). Read-only. |
AxisXIndex |
int |
Raw axis index for X in RawHidState.Axes (custom Extended only, -1 for gamepad). Read-only. |
AxisYIndex |
int |
Raw axis index for Y in RawHidState.Axes (custom Extended only, -1 for gamepad). Read-only. |
IsPointerStick |
bool |
Set at construction. True on a KBM slot, where stick 0 is mouse movement and stick 1 the scroll wheel. Those are rate outputs, so the speed multiplier row applies there and only there. |
IsMouseStick |
bool |
Set at construction. True only for the KBM slot's stick 0. Gates the Momentum rows, since a coasting scroll wheel is kinetic scrolling and out of #291's scope. |
Speed and Momentum
| Property |
Type |
Default |
Range |
Description |
Sensitivity |
double |
1.0 |
0.1–5.0 |
Flat speed multiplier. Live for the pointer sticks above only. On a gamepad stick the per-axis response curves own the shaping. Reset: ResetStickSensitivityCommand. |
MomentumEnabled |
bool |
false |
- |
Stick trackball (#291): flick the mouse stick and release, and the cursor keeps traveling, coasting to a stop on the same constant-deceleration physics the touchpad's Momentum uses. |
MomentumGlide |
double |
0.90 |
0.80–1.00 |
The coast's friction band, identical to the touchpad's Momentum Glide. 0.80 full friction, 1.00 frictionless. |
Reset commands: ResetMomentumCommand, ResetMomentumGlideCommand.
Deadzone Configuration
All percentage properties clamped to 0–100. Each has a *Digit companion for digit-based binding (signed 16-bit: +/−32768).
| Property (%) |
Digit Property |
Default |
Description |
DeadZoneX |
DeadZoneXDigit |
0 |
X deadzone percentage. |
DeadZoneY |
DeadZoneYDigit |
0 |
Y deadzone percentage. |
AntiDeadZoneX |
AntiDeadZoneXDigit |
0 |
X anti-deadzone percentage. |
AntiDeadZoneY |
AntiDeadZoneYDigit |
0 |
Y anti-deadzone percentage. |
MaxRangeX |
MaxRangeXDigit |
100 |
X max range (positive), 1–100. |
MaxRangeY |
MaxRangeYDigit |
100 |
Y max range (positive), 1–100. |
MaxRangeXNeg |
MaxRangeXNegDigit |
100 |
X max range (negative), 1–100. |
MaxRangeYNeg |
MaxRangeYNegDigit |
100 |
Y max range (negative), 1–100. |
CenterOffsetX |
CenterOffsetXDigit |
0 |
X center offset, −100 to 100. |
CenterOffsetY |
CenterOffsetYDigit |
0 |
Y center offset, −100 to 100. |
Linear |
- |
0 |
Linear interpolation factor (0–100). |
Deadzone Shape
| Property |
Type |
Default |
Description |
DeadZoneShape |
DeadZoneShape |
ScaledRadial |
Shape enum. Notifies DeadZoneShapeIndex and Is*Shape properties. |
DeadZoneShapeIndex |
int |
0 |
ComboBox SelectedIndex. Maps display order to enum. |
IsAxialShape |
bool |
- |
Computed. |
IsRadialShape |
bool |
- |
Computed: Radial or ScaledRadial. |
IsSlopedShape |
bool |
- |
Computed: SlopedAxial or SlopedScaledAxial. |
IsHybridShape |
bool |
- |
Computed. |
HasSlopedWedges |
bool |
- |
Computed: SlopedAxial, SlopedScaledAxial, or Hybrid. |
Display order: ScaledRadial(0), Radial(1), Axial(2), Hybrid(3), SlopedScaledAxial(4), SlopedAxial(5).
Sensitivity Curves
| Property |
Type |
Default |
Description |
SensitivityCurveX |
string |
"0,0;1,1" |
X-axis sensitivity curve (control point string). |
SensitivityCurveY |
string |
"0,0;1,1" |
Y-axis sensitivity curve. |
PresetNameX |
string |
- |
Computed: matched preset name for X curve. |
PresetNameY |
string |
- |
Computed: matched preset name for Y curve. |
| Static |
Description |
CurvePresetNames |
string[] of available curve preset display names. Rebuilt on culture change. |
Calibration
| Property |
Type |
Default |
Description |
IsCalibrating |
bool |
false |
Center calibration in progress. |
HardwareRawX |
short |
- |
Unprocessed hardware X value (not affected by offset/deadzone). |
HardwareRawY |
short |
- |
Unprocessed hardware Y value. |
| Method |
Description |
StartCalibration() |
Samples RawX/RawY over ~0.5 s (15 frames at 33 ms). Sets CenterOffsetX/Y to negate drift. |
Boundary Calibration (#174)
Measures the stick's real physical boundary by sweeping the rim, then reshapes it toward a circle at runtime (Step 3 warp). Offered only for the two primary physical thumbsticks the warp covers.
| Property |
Type |
Default |
Description |
BoundaryMap |
string |
"" |
Serialized measured boundary (StickBoundary format). Empty = uncalibrated, no reshaping. The setter rebuilds the radar overlay and circularity readout. |
HasBoundaryCalibration |
bool |
- |
Computed: BoundaryMap is non-empty. |
SupportsBoundaryCalibration |
bool |
- |
Set at construction. True for the Xbox/PlayStation two-stick grid and Extended primary sticks 0/1. False for KBM pseudo-sticks and Extended custom sticks 2+. |
IsCalibratingBoundary |
bool |
false |
A boundary sweep is in progress. Notifies BoundaryButtonText. |
BoundarySectorsRemaining |
int |
0 |
Live count of rim sectors still uncovered during a sweep. Notifies BoundaryButtonText. |
BoundaryButtonText |
string |
- |
Computed button caption: Calibrate / Recalibrate idle prompt, the "N sectors left" countdown, or the second-lap prompt. |
BoundaryPolygonPoints |
PointCollection |
empty |
Measured boundary as a convex polygon in the 200×200 radar plot. |
BoundaryCircularityText |
string |
"" |
Computed circularity readout for the plot. |
| Command |
Description |
CalibrateBoundaryCommand |
Starts (or, if sweeping, commits) a boundary sweep. |
ResetBoundaryCommand |
Cancels any sweep and clears BoundaryMap. |
| Method |
Description |
StartBoundaryCalibration() |
Runs a coverage-driven ~60 Hz rim sweep off HardwareRawX/Y, auto-completing once every sector is covered and at least two full rotations have swept (30 s cap). Clicking again while sweeping commits early. |
Live Preview
| Property |
Type |
Default |
Description |
LiveX |
double |
0.5 |
Live X (0.0–1.0 for Canvas). |
LiveY |
double |
0.5 |
Live Y. |
RawX |
short |
0 |
Processed raw X. Notifies RawDisplay. |
RawY |
short |
0 |
Processed raw Y. |
RawDisplay |
string |
- |
Computed: "X: -1234 (50.0%) Y: 5678 (58.7%)". |
LiveInputX |
double |
0 |
CurveEditor X input (0–1). |
LiveInputY |
double |
0 |
CurveEditor Y input. |
Reset Commands
ResetAllCommand, ResetDeadZoneShapeCommand, ResetCenterOffsetXCommand, ResetCenterOffsetYCommand, ResetDeadZoneXCommand, ResetDeadZoneYCommand, ResetAntiDeadZoneXCommand, ResetAntiDeadZoneYCommand, ResetLinearCommand, ResetSensitivityXCommand, ResetSensitivityYCommand, ResetMaxRangeXCommand, ResetMaxRangeYCommand, ResetMaxRangeXNegCommand, ResetMaxRangeYNegCommand
Static Methods
| Method |
Description |
ApplyCurve(double, string) |
Applies spline LUT curve to a magnitude. Used by preview and Extended raw output. |
BuildTriggerCurvePoints(string, double, double, int, int) |
Builds 0–1 curve points for trigger charts with deadzone flattened. Returns PointCollection. |
TriggerConfigItem
File: TriggerConfigItem.cs
One trigger section in the Triggers tab. Gamepad presets: 0 = Left, 1 = Right. Custom Extended: 0..N per TriggerCount.
Identity
| Property |
Type |
Description |
Title |
string |
Display title (e.g., "Left Trigger", "Trigger 1"). Read-only. |
Index |
int |
Trigger index (0-based). Read-only. |
AxisIndex |
int |
Raw axis index in RawHidState.Axes (custom Extended only, -1 for gamepad). Read-only. |
Configuration
All percentage properties clamped to 0–100 (except MaxRange: 1–100). Each has a *Digit companion for digit-based binding (unsigned 16-bit: 0–65535).
| Property (%) |
Digit Property |
Default |
Description |
DeadZone |
DeadZoneDigit |
0 |
Deadzone %. |
MaxRange |
MaxRangeDigit |
100 |
Max range % (1–100). |
AntiDeadZone |
AntiDeadZoneDigit |
0 |
Anti-deadzone %. |
Sensitivity Curve
| Property |
Type |
Default |
Description |
SensitivityCurve |
string |
"0,0;1,1" |
Sensitivity curve (control point string). |
PresetName |
string |
- |
Computed: matched preset name for the curve. |
| Static |
Description |
CurvePresetNames |
string[] of available curve preset display names. Rebuilt on culture change. |
Live Preview
| Property |
Type |
Default |
Description |
LiveValue |
double |
0 |
Processed trigger value (0.0–1.0). |
RawValue |
ushort |
0 |
Processed raw value. Notifies RawDisplay. |
RawDisplay |
string |
- |
Computed: formatted display "32768 (50.0%)". |
LiveInputForCurve |
double |
0 |
Live input for CurveEditor binding. |
Reset Commands
ResetAllCommand, ResetRangeCommand (resets DeadZone+MaxRange), ResetAntiDeadZoneCommand, ResetSensitivityCommand
ExtendedSlotConfig
File: ExtendedSlotConfig.cs
Per-slot Extended-controller configuration. Drives stick/trigger/POV/button counts, the HID descriptor handed to HIDMaestro for Extended slots, and mapping generation.
| Constant |
Value |
Description |
MaxAxes |
8 |
DirectInput max axis count (shared between sticks and triggers). |
| Property |
Type |
Default |
Range |
Description |
Customize |
bool |
false |
- |
Master toggle for the override fields. When false, the VC is built from the catalog HM profile with no customizations (Product String, layout counts, OEM-name override are ignored even if they hold values). When true, each sub-field is applied on top of the catalog profile via HMProfileBuilder. |
ThumbstickCount |
int |
2 |
0–(MaxAxes - TriggerCount) / 2 |
Thumbsticks (2 axes each). Applied only when Customize is true. |
TriggerCount |
int |
2 |
0–MaxAxes - ThumbstickCount * 2 |
Triggers (1 axis each). Applied only when Customize is true. |
PovCount |
int |
1 |
0–4 |
POV hat switches. Applied only when Customize is true. |
ButtonCount |
int |
11 |
0–128 |
Buttons. Applied only when Customize is true. |
TotalAxes |
int |
- |
- |
Computed: ThumbstickCount * 2 + TriggerCount (max 8). |
MaxThumbsticks |
int |
- |
- |
Computed: max sticks given current triggers. |
MaxTriggers |
int |
- |
- |
Computed: max triggers given current sticks. |
OemNameOverride |
bool |
false |
- |
Whether this slot claims the Windows DirectInput OEM-name table entry for its profile's VID:PID at create time, using ProductString as the label. Customize-gated. |
ProductString |
string |
"" |
- |
Custom product string applied to the HID descriptor when Customize is true. |
VendorId |
int |
0 |
- |
VID override. 0 means "use the active profile's VID" (the box then shows the profile value). Customize-gated. Applied at profile-build time via HMProfileBuilder.Vid. |
ProductId |
int |
0 |
- |
PID override. 0 means "use the active profile's PID". Customize-gated. Applied via HMProfileBuilder.Pid. |
ForceFeedbackEnabled |
bool |
true |
- |
Whether the HID PID 1.0 force-feedback block is appended to the descriptor. Customize-gated. Toggling on a live VC forces a destroy + recreate because HIDMaestro bakes the descriptor at create time. |
| Method |
Description |
ComputeAxisLayout(out int[], out int[], out int[]) |
Computes interleaved axis indices per group. |
ResetToDefaults() |
Resets every field to its fresh-install default in place. Never replaces the instance (MainWindow's autosave hook binds to this object's PropertyChanged). Triggers drop to 0 first so the stick default isn't clamped away by the shared-axis budget. |
The v2 ExtendedPreset enum (Xbox360 / DualShock4 / Custom) and the ApplyPresetDefaults() method that paired with it were dropped in v3 (commit d57a725). v3 picks layouts from HIDMaestro.Core's embedded catalog instead (225 profiles across 32 vendors), with Customize as the single boolean that gates user overrides on top of the catalog profile.
ExtendedSlotConfigData
Serializable DTO for persisting in PadForge.xml. All properties have [XmlAttribute].
| Property |
Type |
Default |
SlotIndex |
int |
0 |
Customize |
bool |
false |
ThumbstickCount |
int |
2 |
TriggerCount |
int |
2 |
PovCount |
int |
1 |
ButtonCount |
int |
11 |
OemNameOverride |
bool |
false |
ProductString |
string |
"" |
VendorId |
int |
0 |
ProductId |
int |
0 |
ForceFeedbackEnabled |
bool |
true |
VendorId / ProductId default to 0, meaning "use the active profile's VID/PID" (no override).
ForceFeedbackEnabled defaults to true so v3.0.0/v3.0.1/v3.0.2 PadForge.xml files (which never wrote this attribute) deserialize with FFB enabled. Customize-gated like the layout overrides: only honored when Customize == true.
Older PadForge.xml files written by v2 contained a Preset attribute (Xbox360 / DualShock4 / Custom). v3 ignores it on read since the ExtendedPreset enum was removed in commit d57a725. Equivalent layouts in v3 come from the active HIDMaestro profile.
MidiSlotConfig
File: MidiSlotConfig.cs
Per-slot MIDI output configuration: CC/note counts, starting numbers, channel, and velocity.
| Property |
Type |
Default |
Range |
Description |
Channel |
int |
1 |
1–16 |
MIDI channel (1-based). |
CcCount |
int |
6 |
0–128 - StartCc |
CC output count. |
StartCc |
int |
1 |
0–127 |
Starting CC number. Re-clamps CcCount. |
NoteCount |
int |
11 |
0–128 - StartNote |
Note output count. |
StartNote |
int |
60 |
0–127 |
Starting note number. Re-clamps NoteCount. |
Velocity |
byte |
127 |
0–127 |
Note velocity for button presses. |
| Method |
Description |
GetCcNumbers() |
Returns int[] of sequential CC numbers from StartCc. |
GetNoteNumbers() |
Returns int[] of sequential note numbers from StartNote. |
MidiSlotConfigData
Serializable DTO. All properties have [XmlAttribute].
| Property |
Type |
Default |
SlotIndex |
int |
0 |
Channel |
int |
1 |
CcCount |
int |
6 |
StartCc |
int |
1 |
NoteCount |
int |
11 |
StartNote |
int |
60 |
Velocity |
byte |
127 |
DeviceSlotConfig
File: DeviceSlotConfig.cs
Per-(slot, device) output configuration. Renamed from PlayStationSlotConfig in commit 3fd97c89. It is not PlayStation-only. Drives the Adaptive Triggers and Lighting tabs. Held per physical device on a slot (PadViewModel.PerDeviceSlotConfigs), so two devices on one slot each carry their own config. Parallel to ExtendedSlotConfig and MidiSlotConfig: ObservableObject with a paired [XmlAttribute] data record.
Adaptive Triggers (per trigger)
| Property |
Type |
Default |
Description |
LeftTriggerMode / RightTriggerMode |
AdaptiveTriggerMode |
Off |
Effect mode. Off reverts to the standard linear response. |
LeftStartPosition / RightStartPosition |
byte |
0 |
Start of the pull range the effect targets. |
LeftEndPosition / RightEndPosition |
byte |
255 |
End of the pull range (full travel). |
LeftStrength / RightStrength |
byte |
200 |
Effect force (0–255). |
LeftFrequency / RightFrequency |
byte |
10 |
Vibration frequency for the Vibration modes. |
Lightbar base color
| Property |
Type |
Default |
Description |
LightbarRed / LightbarGreen |
byte |
0 |
Base RGB red / green channel. |
LightbarBlue |
byte |
0xFF |
Base blue channel. Fresh slot lights blue (Sony player-1 color). |
LightbarEnabled |
bool |
false |
Master toggle for the user-configured base color. Off leaves whatever the game last wrote. |
Audio mirror / speaker passthrough (#83)
| Property |
Type |
Default |
Description |
AudioPassthroughEnabled |
bool |
false |
Mirror the system audio to this pad's built-in speaker (per device). |
AudioMirrorSourceId |
string |
"" |
MMDevice ID of the render endpoint to capture. Empty = the system default. |
Synthetic touchpad pressure (#239)
| Property |
Type |
Default |
Description |
TouchpadSyntheticPressure |
bool |
false |
Synthesize a pressure reading on pads whose hardware reports none (DualShock 4, DualSense, Steam Controller 2015). No touch reads 0, a resting touch reads TouchpadSyntheticTouchPercent, a pad click reads 100%. Valve pads with true analog pressure keep their raw readings while this is off. |
TouchpadSyntheticTouchPercent |
int |
50 |
The touch stop of that curve, percent of full pressure a resting unclicked touch reads. Clamped 0-100. |
Haptic mirror engage gate (#185)
Applies to haptic-tone sinks only (Joy-Con, Switch Pro, Steam family). Sony/Wii speaker mirrors stay ungated.
| Property |
Type |
Default |
Description |
AudioMirrorEngageMode |
string |
"Always" |
Always, Input (while an input is held), or Rumble (while game vibration is active). |
AudioMirrorEngageDeviceGuid |
string |
"" |
Device carrying the engage input for Input mode. |
AudioMirrorEngageButton |
string |
"" |
Input descriptor held to engage in Input mode. |
AudioMirrorEngageReleaseMs |
int |
500 |
How long the mirror keeps playing after the engage source drops (ms). |
High-tone filter (#202)
Filters the single (pitch, amplitude) pair the haptic-tone sinks reduce everything to, upstream of every family encoder.
| Property |
Type |
Default |
Description |
AudioToneFilterMode |
string |
"Off" |
Off, Cut (silence above the limit), or Fold (octave-halve into the pass band). |
AudioToneLimitHz |
int |
800 |
Ceiling for Cut / Fold in Hz. |
Persona haptics (#271 item 1)
| Property |
Type |
Default |
Description |
AudioPersonaHapticsEnabled |
bool |
false |
Render the virtual DualSense's authored haptic audio (persona UAC channels 3/4) on this device's actuators through the haptic-tone chain. Off by default because the derived tones only approximate the designer's track. |
AudioPersonaHapticsGain |
int |
100 |
Input gain percent (25–300) applied before the tone reducer, so a quiet authored track can still reach the actuators. |
Reset commands: ResetPersonaHapticsCommand, ResetPersonaHapticsGainCommand (both on PadViewModel, alongside the Audio tab's other row resets).
Steam Controller 2026 PCM haptics (#381)
| Property |
Type |
Default |
Description |
AudioTritonLowPassHz |
int |
250 |
Low-pass cutoff in Hz for the Steam Controller 2026 PCM haptic stream, clamped 60-1000. Past roughly 250 Hz the pad's actuators start behaving like small speakers. 250 is one requester's hardware-measured threshold on their own unit, not a device specification, which is why it stays tunable. |
Audio DSP chain (#347)
Per (slot, device) like the rest of the bag, never gated on OutputType. The chain runs on the AudioPassthroughService sink only, upstream of the Opus encoder. Edited from the PadViewModel.AudioDsp.cs cards.
| Property |
Type |
Default |
Description |
AudioCrossfeedLevel |
int |
0 |
bs2b crossfeed level, clamped 0–9: 0 off, 1–3 crossfeed, 4–6 easy crossfeed, 7 Jan Meier, 8 the bs2b default, 9 custom. Skipped on the speaker paths, which carry a mono downmix. |
AudioCrossfeedIsCustom |
bool |
- |
Computed: level 9. Shows the two custom knobs. |
AudioCrossfeedCutHz |
int |
700 |
Custom crossover cutoff, clamped 300–2000 (libbs2b's BS2B_MINFCUT / BS2B_MAXFCUT). |
AudioCrossfeedFeedDb |
double |
4.5 |
Custom feed level in dB, clamped 1.0–15.0 (libbs2b's BS2B_MINFEED / BS2B_MAXFEED in tenths). |
AudioEqEnabled |
bool |
false |
Master switch for the parametric EQ, separate from an empty band list so a tuned EQ can be muted without being lost. |
AudioEqBands |
string |
"" |
The band list as one attribute encoded by EqBandCodec, bridged to grid rows by PadViewModel.AudioDsp.cs. |
AudioEqPreampDb |
double |
0 |
Preamp in dB applied before the EQ, clamped −30..12. AutoEq imports carry their negative preamp through here. |
AudioLimiterEnabled |
bool |
true |
On by default, since a positive EQ band without a limiter clips the Opus encoder. |
AudioLimiterCeiling |
int |
98 |
Ceiling as a percent of full scale, clamped 5–100. |
Headphone jack and audio path
| Property |
Type |
Default |
Description |
HeadphoneVolume |
int |
100 |
Headphone-jack hardware volume percent, clamped 0–100. Maps onto the DS5 output report's VolumeHeadphones byte over Sony's own scePad window. 0% writes 0x00. Owned by the Audio-tab card and the HeadphoneVolumeUp / HeadphoneVolumeDown macro actions. |
AudioOutputPath |
AudioOutputPath |
Automatic |
Where the DualSense plays its audio (output report byte 7 bits 4–5). |
Ds5AudioBufferLength |
int |
48 |
DualSense Bluetooth audio buffer length (#314): the packet 0x11 header byte governing the transport that carries speaker audio, haptics, and microphone capture together. Clamped 16–255, defaulting to AudioPassthroughService.Ds5AudioBufferLengthDefault. Lower is less delay and more dropout risk. |
| Command |
Description |
ResetHeadphoneVolumeCommand |
Back to 100. |
ResetDs5AudioBufferLengthCommand |
Back to the 48 default. |
ResetAudioOutputPathCommand |
Back to Automatic. |
Macro lightbar override (#63)
Transient runtime state set by MacroActionType.LightbarColor. Not persisted ([XmlIgnore]).
| Member |
Type |
Default |
Description |
MacroOverrideR / MacroOverrideG / MacroOverrideB |
byte |
0 |
Override RGB. |
MacroOverrideStartUtc / MacroOverrideHoldEndUtc / MacroOverrideExpiresAtUtc |
DateTime |
MinValue |
Hold window bounds. Full intensity over [Start, HoldEnd], linear fade to 0 over [HoldEnd, Expires]. |
MacroOverrideHoldMode |
MacroLightbarHoldMode |
Reactive |
Reactive (decay-fade) or Sticky (held until cleared). |
HasActiveMacroLightbarOverride |
bool |
- |
Computed: UtcNow < ExpiresAtUtc. |
ComputeMacroOverrideIntensity() |
method |
- |
0..1 scalar for the override RGB. 1.0 for Sticky. Ramps for Reactive. |
ClearMacroOverride() |
method |
- |
Releases a Sticky override. |
Mic LED
| Property |
Type |
Default |
Description |
MicLedMode |
MicLedMode |
Off |
Mic-mute LED state. Notifies IsMicLedFollowDevice. |
MicLedFollowDeviceId |
string |
"" |
CoreAudio endpoint id polled by FollowDeviceMute. |
IsMicLedFollowDevice |
bool |
- |
Computed: mode is FollowDeviceMute. |
MicLightOn |
bool |
- |
Legacy XML shim over MicLedMode (true = Solid, false = Off). |
MicLedAvailableDevices |
List<MicLedDeviceItem> |
- |
Endpoints for the follow-device dropdown. RefreshMicLedDevices() re-enumerates. |
Player LEDs (#191)
| Property |
Type |
Default |
Description |
PlayerLedMode |
PlayerLedMode |
PlayerNumber |
Bottom-row player pips. PlayerNumber idles on the virtual controller's number. |
PlayerLedBrightness |
PlayerLedBrightness |
High |
Pip brightness (byte 42). |
Guide LED (#209)
| Property |
Type |
Default |
Description |
GuideLedMode |
GuideLedMode |
DeviceDefault |
DeviceDefault never writes, Fixed holds GuideLedBrightness, Battery re-maps battery percent to brightness on a slow cadence with a floor of 10 so a low battery stays visible. Xbox One and later pads take the GIP LED command over \\.\XboxGIP (USB only, XboxGipGuideLedWriter). The 2015 Steam Controller takes SDL's process-global home-LED hint (SteamHomeLedSetter). Switch Pro Controllers, right Joy-Cons, the combined pair, and the charging grip take per-device SDL_SetJoystickLED (SwitchHomeLedSetter, #226). Notifies IsGuideLedFixed. |
IsGuideLedFixed |
bool |
- |
Computed: mode is Fixed. Gates the brightness slider. |
GuideLedBrightness |
int |
100 |
Fixed-mode brightness percent. Clamped 0–100. Writers scale it onto each device's own range: 0-47 for GIP per MS-GIPUSB, 0..1 for the SDL hint, and a 4-bit subcommand 0x38 intensity for the Switch home LED. |
Lightbar mode and animation
| Property |
Type |
Default |
Description |
LightbarMode |
LightbarMode |
PlayerNumber |
Base lightbar effect. Idle modes (Off, Static) only work on config change. Animated modes run the dispatcher's ~30 Hz timer. |
InputReactiveMode |
InputReactiveMode |
Off |
Per-press flash overlay on top of the base mode. Notifies IsInputReactiveActive, ShowPaletteForBase, ShowPaletteForOverlay, IsInputReactiveFixed. |
InputReactiveR / G / B |
byte |
0xFF |
Per-press flash color for the Fixed overlay variant. |
LightbarPeriodMs |
int |
3000 |
Animation period for time-based modes. Clamped 250–10000. |
LightbarColorCycleSmooth |
bool |
true |
ColorCycle blends between palette entries when true. |
LightbarRainbowBrightness |
int |
100 |
Rainbow output scale. Clamped 0–100. |
LightbarBatteryLowR/G/B |
byte |
red |
Battery-mode 0% endpoint color (default red). |
LightbarBatteryHighR/G/B |
byte |
green |
Battery-mode 100% endpoint color (default green). |
LightbarInputHoldMs |
int |
0 |
Input-reactive full-intensity hold before decay. |
LightbarInputDecayMs |
int |
600 |
Input-reactive decay length. |
LightbarPalette |
ObservableCollection<LightbarPaletteEntry> |
- |
ColorCycle palette. Add / Remove / Reset commands. |
LightbarInputReactivePalette |
ObservableCollection<LightbarPaletteEntry> |
- |
Dedicated palette for the InputReactive = Cycle overlay. |
Audio-to-lightbar (#55)
| Property |
Type |
Default |
Description |
AudioLightbarEnabled |
bool |
false |
Legacy toggle (migrated into LightbarMode on load). |
AudioLightbarSensitivity |
double |
4.0 |
Pre-clamp gain on the audio peak. Clamped 1–20. |
AudioLightbarMode |
AudioLightbarMode |
Pulse |
Pulse / Thresholds / Gradient / CrossFade behavior. |
AudioLowR/G/B |
byte |
green |
Quiet-band color. |
AudioMidR/G/B |
byte |
yellow |
Mid-band color. |
AudioHighR/G/B |
byte |
red |
Loud-band color. |
AudioLowToMidPercent |
double |
33 |
Low→Mid threshold. Clamped 0–100. |
AudioMidToHighPercent |
double |
66 |
Mid→High threshold. Clamped 0–100. |
AudioCrossFadePercent |
double |
5.0 |
Half-width of the crossfade window around each threshold. Clamped 0–50. |
Each control has a matching Reset…Command (mirroring the Sticks / Triggers per-row reset pattern), plus the palette add/remove commands AddPaletteColorCommand, RemovePaletteColorCommand, AddInputReactivePaletteColorCommand, RemoveInputReactivePaletteColorCommand, ResetInputReactivePaletteCommand, ResetPaletteCommand.
Enums and helper classes (DeviceSlotConfig.cs)
AdaptiveTriggerMode: Off(0), Feedback(1), Weapon(2), Vibration(3), MultiplePositionFeedback(4), SlopeFeedback(5), MultiplePositionVibration(6). Sony's seven PS5 SDK effect modes.
AudioOutputPath: Automatic(0), StereoHeadset(1), MonoHeadset(2), HeadsetAndSpeaker(3), SpeakerOnly(4), FollowHeadphoneJack(5). Values 1–4 map to firmware paths 0–3, the four names in duaLib's scePad surface. Automatic writes nothing and keeps the #83 behavior: firmware routing, with PadForge forcing the speaker only while it plays sounds there. FollowHeadphoneJack tracks the pad's own jack detect. The jack bit is read by AudioPassthroughService's sink-owned JackWatch on either transport (USB input report 0x01 or Bluetooth 0x31, the duaLib PluggedHeadphones status bit), independent of any persona lane. Plugged resolves to StereoHeadset, unplugged to SpeakerOnly, and no reading to the default. Persisted numerically, so the members are append-only.
MicLedMode: Off(0), Solid(1), Pulse(2), FollowDeviceMute(3).
PlayerLedMode: Off(0), Player1(1), Player2(2), Player3(3), Player4(4), All(5), PlayerNumber(6, default). Sequential to map 1:1 with the dropdown.
PlayerLedBrightness: High(0), Medium(1), Low(2).
GuideLedMode (#209): DeviceDefault(0), Fixed(1), Battery(2).
LightbarMode: Off(0), Static(1), Breathing(2), Rainbow(3), ColorCycle(4), AudioPulse(5), AudioPulseRandom(6), AudioPulseRainbow(7), AudioThresholds(8), AudioGradient(9), AudioCrossFade(10), InputReactive(11, legacy), InputReactiveCycle(12, legacy), InputReactiveFixed(13, legacy), Battery(14), Strobe(15), PlayerNumber(16, default). Legacy 11–13 stay in the enum for XML round-trip and are migrated to InputReactiveMode on load.
InputReactiveMode: Off(0), Random(1), Cycle(2), Fixed(3).
AudioLightbarMode: Pulse(0), Thresholds(1), Gradient(2), CrossFade(3).
MicLedDeviceItem: Id (CoreAudio endpoint string), Display (label, prefixed [In] / [Out]).
LightbarPaletteEntry (ObservableObject): R, G, B (byte), Hex (two-way "RRGGBB" shim, [XmlIgnore]).
DeviceSlotConfigData
Serializable DTO. All scalar properties are [XmlAttribute]. The two palettes are [XmlArray]. Key defaults: LeftEndPosition/RightEndPosition = 255, LeftStrength/RightStrength = 200, LeftFrequency/RightFrequency = 10, LightbarBlue = 0xFF, AudioMirrorEngageMode = "Always", AudioMirrorEngageReleaseMs = 500, AudioToneFilterMode = "Off", AudioToneLimitHz = 800, TouchpadSyntheticPressure = false, TouchpadSyntheticTouchPercent = 50, AudioPersonaHapticsEnabled = false, AudioPersonaHapticsGain = 100, AudioTritonLowPassHz = 250, HeadphoneVolume = 100 (a missing attribute on legacy XML keeps the initializer, so old configs load at full volume, the pre-feature effective behavior), Ds5AudioBufferLength = 48, AudioOutputPath = Automatic, AudioCrossfeedLevel = 0, AudioCrossfeedCutHz = 700, AudioCrossfeedFeedDb = 4.5, AudioEqEnabled = false, AudioEqBands = "", AudioEqPreampDb = 0, AudioLimiterEnabled = true, AudioLimiterCeiling = 98, GuideLedMode = DeviceDefault, GuideLedBrightness = 100, LightbarMode = Off (the DTO default, migrated to PlayerNumber via LightingRev), PlayerLedMode = Off (likewise). It also carries a DeviceGuid (per-device key, empty = a legacy slot-level entry the loader fans out) and LightingRev (schema revision: 0 predates the PlayerNumber default and triggers the Off→PlayerNumber lift on load). LightbarPaletteEntryData is the palette element (R, G, B byte attributes).
KbmSlotConfig
File: KbmSlotConfig.cs
Per-slot keyboard + mouse output config (discussion #205, SOCD / Snap Tap). Same per-slot lane as MidiSlotConfig: lives on PadViewModel.KbmConfig, referenced into the engine by InputService, persisted as KbmSlotConfigData.
| Property |
Type |
Default |
Description |
SocdMode |
string |
"Off" |
Resolution mode: Off, LastWins, Neutral, FirstWins. Stored locale-stable. |
SocdPairs |
string |
"87:83\|65:68\|38:40\|37:39" |
Pipe-separated "vkA:vkB" decimal pairs (the DefaultSocdPairs constant: W/S, A/D, Up/Down, Left/Right). |
SocdPairItems |
ObservableCollection<SocdPairItem> |
- |
Editable projection of SocdPairs for the pair editor. |
AvailableSocdModes |
IReadOnlyList<SocdModeOption> |
- |
Culture-cached dropdown items. |
| Constant |
Value |
DefaultSocdPairs |
"87:83\|65:68\|38:40\|37:39" |
| Command |
Description |
AddSocdPairCommand |
Appends a fresh W/S pair. |
RemoveSocdPairCommand |
RelayCommand<SocdPairItem>. Removes the passed pair. |
ResetSocdCommand |
Resets SocdMode to Off and SocdPairs to DefaultSocdPairs in place. |
| Static Method |
Description |
GetKeyOptions() |
Culture-cached SocdKeyOption list, the same key set and labels as the KBM mapping targets. |
Companion classes:
- SocdModeOption carries Value (engine-stable), Name, and Description. Localized dropdown entry.
- SocdKeyOption carries Vk (int) and Label. One pickable key.
- SocdPairItem (ObservableObject) carries VkA and VkB. Edits reserialize the owner's SocdPairs. KeyOptions returns GetKeyOptions().
- KbmSlotConfigData is the DTO with [XmlAttribute] SlotIndex, SocdMode (default "Off"), and SocdPairs (default DefaultSocdPairs).
RemoteLinkTrustedPeer
File: RemoteLinkTrustedPeer.cs
One trusted peer in the Settings paired-peer manager (#138). The name is editable (committed on focus loss, persisted to the trust store). The online dot refreshes in place so editing isn't disrupted.
| Property |
Type |
Description |
Name |
string |
Friendly name. Setting it persists the rename via the callback. Empty falls back to the host name. |
HostName |
string |
The peer's machine name. HasHostName gates showing it. |
IsOnline |
bool |
Live-session state. Notifies OnlineText, CanConnect. |
OnlineText |
string |
Computed localized "Online" / "Offline". |
ReachableHostPort |
string |
Where the peer is reachable now (host:port from discovery), or null. Notifies CanConnect. |
CanConnect |
bool |
Computed: on the LAN but not connected. Shows the Connect button. |
FingerprintHex |
string |
Full key fingerprint. |
FingerprintDisplay |
string |
Computed: short grouped fingerprint, with a gamepad-only suffix when applicable. |
PairedUtc |
string |
Pairing timestamp. |
GamepadOnly |
bool |
Whether the peer is restricted to gamepad input. |
| Command |
Description |
ConnectCommand |
Reconnect to this known peer (no SAS prompt). |
RevokeCommand |
Revoke trust (raises the revoke callback with the fingerprint). |
RemoteLinkNearbyPeer
File: RemoteLinkNearbyPeer.cs
One PadForge PC discovered on the LAN (#138), shown in the "Nearby PCs" list. Immutable. Clicking Pair initiates pairing without IP typing.
| Property |
Type |
Description |
Name |
string |
Discovered machine name. |
HostPort |
string |
Host:port to pair against. |
FingerprintHex |
string |
The peer's key fingerprint. |
IsPaired |
bool |
Already in the trust store. |
IsConnected |
bool |
A live session already exists. |
DisplayName |
string |
Computed: name with a Connected / Paired state suffix. |
ButtonLabel |
string |
Computed: Connected (disabled) / Connect (paired) / Pair (new). |
CanPair |
bool |
Computed: the action button is enabled unless already connected. |
| Command |
Description |
PairCommand |
Initiates pairing with HostPort. |
See Also
- Architecture Overview: MVVM design philosophy,
CommunityToolkit.Mvvm usage
- XAML Views: View counterparts:
DashboardPage, PadPage, DevicesPage, SettingsPage
- Services Layer:
InputService, SettingsService, DeviceService consumed by ViewModels
- Engine Library:
Gamepad, RawHidState, KbmRawState, VrRawState, MidiRawState, PadSetting
- Settings and Serialization:
SettingsManager data synced to/from ViewModels by SettingsService
- Input Pipeline:
InputManager state reflected in PadViewModel output snapshots
- 2D Overlay System:
ControllerModel2DView, ControllerSchematicView bound to PadViewModel
- 3D Model System:
ControllerModelView bound to PadViewModel
Last updated for PadForge 4.5.0.