Skip to content

XAML Views Reference

The main window shell, the page hierarchy, the dialog windows, custom controls, value converters, and how themes switch at runtime.

v4 (2026-07-12): Updated for the v4 ember restyle (#175). 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.


All views live in PadForge.App/Views/ (PadForge.Views namespace), except MainWindow.xaml in PadForge.App/. Shared custom controls sit alongside them in PadForge.App/Controls/ (CurveEditor, TriggerTravelArc, plus the code-only RangeSlider, EqCurveControl, and SettingResetButton, the per-setting reset button most setting rows carry) and PadForge.App/Views/Controls/ (LabeledShapeIcon, ProfilePill, TriggerEffectGraph). Styled with WPF UI 4.3 (Lepo.Wpf.Ui) for Fluent 2 design.

Contents


Application Shell (MainWindow)

Files: MainWindow.xaml, MainWindow.xaml.cs

Application shell: app branding bar, NavigationView sidebar, page content area, status bar, and driver overlay.

App Branding Bar

A custom branding bar replaces the traditional title bar. Grid x:Name="AppBrandingBar" (48px tall) holds a hamburger Button (PaneToggleBtn) on the left and a ui:TitleBar (AppTitleBar) that carries the PadForge icon + name in its header and native minimize/maximize/close with Snap Layout support. The window sets ExtendsContentIntoTitleBar="True" and WindowBackdropType="Mica". The full-screen toggle (FullScreenBtn) in the title bar's trailing content sets WindowChrome.IsHitTestVisibleInChrome="True" so it stays clickable in the non-client area. The hamburger button carries no such attribute.

The window ground is Mica plus a SteelLayer (Grid.RowSpan="3", hit-test-invisible): a near-black steel fill with a faint ember wash top-right and a cold wash on the left. UpdateSteelLayer collapses it in Light theme. The old SyncBarBackgrounds pane-sampling scheme and the TranslateTransform.Y = -12 gap-closing transform are gone.

XAML Structure

Three-row Grid: - Row 0 (auto): App branding bar (AppBrandingBar: hamburger + ui:TitleBar). - Row 1 (star): NavigationView with page containers in its ContentOverlay. - Row 2 (auto): Status bar Border (StatusBarBorder). - Full-window layers (Grid.RowSpan="3"): SteelLayer (ground), DriverOverlay (ZIndex 1000), FirstRunOverlay (ZIndex 900), ShutdownOverlay / StartupOverlay (ZIndex 1001).

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>  <!-- Branding bar -->
        <RowDefinition Height="*"/>     <!-- NavigationView -->
        <RowDefinition Height="Auto"/>  <!-- Status bar -->
    </Grid.RowDefinitions>

    <Grid x:Name="SteelLayer" Grid.RowSpan="3" IsHitTestVisible="False"> <!-- ember steel ground --> </Grid>

    <Grid x:Name="AppBrandingBar" Grid.Row="0" Height="48"> <!-- Hamburger Button + ui:TitleBar --> </Grid>

    <ui:NavigationView x:Name="NavView" Grid.Row="1" PaneDisplayMode="Left" OpenPaneLength="244"
                       IsBackButtonVisible="Collapsed" IsPaneToggleVisible="False"
                       AlwaysShowHeader="False" CompactPaneLength="48">
        <ui:NavigationView.FooterMenuItems>
            <ui:NavigationViewItem x:Name="NavSettings" Tag="Settings"/>
            <ui:NavigationViewItem x:Name="NavAbout" Tag="About"/>
        </ui:NavigationView.FooterMenuItems>
        <ui:NavigationView.ContentOverlay>
            <Grid>
                <views:DashboardPage x:Name="DashboardPageView" Visibility="Visible"/>
                <views:PadPage x:Name="PadPageView" Visibility="Collapsed"/>
                <views:DevicesPage x:Name="DevicesPageView" Visibility="Collapsed"/>
                <views:SettingsPage x:Name="SettingsPageView" Visibility="Collapsed"/>
                <views:ProfilesPage x:Name="ProfilesPageView" Visibility="Collapsed"/>
                <views:AboutPage x:Name="AboutPageView" Visibility="Collapsed"/>
            </Grid>
        </ui:NavigationView.ContentOverlay>
    </ui:NavigationView>

    <Border x:Name="StatusBarBorder" Grid.Row="2"> <!-- Status bar (5 columns) --> </Border>
    <Grid x:Name="DriverOverlay" Grid.RowSpan="3" Panel.ZIndex="1000"> <!-- ... --> </Grid>
</Grid>

No WPF Frame-based navigation. All pages are instantiated once and visibility-swapped:

  1. NavView_SelectionChanged passes the selected item's Tag string to NavigateToTag.
  2. All page containers set to Visibility.Collapsed.
  3. The matching page set to Visibility.Visible.
  4. For controller slots (tag "Pad{N}", 1-based, so Pad1 is the first slot), PadPage's DataContext is set to the selected PadViewModel.

This preserves control state (scroll position, selected tabs, text fields) across navigation since pages are never destroyed.

NavigationView items use 48px height and 14px font size. Dashboard, Profiles, and Devices are built programmatically in BuildNavigationItems(). Settings and About are declared in XAML as FooterMenuItems (NavSettings / NavAbout) with ember-colored icons, not as BuildNavigationItems entries. Items (in order):

Tag Content Icon Source
Dashboard Dashboard F404 FontIcon (home) BuildNavigationItems()
Profiles Profiles E8F1 FontIcon (people) BuildNavigationItems()
Devices Devices E772 FontIcon (USB) BuildNavigationItems()
Settings Settings E713 FontIcon (gear) XAML footer (NavSettings)
About About E946 FontIcon (info) XAML footer (NavAbout)

Dynamic controller cards are appended after "Devices" (index 3 onward) via RebuildControllerSection(), followed by an "Add Controller" entry while any slot remains (HasAnyControllerTypeCapacity()). Each card is a fixed-width pill (233 px) inside its NavigationViewItem and contains: - Flame power toggle. Its heat shows the slot's state (ember live, gold waiting, outline cold) and its tooltip names it - Global slot number in telemetry mono - Mini type segment: Xbox / PlayStation / Nintendo / Extended / KB+M / MIDI / VR tiles in VirtualControllerGroups.InOrder, active type lit, plus a fixed-width "#N" instance token. The MIDI tile is disabled while no MIDI API starts and the VR tile without SteamVR, unless the slot already carries that type - Delete button, revealed on card hover

Called on slot create, delete, or reorder. Uses a _rebuildingControllerSection guard to prevent re-entrancy during selection changes.

Drag controller cards to reorder virtual controller slots:

  1. OnCardDragStart. PreviewMouseLeftButtonDown records start position.
  2. OnNavViewDragMove. PreviewMouseMove checks threshold, then BeginCardDrag() creates a CardDragAdorner (ghost preview) and InsertionLineAdorner (drop indicator).
  3. UpdateDragPosition. Updates adorner positions, computes target index.
  4. EndCardDrag. PreviewMouseLeftButtonUp (OnNavViewDragEnd) finishes the drag. A drop on another card swaps the two through InputService.SwapSlots(padIndexA, padIndexB). A drop between cards of the same type group moves the slot through InputService.MoveSlot(sourcePadIndex, targetVisualPosition). Escape cancels.

Cross-Panel Device Drag-Drop

Drag devices from the Devices page to a sidebar controller card: - DevicesPage initiates DragDrop.DoDragDrop() with a DataObject keyed "DeviceInstanceGuid" carrying the device's InstanceGuid (a Guid). - Each sidebar card's DragOver / Drop handlers (OnSidebarCardDragOver / OnSidebarCardDrop) accept the drop and assign the device through DeviceService.AssignDeviceToSlot.

Add Controller Popup

Popup with one button per output type, built in ShowControllerTypePopup. Re-invoking the method while the popup is open closes it rather than opening a duplicate, and a reopen inside 300 ms of the last close is suppressed, so the same click cannot dismiss and re-raise it. The seven buttons follow VirtualControllerGroups.InOrder:

Button AutomationId Icon Per-type cap
Xbox AddXbox360Btn Xbox SVG SettingsManager.MaxXbox360Slots (16)
PlayStation AddDS4Btn DS4 SVG SettingsManager.MaxPlayStationSlots (16)
Nintendo AddNintendoBtn Switch logo SVG SettingsManager.MaxNintendoSlots (16)
Extended AddRawBtn Joystick SVG SettingsManager.MaxExtendedSlots (16)
Keyboard+Mouse AddKeyboardMouseBtn E961 glyph SettingsManager.MaxKeyboardMouseSlots (16)
MIDI AddMidiBtn E8D6 glyph SettingsManager.MaxMidiSlots (16)
VR AddVrBtn F119 glyph SettingsManager.MaxVrSlots (1)

The method counts each type from Pads[].OutputType and disables a button (icon opacity 0.35) when the global slot total reaches 16 or that type hits its own per-type cap. A type at its own cap gets a "(max N)" tooltip, e.g. Main_Nintendo_Max_Format = "Nintendo (max {0})". MIDI additionally requires a MIDI API that started, Windows MIDI Services or the legacy one (SettingsViewModel.IsMidiAvailable, the Settings card's cached answer) and VR requires SteamVR (HMaestroVRController.IsAvailable()). HasAnyControllerTypeCapacity() is a separate check: it tallies created slots from SettingsManager.SlotCreated and returns true while the total stays under 16 (MaxPads). The same seven types repeat, in the same order, in the sidebar card's type segment and on the dashboard slot cards.

Status Bar

Bottom Border (StatusBarBorder), five columns: 1. Status text. StatusText binding (StatusMessageText), trimmed with CharacterEllipsis. A #175 decay sweep fades it out before clearing (code-behind BeginAnimation). 2. Active-profile pill. ProfilePill (StatusProfilePill, #175 item 8). Click opens the switcher flyout. An applied auto-switch flares it. 3. Device count. ConnectedDeviceCount + localized suffix, cold telemetry mono. 4. Polling frequency. PollingFrequency formatted {0:F0} + " Hz", cold telemetry mono. 5. Engine indicator. A flame Path (the shared FlameOuterGeometry) with DataTriggers on Dashboard.EngineStateKey: Running fills ember, Idle/Stopping fills gold (WaitBrush), else outline-only. EngineStatusText beside it turns EmberHotBrush when Running.

Driver Overlay

Semi-transparent overlay during driver install/uninstall: - ProgressRing spinner + text message (DriverOverlayText). - Blocks all UI (Grid.RowSpan="3", Panel.ZIndex="1000"). - Shown and hidden by RunDriverOperationAsync(), and directly by the SteamVR install handler.

Full-Window Overlays

Four more full-window layers span all three rows (Grid.RowSpan="3"). SteelLayer sits under the content, and the three overlays above it stay Collapsed until shown:

Layer ZIndex Shown when Contents
SteelLayer (bottom, hit-test off) Dark theme always Ember steel ground under all content
FirstRunOverlay 900 FirstRunTourCompleted is false at launch, or the tour is re-run from Settings. A leftover pre-v4 PadForge.firstrun marker sets the flag and is deleted Welcome panel (WelcomePanel) + spotlight tour (TourCanvas: TourHighlight + TourTip)
ShutdownOverlay 1001 App is closing ProgressRing + "Closing PadForge..." text (Main_ShuttingDown)
StartupOverlay 1001 Orphan-sweep task (App.OrphanSweepTask) still running at launch ProgressRing + "Starting PadForge…" headline (Main_StartingUp) + "Cleaning up virtual controllers left from a previous session." detail (Main_CleaningPreviousSession), auto-hidden on completion

Composition Root (Code-Behind)

MainWindow.xaml.cs is the service wiring hub (~8800 lines). Constructor:

  1. Creates MainViewModel as root and sets DataContext.
  2. Sets child DataContext on Dashboard, Devices, Settings, Profiles pages.
  3. Creates services: SettingsService, InputService, RecorderService, DeviceService.
  4. Wires ViewModel events to services:
  5. StartEngineRequested/StopEngineRequested to InputService.Start()/Stop().
  6. SaveRequested/ReloadRequested/ResetRequested to SettingsService.
  7. Driver install/uninstall to DriverInstaller via RunDriverOperationAsync.
  8. TestRumbleRequested/TestLeftMotorRequested/TestRightMotorRequested per pad.
  9. Recording flow events per pad/mapping row.
  10. Profile management (New, SaveAs, Edit, Load, Delete, RevertToDefault).
  11. Device assignment via DeviceService.

Timer Architecture

Timer Interval Purpose
DispatcherTimer 33ms (~30Hz) InputService._uiTimer fires UiTimer_Tick to push engine state into ViewModels
_driverStatusTimer 5s RefreshHidHideStatus(), RefreshHIDMaestroStatus(), RefreshMidiServicesStatus(), and SweepStatusMessage(). Started in the constructor and stopped only in OnClosing, so the status-bar decay keeps running after the engine stops. Hosting the decay on the engine 30 Hz timer would have burned in "Engine stopped." The HIDMaestro poll reads the driver store folder, since the engine installs HIDMaestro with its first virtual controller
CompositionTarget.Rendering ~60fps Used by all visualization views (3D, 2D, Schematic, MIDI, KBM, VR, MousePreview) for per-frame visual updates

DashboardPage

Files: DashboardPage.xaml, DashboardPage.xaml.cs

Engine toggle, slot summary cards, and the service sections.

Section order is pinned by PadForge.Tests/PageOrderContractTests.cs. Dashboard_SectionsRunInTheDecidedOrder asserts each section title's binding appears once and after the one before it, and that the Services header sits between the slot cards and the Web Controller card. Dashboard_DriverStatusStripIsGone asserts the page carries no ED5D glyph, no Dashboard_Drivers key in the XAML, in Strings.Designer.cs, or in any of the ten resx files, and no HidHideStatusText / MidiServicesStatusText / SteamVrStatusText in the page or DashboardViewModel. It also asserts the Settings page still binds all three. Those rows live there only.

Layout Structure

ScrollViewer
  └─ StackPanel (Margin="24,16,24,16")
       ├─ Page header (icon + title)
       ├─ "Input Engine" section header (E9F5 glyph)
       ├─ CardBorder: Engine status card (EngineCard, Grid, 4 columns)
       │   ├─ Col 0: Engine toggle flame `Path` (FlameOuterGeometry, ember when Running, gold when Idle/Stopping, else outline)
       │   ├─ Col 1: EngineStatus text
       │   ├─ Col 2: PollingFrequencyText
       │   └─ Col 3: Online/Total devices count
       ├─ "Virtual Controllers" section header (E7FC glyph)
       ├─ ItemsControl (SlotsItemsControl, WrapPanel over a CompositeCollection:
       │   SlotSummaries plus the Add Controller tile in the same wrap flow)
       │   ├─ DataTemplate: slot card Border (252px wide, 5 rows)
       │   │   ├─ Row 0: Power flame btn + "Slot" + SlotNumber, Delete button
       │   │   │    right-aligned (revealed on hover or keyboard focus)
       │   │   ├─ Row 1: Type segment (Xbox / PlayStation / Nintendo / Extended /
       │   │   │    KB+M / MIDI / VR) on a recessed track + "#N" instance label
       │   │   ├─ Row 2: Device roster (per device: type glyph, name, E702 Bluetooth
       │   │   │    glyph on a Bluetooth link, battery glyph and percent, marquee on
       │   │   │    overflow) or the DeviceName empty-state line
       │   │   ├─ Row 3: StatusText + mapped/connected counts
       │   │   └─ Row 4: StageLedger chips (per-stage glyphs with hover readout)
       │   └─ Add Controller tile (AddControllerCard, a ClickBorder, 252px, dashed steel outline,
       │        Click from the mouse, Enter or Space → AddControllerRequested, gated on ShowAddController)
       └─ ServicesSection StackPanel (one panel so the welcome tour can ring
            the whole group as a single target)
            ├─ "Services" divider (ServicesHeader, ember tick + hairline rule)
            ├─ "Web Controller" section (E774 glyph)
            │   └─ CardBorder: Enable toggle (EnableWebControllerCheckBox), port
            │      NumberBox + reset, status flame + WebControllerStatus, HTTPS note
            │      (caution border, shown on WebControllerHttpsWarning), QR image +
            │      URL box + copy button (shown on HasWebControllerQr, #296), footer,
            │      then a divider and the Plain HTTP Address section: Also Serve
            │      Plain HTTP and This PC Only toggles with resets, a port and
            │      access-code grid (NumberBox + reset, read-only code box + New
            │      Code), plain status flame, and the plain URL with a copy button
            │      and a QR shown on HasWebControllerPlainQr
            ├─ "Remote Link" section (E969 glyph, #138)
            │   └─ CardBorder: Enable toggle (EnableRemoteLinkCheckBox), auto-reconnect toggle,
            │      port NumberBox + reset, status flame + RemoteLinkStatus text,
            │      identity-protection mode ComboBox, Paired PCs list (per peer: rename,
            │      Allow Remote Assignment Changes toggle, Connect, Revoke, and Assign
            │      Shared Devices while online, then Revoke All), nearby-unpaired list,
            │      my-code readout, connect-by-address box, footer
            ├─ "Head Tracking" section (E77B glyph, #355)
            │   └─ CardBorder: UDP enable toggle (HeadTrackingEnabled), FreeTrack toggle
            │      (HeadTrackingFreeTrack), OpenXR toggle (HeadTrackingOpenXr), OpenXR
            │      runtime ComboBox (HeadTrackingOpenXrRuntimeCombo, enabled with OpenXR),
            │      one Grid of port / rotation range / translation range NumberBoxes plus
            │      six per-axis range NumberBoxes (#403), each with a reset button,
            │      Set Neutral button (HeadTrackingRecenterCommand), HeadTrackingStatus
            │      source line, footer
            ├─ "Motion Server" section (E7AD glyph, DSU)
            │   └─ CardBorder: Enable toggle, port NumberBox, status flame, footer
            ├─ "Game Mods" section (EA86 glyph, #529)
            │   └─ CardBorder: description, Accept DSX Game Mods toggle
            │      (EnableGameModServer), port NumberBox (GameModServerPort) with
            │      ResetGameModPortCommand, Run a DSX.exe Stand-In toggle (GameModDsxStandIn,
            │      tooltip), each toggle with a reset button, status flame +
            │      GameModServerStatus as a polite live region, footer
            ├─ "Game Telemetry" section (EC4A glyph, #531)
            │   └─ CardBorder: description, Listen for Game Telemetry toggle
            │      (GameTelemetryCheckBox, EnableGameTelemetry) with a reset button, then a
            │      Grid enabled with the switch: Game ComboBox (GameTelemetryGameCombo over
            │      GameTelemetryOptions, SelectedGameTelemetryOption) with
            │      ResetGameTelemetryGameCommand, Game Telemetry Port, Forward to Port and
            │      Full Speed (km/h) NumberBoxes, each with its reset command (the two port
            │      rows also enabled by GameTelemetryReadsUdp), then the
            │      Custom Formats… button (GameTelemetryFormatsButton, outside the Grid),
            │      status flame + GameTelemetryStatus as a polite live region, footer
            ├─ "Overlays" section (E700 glyph)
            │   └─ CardBorder: Menu Overlay (EnableMenuOverlay), Shift Layer Flyout
            │      (EnableShiftLayerFlyout), Profile Overlay (EnableProfileOverlay) toggles
            └─ "Touchpad Overlay" section (EDA4 glyph)
                └─ CardBorder: Enable toggle (EnableTouchpadOverlay), opacity slider +
                   NumberBox + reset, reset-position button, status flame +
                   TouchpadOverlayStatus

The Lightbar Mirrors and Razer Sensa HD Haptics sections are gone (#494). Their outputs are the vendor rows on the Devices page, which a virtual controller drives through the Pad page's Lighting and Force Feedback tabs like any device.

Key Bindings

Binding ViewModel Description
EngineStateKey DashboardViewModel Drives the engine flame Path fill: Running = ember, Idle / Stopping = gold (WaitBrush), else outline-only
EngineStatus DashboardViewModel Status text next to engine button
PollingFrequencyText DashboardViewModel Dashboard_PollingHz_Format ("{0:F1} Hz"), e.g. "998.0 Hz". Dashboard_PollingDash while no rate is measured
OnlineDevices / TotalDevices DashboardViewModel Device count display
SlotSummaries DashboardViewModel ObservableCollection<SlotSummary> for slot cards
ShowAddController DashboardViewModel Controls Add Controller card visibility
EnableDsuMotionServer DashboardViewModel DSU enable checkbox
DsuMotionServerPort DashboardViewModel DSU port NumberBox
DsuServerStatus DashboardViewModel DSU status text
EnableGameModServer / GameModServerPort / GameModDsxStandIn DashboardViewModel Game Mods toggle, port NumberBox and stand-in toggle (#529)
GameModServerStatus / IsGameModServerRunning DashboardViewModel Game Mods status text and flame
EnableGameTelemetry / GameTelemetryOptions / SelectedGameTelemetryOption DashboardViewModel Game Telemetry toggle and game picker (#531)
GameTelemetryPort / GameTelemetryForwardPort / GameTelemetryFullSpeed DashboardViewModel NumberBoxes ranged 1024-65535, 0-65535 and 10-1000, each with a reset command
GameTelemetryStatus / IsGameTelemetryRunning DashboardViewModel Game Telemetry status text and flame
EnableWebController DashboardViewModel Web controller enable checkbox
WebControllerPort / WebControllerStatus DashboardViewModel Web controller port and status text
HasWebControllerQr / WebControllerQr / WebControllerUrl DashboardViewModel QR panel visibility, the QR bitmap, and the URL shown beside it (#296)
WebControllerHttpsWarning DashboardViewModel The HTTPS note under the status line
EnableWebControllerPlainHttp / WebControllerPlainHttpPort / WebControllerPlainHttpLocalOnly / WebControllerAccessCode DashboardViewModel The plain HTTP address's toggles, port and access code
WebControllerPlainStatus / IsWebControllerPlainRunning / WebControllerPlainUrl / HasWebControllerPlainQr / WebControllerPlainQr DashboardViewModel The plain address's status line and flame, its URL, and its QR
EnableRemoteLink / RemoteLinkPort / RemoteLinkConnectHost DashboardViewModel Remote Link enable, port, and connect-by-address host (#138)
AutoReconnect DashboardViewModel Remote Link auto-reconnect toggle
IsRemoteLinkRunning / RemoteLinkStatus DashboardViewModel Remote Link status flame and text
RemoteLink DashboardViewModel DashboardViewModel.RemoteLink is a second reference to the same SettingsViewModel instance, not a separate object. It carries the identity-protection modes and hint, the trusted peers, the nearby-unpaired list, and the revoke commands
HeadTrackingEnabled / HeadTrackingFreeTrack / HeadTrackingOpenXr DashboardViewModel Head Tracking UDP, FreeTrack, and OpenXR source toggles (#355, #403)
OpenXrRuntimes / SelectedOpenXrRuntime DashboardViewModel OpenXR runtime picker items and selection
HeadTrackingUdpPort / HeadTrackingRotationRange / HeadTrackingTranslationRange DashboardViewModel NumberBoxes ranged 1-65535, 1-180, and 1-500, each with a reset command
HeadTrackingRangeYaw / Pitch / Roll / X / Y / Z DashboardViewModel Per-axis range NumberBoxes, 0-180 for the rotation axes and 0-500 for the translation axes. 0 follows the family range
HeadTrackingRecenterCommand DashboardViewModel Set Neutral button
HeadTrackingStatus DashboardViewModel Which of the three head-tracking sources is live, or why none is
EnableMenuOverlay / EnableShiftLayerFlyout / EnableProfileOverlay DashboardViewModel Overlays-section toggles
EnableTouchpadOverlay / TouchpadOverlayOpacity / TouchpadOverlayStatus DashboardViewModel Touchpad overlay enable, opacity, and status text

Slot Card DataTemplate Bindings (SlotSummary)

Binding Type Description
PadIndex int Used as Tag for button click routing
SlotNumber int Global slot display number (1-based)
IsEnabled bool Controls power toggle color
OutputType VirtualControllerType Selects which type button is highlighted (ember fill vs tertiary text)
TypeInstanceLabel string Per-type instance number
MappedDevices ObservableCollection<PadViewModel.MappedDeviceInfo> Row 2 device roster (type glyph, name, TransportGlyph on a Bluetooth link, battery glyph and percent, marquee on overflow)
DeviceName string Empty-state line, shown only when MappedDevices.Count is 0
StatusText string e.g. "Forging", "Cold" (enabled with nothing mapped), "Awaiting devices", "Disabled", "Idle"
MappedDeviceCount / ConnectedDeviceCount int Mapped/connected counts
HasMappedDevices bool MappedDeviceCount > 0. Gates the ember flame and the card's warm rim
IsVirtualControllerConnected bool Live VC present. Drops the flame to gold when false
IsSelected bool Card whose pad page is in focus keeps the ember glow at rest
IsCreateFailed bool VC creation failed. Outranks the awaiting-devices tooltip
IsInitializing bool Triggers the ember flash animation (steady ember with reduced motion)
StageLedger ObservableCollection<SlotStageInfo> Row 4 stage chips (sticks / triggers / gyro / lighting / touchpad / audio)

Power Toggle State Machine

The slot power toggle is a flame Path (FlameOuterGeometry), not a glyph. It keys off HasMappedDevices and IsVirtualControllerConnected, not a raw device count. Enabled with no mapped devices stays the cold outline (heat needs fuel). There is no HIDMaestro-install state, since the driver is embedded.

Rows are in trigger order. A later row wins when several hold.

Condition Flame fill Tooltip
IsEnabled=False Cold outline (TextFillColorTertiaryBrush) "Disabled"
IsEnabled=True Cold outline "Forging"
IsEnabled=True + HasMappedDevices=True EmberBrush + glow "Forging"
IsEnabled=True + IsVirtualControllerConnected=False WaitBrush gold when HasMappedDevices=True, else cold outline "Awaiting devices"
IsEnabled=True + EngineStateKey="Stopped" WaitBrush gold when HasMappedDevices=True, else cold outline "Engine stopped"
IsEnabled=True + IsCreateFailed=True (fill unchanged) "Virtual controller failed"
IsInitializing=True EmberBrush (flashing, steady with reduced motion) "Initializing"

Type Switch Buttons

7 type buttons per slot card (Xbox, PlayStation, Nintendo, Extended, KB+M, MIDI, VR) using a custom TypeSwitchButton style, seated on a recessed SegTrackBrush segment. Transparent and borderless at rest, with a rounded subtle-fill background on hover. The active type fills with EmberSegGradient and a glow (steel SteelRaisedBrush on a card with no mapped devices). Inactive types use the tertiary text color. Unavailable types (missing prerequisite, e.g. MIDI while no MIDI API runs, VR without SteamVR) show Cursor.No and a tooltip that says why (Main_MIDI_NotRunning, Main_VR_RequiresSteamVR). Clicks are guarded in code-behind. The power button also uses the TypeSwitchButton style for visual consistency.

UI Automation

AutomationId Element Purpose
EnableWebControllerCheckBox CheckBox Web controller enable toggle
EnableWebControllerPlainHttpCheckBox CheckBox Also Serve Plain HTTP toggle
WebControllerPlainHttpLocalOnlyCheckBox CheckBox This PC Only toggle
WebControllerAccessCodeBox TextBox The read-only access code
NewWebAccessCodeButton Button New Code
EnableRemoteLinkCheckBox CheckBox Remote Link enable toggle (#138)
Reset_{Class}_{Property}_{n} SettingResetButton Per-setting reset buttons on the service cards and paired-PC rows

Event Handlers (Code-Behind)

Handler Event Action
EngineToggle_Click Button.Click Raises EngineToggleRequested
AddControllerCard_Click ClickBorder.Click Raises AddControllerRequested
GameTelemetryFormatsButton_Click Button.Click Opens CustomTelemetryFormatsDialog over GameTelemetryFormats and, when the dialog returns true, writes its Result back (#531)
DeleteSlot_Click Button.Click Raises DeleteSlotRequested(slotIndex)
PowerToggle_Click Button.Click Raises SlotEnabledToggled(slotIndex, !SettingsManager.SlotEnabled[slotIndex]), inverting the live setting rather than the summary VM
XboxType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Xbox). HIDMaestro is embedded so no install gate
DS4Type_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, PlayStation)
NintendoType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Nintendo)
ExtendedType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Extended)
KeyboardMouseType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, KeyboardMouse)
MidiType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Midi)
VrType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Vr) (#49)
SlotCard_Loaded Border.Loaded Wires PreviewMouseLeftButtonDown for drag start
OnCardMouseDown PreviewMouseLeftButtonDown Records drag start position. Skips if inside a Button
OnDragMove PreviewMouseMove Begins/updates card drag with ghost adorner
OnDragEnd PreviewMouseLeftButtonUp Completes swap/insert or fires SlotCardClicked for navigation
OnDragKeyDown PreviewKeyDown Cancels drag on Escape
OnDragCaptureLost LostMouseCapture Cancels the drag when capture is taken away

Dashboard Card Drag Reordering

Drag to reorder (same adorner system as sidebar): - CardDragAdorner. Ghost preview from RenderTargetBitmap snapshot. Adds 4 physical pixels to bitmap dimensions to prevent clipping at high DPI. - InsertionLineAdorner. Accent-colored vertical line at insertion point. - Three zones per card: left 25% = insert before, middle 50% = swap, right 25% = insert after. - Type-group validation: cross-type drag blocked. Same-type only. - Sidebar rebuild deferral: while a sidebar card drag is in progress, RebuildControllerSection() queues itself (_rebuildPendingAfterFade) and EndCardDrag replays the rebuild. - Events: SlotSwapRequested(PadIndexA, PadIndexB) and SlotMoveRequested(SourcePadIndex, TargetVisualPos).


PadPage

Files: PadPage.xaml, PadPage.xaml.cs, PadPage.SettingResets.cs

Per-slot configuration: two-tier tab strip, optional config bars, and 18 tab panels (Tags 0-17). Tier 1 (slot scope) holds Preview, Mappings, Macros, Menus, Bass Shakers, and Output. Tier 2 (device scope) holds the device selector and the capability tabs, most gated on source-device capability.

Layout Structure

Grid (3 rows)
├─ Row 0 (Auto): Two-tier tab strip (StackPanel: two gradient-bordered Borders + assign-offer banner)
│   ├─ Tier 1 (slot scope, ember underline): a WrapPanel of the scope label, identity
│   │   chip, preset chip and the slot tabs, which wrap among themselves in a WrapPanel
│   │   of their own (TabStripButton, GroupName="PadTab")
│   │   ├─ RadioButton "Preview" (Tag=0, x:Name="TabController", AutomationId="TabController")
│   │   ├─ RadioButton "Mappings" (Tag=2, AutomationId="MappingsTab")
│   │   ├─ RadioButton "Macros" (Tag=1, AutomationId="MacrosTab")
│   │   ├─ RadioButton "Menus" (Tag=15, AutomationId="MenusTab", #9)
│   │   ├─ RadioButton "Bass Shakers" (Tag=16, AutomationId="BassShakersTab",
│   │   │    Visibility bound to RumbleAudioTabVisible, #236)
│   │   └─ RadioButton "Output" (Tag=17, AutomationId="OutputTab",
│   │        Visibility bound to OutputTabVisible, #270 follow-up)
│   ├─ Tier 2 (device scope, cold underline): scope label + device ComboBox on the left,
│   │  capability tabs pushed right in a WrapPanel (TabStripButtonCold, GroupName="PadTabDevice")
│   │   ├─ ComboBox (MappedDevices, item = liveness flame Path (LivenessFlameBase) + Name + battery)
│   │   ├─ RadioButton "Sticks" (Tag=3, x:Name="TabSticks")
│   │   ├─ RadioButton "Triggers" (Tag=4, x:Name="TabTriggers")
│   │   ├─ RadioButton "Force Feedback" (Tag=5, x:Name="TabForceFeedback", gated on hasForceFeedback)
│   │   ├─ RadioButton "Wheel" (Tag=11, x:Name="TabWheel", gated on wheel VID/PID)
│   │   ├─ RadioButton "Impulse Triggers" (Tag=9, x:Name="TabImpulseTriggers", gated on hasRumbleTriggers)
│   │   ├─ RadioButton "Adaptive Triggers" (Tag=6, x:Name="TabAdaptiveTriggers", gated on hasAdaptiveTriggers)
│   │   ├─ RadioButton "Lighting" (Tag=7, x:Name="TabLighting", gated on hasLightbar || hasGuideLed
│   │   │    || hasPeripheralLighting, #494)
│   │   ├─ RadioButton "Gyro" (Tag=8, x:Name="TabGyro", gated on any motion sensor, #392)
│   │   ├─ RadioButton "Touchpad" (Tag=10, x:Name="TabTouchpad", gated on hasTouchpad)
│   │   ├─ RadioButton "Audio" (Tag=12, x:Name="TabAudio", gated on hasAudio)
│   │   ├─ RadioButton "Pointer" (Tag=13, x:Name="TabPointer", gated on hasIrPointer, #146)
│   │   └─ RadioButton "Mouse" (Tag=14, x:Name="TabMouse", gated on mouse device, #200)
│   └─ AssignOfferBanner (Border, visible on HasAssignOffer): Assign / Not Now
├─ Row 1 (Auto): Extended, MIDI or Keyboard + Mouse config bar (conditionally visible)
│   ├─ ExtendedConfigBar (Visibility=Collapsed unless OutputType==Extended)
│   ├─ MidiConfigBar (Visibility=Collapsed unless OutputType==Midi)
│   └─ KbmDeviceBar (Visibility=Collapsed unless OutputType==KeyboardMouse)
└─ Row 2 (*): ContentTabControl (a TabControl with no header, whose UI Automation peer lists the selected tab's content), one TabItem per Tag (0-17),
   SelectedIndex bound to SelectedConfigTab

The HMaestroProfileBar preset chip sits inline in tier 1 rather than on its own row. The Extended, MIDI and Keyboard + Mouse bars are the only Row 1 occupants. KbmDeviceBar holds the Keyboard and Mouse pickers (KbmKeyboardDeviceCombo, KbmMouseDeviceCombo, bound to KbmConfig.AvailableKeyboardDevices / AvailableMouseDevices and to KbmKeyboardDevice / KbmMouseDevice), each with its reset button and shown while the surface mode drives its half, and the picked model's capability tags (KbmKeyboardCapabilities, KbmMouseCapabilities, ChipGhost chips).

Custom Styles (UserControl.Resources)

Style Key TargetType Properties
DzLabel TextBlock Width=270, vertically centered, TextFillColorSecondaryBrush. Used for slider row labels.
DzSlider Slider Min=0, Max=100, Width=200, IsSnapToTickEnabled=True, TickFrequency=0.1.
OffsetSlider Slider Extends DzSlider with Min=-100 (for center offset).
DzValueEdit TextBox Width=56, right-aligned, editable percentage value. Neutral hover glow.
DzDigitEdit TextBox Width=56, right-aligned, tooltip "Raw axis value". Raw axis value edit.
DzPercent TextBlock "%" suffix text, TextFillColorTertiaryBrush.
ResetButton ui:Button Content glyph E72C (undo arrow), ContentTemplate FontSize=12, tooltip "Reset". Based on EmberIconButton. Per-row reset.
ResetAllButton ui:Button Padding=8,3, FontSize=11, left-aligned, based on EmberIconButton. Section-level reset.
TabStripButton RadioButton Slot-tier tab. GroupName="PadTab". Checked = #1AFF6B2C ember-tint fill, EmberHotBrush SemiBold text, 2px ember underline. Hover carries EmberHoverGlow.
TabStripButtonCold RadioButton Device-tier tab. GroupName="PadTabDevice" (own radio group). Checked = ColdTintBrush fill, ColdBrush SemiBold text, 2px cold underline. Hover carries ColdHoverGlow.

Custom Tab Strip

Two RadioButton groups. The slot tier (Preview / Mappings / Macros / Menus / Bass Shakers / Output) uses TabStripButton with GroupName="PadTab". The device tier (Sticks through Mouse) uses TabStripButtonCold with GroupName="PadTabDevice". The two groups are independent so WPF never auto-unchecks across tiers. SyncTabStripSelection() keeps exactly one tab checked overall (#175 item 18: only the tier owning the active tab shows a highlight). Its slot-tier test is selected <= 2 || selected == 15 || selected == 16 || selected == 17, since Menus, Bass Shakers, and Output are appended TabItem indices rather than tier-2 tags. Each button stores its Tag (0-17) and sets vm.SelectedConfigTab = Tag on click via TabBtn_Click. ConfigTabControl_PreviewKeyDown swallows Ctrl+Tab and calls CycleConfigTab, which walks the visible RadioButtons of both groups because the header-less TabControl template breaks WPF's own Ctrl+Tab handling (discussion #140).

A TabControl with hidden header (custom ControlTemplate showing only PART_SelectedContentHost) provides content switching. SelectedIndex is bound to SelectedConfigTab.

Tab Visibility Rules

Sticks and Triggers are hidden by output type. The capability tabs follow the selected device alone, whatever the slot type, so a KB+M or MIDI slot with a gamepad selected shows that gamepad's tabs:

Tab Xbox / PlayStation / Nintendo / Extended KB+Mouse MIDI Capability gate
Preview Visible Visible Visible always
Macros Visible Visible Visible always
Mappings Visible Visible Visible always
Menus Visible Visible Visible always (slot scope, #9)
Sticks Visible Visible (Mouse X/Y + Scroll) Hidden always within Xbox/PS/Nintendo/Extended
Triggers Visible Hidden Hidden hidden on an Extended or Nintendo slot whose profile declares no analog triggers (ExtendedConfig.TriggerCount == 0, the Switch Pro's digital ZL/ZR), visible otherwise
Force Feedback Visible if hasForceFeedback Same gate Same gate selected device's CapType is stick-class (Gamepad / Joystick / Driving / Flight / FirstPerson), or the device is a haptic mouse, keyboard or the Razer Sensa row (PeripheralOutputs.IsHapticPeripheral, #494). Hidden for every other keyboard, mouse, touchpad and MIDI device, even on an Xbox/PS/Extended slot
Impulse Triggers Visible if hasImpulseTriggers Same gate Same gate source device has impulse-trigger motors (Xbox One / One S / Elite / Elite Series 2 / Series X|S, Microsoft VID). Xbox 360 and DualSense excluded
Adaptive Triggers Visible if hasAdaptiveTriggers Same gate Same gate source device is a DualSense or DualSense Edge
Lighting Visible if hasLightbar \|\| hasGuideLed \|\| hasPeripheralLighting Same gate Same gate a lightbar (DS4 / DualSense family, the PS Move sphere, or a web controller drawing a DS4 / DualSense) shows the lightbar cards. A mouse, keyboard or vendor lighting row that a lighting path links, or whose record says it has lighting (UserDevice.HasPeripheralLighting), shows the mode card and the peripheral panel (#494). A Guide/HOME-button LED shows only the GuideLedCard: a first-party GIP Xbox pad (One / Elite / Series) on SDL's XInput path, whose LED command works over USB only, or the 2015 Steam Controller (#209), plus the Switch home-LED population (#226: Pro Controller, right Joy-Con, Joy-Con pair, charging grip)
Gyro Visible if hasGyro Same gate Same gate source device has any motion sensor (ud.HasGyro \|\| ud.HasAccel). On an accelerometer-only device the tab shows with its five gyro-rate cards collapsed (#392)
Pointer Visible if hasIrPointer Same gate Same gate source device is an IR-capable Wii Remote (#146)
Touchpad Visible if hasTouchpad Same gate Same gate source device has a touchpad (DualSense family, DS4, Steam Controller)
Wheel Visible if hasWheel \|\| hasGenericWheel Same gate Same gate source device is a force-feedback wheel
Audio Visible if hasAudio Same gate Same gate source has a speaker (DualSense / DS4 / Wii Remote) or plays HD haptic tones (Joy-Con, Switch Pro, Steam Controller / Deck, SC 2026) (#147)
Mouse Visible if source is a mouse Same gate Same gate source device is a mouse (per-device mouse-gesture settings, #200)

VR slots (#49) hide both Sticks and Triggers: the Vr lane reads none of the stick or trigger tuning keys those tabs edit.

Tag numbers: Preview 0, Macros 1, Mappings 2, Sticks 3, Triggers 4, Force Feedback 5, Adaptive Triggers 6, Lighting 7, Gyro 8, Impulse Triggers 9, Touchpad 10, Wheel 11, Audio 12, Pointer 13, Mouse 14, Menus 15, Bass Shakers 16, Output 17. Bass Shakers (4.1.0, #236, AutomationId BassShakersTab) is slot-tier and gates on SLOT TYPE, not device capability: visible via RumbleAudioTabVisible for Xbox, PlayStation, and Nintendo slots plus Extended slots with a force-feedback surface. Output (#270 follow-up, AutomationId OutputTab) is slot-tier too and carries the SOCD and Keep Controller Awake cards. OutputTabVisible is false only for MIDI and VR slots, which have no output-behavior surface. The Audio tab was speaker-only before 3.6.0. It now shows for any haptic-tone pad as well. The Lighting tab used to be lightbar-only. It now also raises for a device with only a Guide/HOME-button LED (#209, and since #226 the Switch home-LED devices: Pro Controller, right Joy-Con, Joy-Con pair, charging grip): the lightbar cards (LightbarModeCard, LightingLightbarSubtitle, LightingPlayerIdleHint) collapse and the GuideLedCard brightness control takes their place. Since #494 it also raises for a lit mouse, keyboard or vendor row: LightbarModeCard stays, the DualShock 4 / DualSense preview art (LightbarPreviewPanel) and the two lightbar hints collapse, and PeripheralLightingPanel shows (see below).

If the selected tab is hidden, the view auto-switches to Preview (index 0). SyncTabVisibility() toggles the device-tier capability tabs from the selected device's capabilities, and also drives the motor activity bars (MotorBarsGrid).

Slot Identity Chip

The Preview tab itself (Tag 0, x:Name="TabController", header text Pad_Tab_Preview) is text-only. The type icon lives in the standalone, non-interactive identity chip at the left of tier 1, which stays ember regardless of which tab is checked. It picks its icon via DataTriggers on OutputType:

  • Xbox → XboxControllerIcon (Image)
  • PlayStation → DS4ControllerIcon (Image)
  • Nintendo → NintendoControllerIcon (Image)
  • Extended → ExtendedControllerIcon (Image)
  • anything else → GenericControllerIcon (the Image default)
  • Midi → EC4F glyph (TextBlock, Image collapsed)
  • KeyboardMouse → E961 glyph (keyboard, TextBlock, Image collapsed)

The DrawingImage resource keys (XboxControllerIcon, DS4ControllerIcon) keep the v2 short names. One icon represents the whole family regardless of which specific HM profile (Xbox 360 / Xbox One / Series / Elite / Adaptive, or DS4 / DualSense / DualSense Edge) the slot ends up running. Beside the icon, one TextBlock carries OutputTypeDisplayName plus a TypeInstanceLabel Run in telemetry mono, so the type name and the "#2" token share a baseline.

Multi-Device Selector

Inline ComboBox bound to MappedDevices / SelectedMappedDevice. Each item shows a liveness-flame Path on the LivenessFlameBase style (fills ember with a glow when IsOnline, outline-only stroke when offline), device Name in cold, and its battery percentage when the device reports one (#167). Sits in tier 2 (device scope) on the left, beside the "Device" scope label, ahead of the right-pushed capability tabs.

UI Automation Properties

AutomationId Element Purpose
TabController RadioButton (tab 0) Preview tab identification. x:Name kept from v2/v3, header text is now "Preview"
MappingsTab RadioButton (tab 2) Mappings tab identification
KbmSurfacesCombo ComboBox Keyboard + Mouse surfaces picker in the preset chip (#408)
HMaestroProfileCombo ComboBox HIDMaestro profile selection for Xbox / PlayStation / Nintendo slots. Extended has its own ExtendedProfileCombo (no AutomationId)
RawStickCountBox TextBox Extended slot thumbstick count override
ExtendedTriggerCountBox TextBox Extended slot trigger count override
RawPovCountBox TextBox Extended slot POV count override
RawButtonCountBox TextBox Extended slot button count override
DeadZoneShapeCombo ComboBox Deadzone shape selector (Sticks tab)
SensitivityXCombo ComboBox Sensitivity X preset (Sticks tab)
TriggerPresetCombo ComboBox Trigger sensitivity preset (Triggers tab)
ControllerModelHost Grid Preview tab visualization host
TouchSensors TouchSensorStrip A Valve slot's touch sensor capsules at the top center of the host, over the 2D and 3D views. Collapsed on every other slot. See TouchSensorStrip
ViewModeToggle Button 2D/3D preview toggle
MacroAddButton / MacroRemoveButton ui:Button Macro list add and remove
MenuAddButton / MenuRemoveButton ui:Button Menu list add and remove (#9)
MacrosTab / MenusTab / BassShakersTab / OutputTab RadioButton The remaining slot-tier tabs
AssignOfferBanner Border The assign-offer banner raised when a newly seen device matches the slot
AssignOfferAccept / AssignOfferDismiss ui:Button Its two answers
Reset_{Class}_{Property}_{n} SettingResetButton Per-setting reset buttons. The numeric suffix keeps each id unique

Event Handlers (Code-Behind)

Handler Trigger Action
PadPage_Loaded UserControl.Loaded Calls ApplyViewMode, SyncTabStripSelection, SyncExtendedConfigBar, SyncMidiConfigBar, the lightbar and audio hex-box syncs, subscribes the sound and icon package registries, refreshes both package lists, syncs the Bass Shakers meter timer, and subscribes PeripheralOutputs.LinksChanged, StatusChanged and ClaimsChanged (#494). PadPage_Unloaded drops those subscriptions
OnPeripheralOutputsChanged PeripheralOutputs events, on the thread that raised them Queues one SyncTabVisibility() on the dispatcher per burst (_peripheralSyncQueued), so a device that gains or loses an output, a path that starts or stops answering, or a claim that changes rank redraws the Force Feedback and Lighting tabs (#494)
OnDataContextChanged DataContextChanged Unsubscribes old VM, subscribes new VM PropertyChanged, resyncs all
ViewModeToggle_Click Button.Click Toggles SettingsViewModel.Use2DControllerView, calls ApplyViewMode
TabBtn_Click RadioButton.Click Sets vm.SelectedConfigTab from Tag
Motor_MouseEnter/Leave ClickBorder.MouseEnter/MouseLeave Hover opacity effect (0.7/1.0)
LeftMotor_Click ClickBorder.Click (ClickMode Press) padVm.FireTestLeftMotor()
RightMotor_Click ClickBorder.Click (ClickMode Press) padVm.FireTestRightMotor()
MapAllToggle_Click Button.Click Starts Map All, or stops it when already active (button text is MapAllButtonText)
CalibrateCenter_Click Button.Click StickConfigItem.StartCalibration()
ProfileCombo_PreviewKeyDown ComboBox.PreviewKeyDown Forwards Enter/Esc on HMaestroProfileCombo and ExtendedProfileCombo to commit / dismiss
ExtendedCustomValue_Changed TextBox.LostFocus Applies clamped Extended layout overrides (sticks/triggers/POVs/buttons)
ExtendedCustomValue_KeyDown TextBox.KeyDown(Enter) Same as LostFocus apply
MidiConfig_Changed TextBox.LostFocus Applies clamped MIDI config, rebuilds mappings if counts change
MidiConfig_KeyDown TextBox.KeyDown(Enter) Same as LostFocus apply
MidiOutputPortBox_DropDownOpened ComboBox.DropDownOpened Reads the output ports again (MidiBackendLegacy.ListOutputPortsBounded) and refills the picker
MidiOutputPortBox_SelectionChanged ComboBox.SelectionChanged Writes the pick through MidiConfig.ChooseOutputPort, empty for None, which also marks it the profile's choice
ResetExtendedSetting_Click / ResetMidiSetting_Click SettingResetButton.Click Resets the Extended or MIDI config field named by the button's Tag (PadPage.SettingResets.cs)
StickPresetX_SelectionChanged ComboBox.SelectionChanged Sets StickConfigItem.SensitivityCurveX from preset
StickPresetY_SelectionChanged ComboBox.SelectionChanged Sets StickConfigItem.SensitivityCurveY from preset
TriggerPreset_SelectionChanged ComboBox.SelectionChanged Sets TriggerConfigItem.SensitivityCurve from preset
AppVolumeProcessDropDown_Opened ComboBox.DropDownOpened action.RefreshAudioProcessesCommand.Execute()
DeviceAxisPicker_DropDownOpened ComboBox.DropDownOpened Populates ComboBox with devices assigned to current slot
DeviceAxisIndexPicker_DropDownOpened ComboBox.DropDownOpened Populates ComboBox with axis-type DeviceObjects from selected device
OnPadVmPropertyChanged PadViewModel.PropertyChanged Syncs tab strip on SelectedConfigTab, resyncs config bars on OutputType

Preview Tab (Tab 0). Detailed

Grid (3 rows)
├─ Row 0 (*): Controller visualization area
│   ├─ ControllerModelView (3D, HelixToolkit)
│   ├─ ControllerModel2DView (2D overlays, Collapsed by default)
│   ├─ ControllerSchematicView (procedural Extended/HID layout, Collapsed)
│   ├─ MidiPreviewView (MIDI, Collapsed)
│   ├─ KBMPreviewView (KB+Mouse, Collapsed)
│   ├─ VRPreviewView (VR hand pair, Collapsed, #49)
│   └─ ViewModeToggle button (top-left, E8B9↔F158 icon)
├─ Row 1 (Auto): Motor activity bars (MotorBarsGrid, 400px wide, centered)
│   ├─ Col 0: Left Motor ProgressBar (MotorCapsuleTemplate) + label, clickable
│   └─ Col 1: Right Motor ProgressBar + label, clickable
└─ Row 2 (Auto): Map All controls
    ├─ Map All / Stop toggle Button (MapAllPillButton, Content=MapAllButtonText, Click=MapAllToggle_Click). Text flips to "Stop" when IsMapAllActive
    └─ MapAllPromptText (EmberHotBrush, SemiBold, collapsed when null)

View Switching Logic (ApplyViewMode):

Output Type HIDMaestro Profile User Pref Active View Toggle Visible
KeyboardMouse . . KBMPreviewView No
Midi . . MidiPreviewView No
Vr . . VRPreviewView No
Extended no dedicated art (HasDedicatedArt false) . ControllerSchematicView No
Extended dedicated art (the Valve pads) 2D / 3D ControllerModel2DView / ControllerModelView Yes
Xbox . 2D ControllerModel2DView Yes
Xbox . 3D ControllerModelView Yes
PlayStation . 2D ControllerModel2DView Yes
PlayStation . 3D ControllerModelView Yes
Nintendo . 2D ControllerModel2DView Yes
Nintendo . 3D ControllerModelView Yes

BindActiveModelView(): Unbinds all six views, subscribes the active view's ControllerElementRecordRequested event, then calls Bind(vm). All views fire ControllerElementRecordRequested with a PadSetting target name for click-to-record. The 2D and 3D views additionally wire AnnotationChipNavigateRequested and AnnotationsToggled.

Motor Activity Bars. Two ProgressBar capsules (MotorBarsGrid, MotorCapsuleTemplate, Minimum=0/Maximum=1) bound to LeftMotorDisplay/RightMotorDisplay. The template fills an ember gradient with a leading-edge glow that grows from zero width at 0%. Each capsule is wrapped in a clickable Grid for motor test. Hover dims to 0.7 opacity via Motor_MouseEnter/Motor_MouseLeave.

Macros Tab (Tab 1). Detailed

Grid (3 columns)
├─ Col 0 (250px): Macro list panel
│   ├─ DockPanel.Top: Add / Remove / Duplicate / Copy / Paste / Copy From buttons
│   └─ ListBox (Macros, item = layer-scope dot when HasLayerScope + Name)
├─ Col 1 (Auto): GridSplitter (4px, draggable)
└─ Col 2 (*): Macro editor (ScrollViewer)
    └─ StackPanel (DataContext=SelectedMacro)
        ├─ Name TextBox (UpdateSourceTrigger=PropertyChanged)
        ├─ Enabled CheckBox
        ├─ Fire mode ComboBox, 12 items in strip order: OnPress, SinglePress,
        │   OnRelease, WhileHeld, HoldForMs, ShortPress, DoublePress, TriplePress,
        │   Toggle, Turbo, Always, CustomExpression
        ├─ Layer-scope ComboBox (LayerMask: Base plus the slot's authored layers)
        ├─ Mode timing rows (TriggerHoldMs, TriggerDoublePressMs, RepeatDelayMs)
        ├─ Trigger Combination panel (ShowsTriggerComboEditor: every mode except
        │   Always and CustomExpression)
        │   ├─ Trigger Source ComboBox (InputDevice/OutputController)
        │   ├─ Dropdown trigger picker (#177): appends an input without recording
        │   ├─ Record + Clear buttons, recording hint, RecordingLiveText while recording
        │   ├─ Trigger input list (TriggerInputItems), one removable row per input.
        │   │   Axis rows carry Invert / Half / Either and a dead-zone slider
        │   ├─ Legacy axis threshold slider (1-100%) and direction ComboBox
        │   │   (Any/Positive/Negative), visible when HasLegacyAxisTrigger
        │   └─ Consume trigger buttons CheckBox
        ├─ Always mode description note
        ├─ Custom Expression formula editor (CustomExpression mode): a-z variable chips (gated on VariableCount) plus operator / comparison / logic / function chips
        │   └─ Variable rows, one per letter: Source ComboBox (Assigned Devices / Virtual
        │      Controller / Game Telemetry, #531), then by source the Record + Clear
        │      buttons (device and virtual controller only), the output channel
        │      ComboBox, or the telemetry ComboBox (TelemetryChannelOptions.Items,
        │      SelectedItem TelemetryOption), each with a reset button
        ├─ Separator
        └─ Action Sequence section
            ├─ Add Action / Remove buttons
            ├─ Actions ListBox (DisplayMemberPath="DisplayText")
            └─ Action editor Border (DataContext=SelectedAction)
                ├─ Action Type ComboBox (grouped, `MacroTypeCatalog.View`)
                └─ Type-specific panels (conditional visibility):

Grouped Action Type Picker. The picker binds ItemsSource="{Binding Source={x:Static vm:MacroTypeCatalog.View}}" with SelectedValuePath="Type" and DisplayMemberPath="Label". MacroTypeCatalog (PadForge.App/ViewModels/MacroTypeCatalog.cs) carries all 64 MacroActionType values exactly once, in display order, across eleven category shelves: Virtual Buttons, Virtual Axes & Wheel, Keyboard & Text, Mouse, Timing & Flow, Rumble, Lightbar & LEDs, Sound & Volume, Motion & Pointer, Layers & Overlays, System & Apps. MacroTypeCatalog.View is a ListCollectionView grouped on Category, and the ComboBox draws each shelf through a GroupStyle.HeaderTemplate, the arrangement the cross-device input picker uses. Item tooltips ride the ItemContainerStyle. The list is one instance for the process lifetime, refilled in place on Strings.CultureChanged so an x:Static-bound view never strands on a stale list. PadForge.Tests/MacroTypeCatalogTests.cs pins the census: every enum member appears exactly once, Choices.Count equals Enum.GetValues<MacroActionType>().Length, the categories stay eleven contiguous blocks, and the picker still binds MacroTypeCatalog.View.

Macro Trigger Section:

Field Binding Visibility
Fire mode dropdown TriggerMode (SelectedValue, SelectedValuePath="Tag") Always
Trigger source TriggerSource ShowsTriggerComboEditor (hidden in Always and CustomExpression modes)
Trigger inputs TriggerInputItems list, plus RecordingLiveText while IsRecordingTrigger ShowsTriggerComboEditor
Record button RecordTriggerCommand (icon RecordTriggerIcon, tooltip RecordTriggerButtonText) ShowsTriggerComboEditor
Axis threshold TriggerAxisThreshold (Slider 1-100%) HasLegacyAxisTrigger
Axis direction TriggerAxisDirectionIndex (Any/Positive/Negative) HasLegacyAxisTrigger
Consume trigger ConsumeTriggerButtons (CheckBox) ShowsTriggerComboEditor

Action Type Editor Panels. One Visibility branch per shape, each bound to an Is*Type predicate on MacroAction. Some branches cover a family (IsAnyRumbleSetType, IsAnyAxisValueType, IsAnyMouseButtonType). The ones below are the shapes worth naming. Grep Is[A-Za-z]*Type in PadPage.xaml for the full set.

Action Type Visible Panel Key Controls
ButtonPress / ButtonRelease (plus RepeatVcButtonWhileHeld, ToggleVcButton) IsAnyVcButtonType WrapPanel of CheckBox items from ButtonOptions
KeyPress / KeyRelease (plus RepeatKeyWhileHeld, ToggleKey) IsAnyKeyType KeyString TextBox (telemetry mono font), VirtualKeyChoices ComboBox picker, Clear button
ButtonPress / KeyPress / Delay / MouseButtonPress / AxisHold / AxisAdd / AxisScale IsDurationType DurationMs TextBox + "ms" label
AxisSet / AxisHold / AxisSetLatched / AxisScale (plus ToggleVcAxis, RepeatVcAxisWhileHeld, AxisAdd) IsAnyAxisValueType Axis target ComboBox (None, LStickX/Y, RStickX/Y, LT, RT, then the ten button pressure targets, shown by the action's CarriesPressure and labeled from MacroPressureLabels.Instance, #530) + AxisValuePercent TextBox. Release Axis Latches' picker (IsAxisLatchReleaseType) carries the same ten
SystemVolume IsSystemVolumeType Axis source (Output/Input), axis selector, device picker, volume limit slider, invert toggle, OSD toggle
AppVolume IsAppVolumeType Process ComboBox (editable, refreshes on dropdown), axis source, device picker, volume limit, invert toggle
MouseMove / MouseScroll IsMouseMoveType Axis source (Output/Input), axis selector, device picker, sensitivity slider
MouseButtonPress / MouseButtonRelease (plus ToggleMouseButton) IsAnyMouseButtonType Mouse button ComboBox (Left/Right/Middle/X1/X2)
DisconnectController IsDisconnectControllerType Target-mode ComboBox (DisconnectTarget: Triggering Device / Specific Device / Slot Devices / All Devices) + specific-device picker ComboBox (DisconnectDeviceOptions, gated on IsDisconnectSpecificDevice)
SwitchLayer IsSwitchLayerType A CardBorder with MacroAction_Type_SwitchLayer as its title, the Macro_SwitchLayer_Hint line, and a 240px ComboBox over the slot's own LayerTabs (SelectedValuePath="LayerMask", DisplayMemberPath="LayerName") writing SwitchLayerMask. Choices are Base plus every authored layer, the same name/mask pairs the layer-scope picker uses (#377)
TriggerEffect IsTriggerEffectType A CardBorder titled MacroAction_Type_TriggerEffect (#531): Trigger ComboBox (TriggerEffectSide: Right / Left / Both), Effect ComboBox over TriggerEffectCatalog.Kinds (TriggerEffectKind), one Slider + TextBox row per parameter (TriggerEffectRows), the invalid-values line while TriggerEffectIsValid is false, Holds ComboBox (TriggerEffectHold), the Time Slider + TextBox in ms while IsTriggerEffectTimed, each with a reset button, and the MacroAction_TriggerEffect_Hint line
TriggerEffectStop IsTriggerEffectStopType A CardBorder titled MacroAction_Type_TriggerEffectStop with the Trigger ComboBox alone (#531)

Device Axis Picker (shared by SystemVolume, AppVolume, MouseMove): - Device ComboBox: DropDownOpened populates from devices assigned to current slot. - Axis index ComboBox: DropDownOpened populates with IsAxis DeviceObjects from selected device. - Uses AxisPickerItem wrapper with InputIndex and localized DisplayName.

Mappings Tab (Tab 2). Detailed

Grid (4 rows, x:Name="MappingDataGrid" at Row 3)
├─ Row 0 (Auto): Toolbar
│   ├─ "Copy" Button (CopySettingsCommand)
│   ├─ "Paste" Button (PasteSettingsCommand)
│   ├─ "Copy From" Button (CopyFromCommand)
│   ├─ "Map All" Button (MapAllButtonText, Click=MapAllToggle_Click)
│   ├─ "Add Layer" Button (AddShiftLayerButton, Click=AddShiftLayer_Click)
│   ├─ MappingFilterSearchBox (ui:TextBox, MappingInputSearch, find-as-you-type)
│   ├─ MappingDeviceFilterToggle (Filter24 funnel) + Popup (PickerDeviceFilterEntries checkboxes)
│   ├─ MappingSurfaceScopeBox (All / Mouse / Keyboard, session-only, shown on a
│   │   Keyboard + Mouse slot driving both halves: KbmSurfaceScopeVisible, #408)
│   ├─ Hint text (italic, secondary brush)
│   └─ "Clear All" Button (far end, EmberDestructiveButton, Click=ClearAllMappings_Click → ConfirmDialog)
├─ Row 1 (Auto): ShiftLayerTabStrip (nested layer tabs bound to LayerTabs, hidden when only Base)
├─ Row 2 (Auto): Pipeline-status chips (CURVE / GYRO / SHIFT / INV / DZ). A click
│   scrolls to the next owning row, and SHIFT cycles the layer tabs
└─ Row 3 (*): DataGrid (HorizontalAlignment=Left, every column Width="Auto")
    ├─ Column: "Output" (TargetLabel text)
    ├─ Column: "Source" (grouped ComboBox dropdown)
    ├─ Column: "Value" (CurrentValueText, mono)
    ├─ Column: Record button (ToggleRecordCommand)
    ├─ Column: Clear button (ClearCommand)
    ├─ Column: "Options" (Invert / Half / Bidirectional, Flip Output, Do Not Inherit,
    │    per-source gyro sensitivity)
    └─ Column: "Axis-to-Button Deadzone" (per-mapping activation threshold, #42)

DataGrid Properties: - AutoGenerateColumns="False", CanUserAddRows="False", CanUserDeleteRows="False", CanUserReorderColumns="False", IsReadOnly="True". - Row style: transparent background, retemplated with a #175 "rowfire" ember underline drawn at the cells' bottom edge. A plain row with no options collapses to a 26px mono compact line and expands to the full editor on click or selection.

Source Column ComboBox: - ItemsSource="{Binding AvailableInputsView}". Grouped ICollectionView over the row's AvailableInputs (per-device groups via ComboBox.GroupStyle). On the Motion Gyro and Motion Accelerometer rows that is the slot's list for the row's sensor (#475). The modifier pickers, in the row's detail strip and on extra-source chips, bind DataContext.ParamInputsView, the slot's full list. The Up and Down pickers beside them bind DataContext.KeyInputsView, the slot's inputs that read as a key. - SelectedItem="{Binding SelectedInput, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}". - DisplayMemberPath="DisplayName".

Options Column: - Invert CheckBox. IsInverted binding. - Half CheckBox. IsHalfAxis binding. - Bidirectional CheckBox (Pad_Either). IsBidirectional binding. - Flip Output CheckBox. InvertOutput binding, shown while IsInvertOutputApplicable. - Do Not Inherit CheckBox. NoInherit binding, shown while the active layer inherits (IsActiveLayerInheriting). - Gyro sensitivity slider (0.1-10). GyroSensitivity binding, shown for gyro sources (IsGyroSource).

Motion row panel (#475): In the row's detail strip, above the Combine row:

  • A caution-colored note (MotionRowNote, shown while ShowMotionRowNote) on any row.
  • On the Motion Pitch, Yaw and Roll rows (IsMotionAxisRow): the Motion Mode ComboBox over MotionResponseOptions (shown while CanUseMotionAngle), then Top Speed and Start Speed (ShowMotionSpeedSettings) or Lean Angle (ShowMotionAngleSettings), then Motion Deadzone. Each setting is a slider, a number box, a unit and a SettingResetButton, laid out like the Ramp panel.

Picker Filter (#322): One search box and one device-visibility popup filter the slot's choice views, the shared list and the two Motion lists (#475), so every picker on the tab reflects them when opened. Ctrl+F focuses the box through MappingsTabRoot_PreviewKeyDown on the tab root.

  • MappingInputSearch is find-as-you-type and session-only, never persisted. Its setter calls ApplyMappingPickerFilter().
  • The search also filters the grid rows as well as the dropdown contents: RowMatchesSearch matches a row's target label or its selected source's display name. Rows are outputs, so the device-visibility set never hides them. Only typed text does.
  • PickerDeviceFilterEntries drives the popup's per-device checkboxes. HiddenPickerDeviceKeys (device guids plus "any" for the device-agnostic group) persists per slot in the settings root, not in profiles.
  • MappingPickerFilterActive is true while either filter narrows the list, so the funnel reads as engaged.

A view filter changes what a dropdown OFFERS, never what a row's binding holds.

Sticks Tab (Tab 3). Detailed

ScrollViewer
  ├─ ItemsControl (ItemsSource=StickConfigs, DataType=StickConfigItem)
  │   └─ per-stick StackPanel:
  │       ├─ Title + "Reset All" button
  │       ├─ Grid (2 columns)
  │       │   ├─ Col 0 (*): Slider controls
  │       │   │   ├─ "Calibrate Center" button (click → StartCalibration)
  │       │   │   ├─ Center Offset X (OffsetSlider, -100 to 100, + digit edit + reset)
  │       │   │   ├─ Center Offset Y (OffsetSlider + digit edit + reset)
  │       │   │   ├─ Deadzone Shape ComboBox (6 shapes)
  │       │   │   ├─ Deadzone X (DzSlider + % edit + digit edit + reset)
  │       │   │   ├─ Deadzone Y (DzSlider + % edit + digit edit + reset)
  │       │   │   ├─ Anti-Deadzone X (DzSlider + % edit + digit edit + reset)
  │       │   │   ├─ Anti-Deadzone Y (DzSlider + % edit + digit edit + reset)
  │       │   │   ├─ Linear (DzSlider + % edit + reset)
  │       │   │   ├─ Sensitivity (0.1-5x multiplier, pointer sticks only: IsPointerStick)
  │       │   │   ├─ Momentum CheckBox + glide slider (mouse stick only: IsMouseStick, #291)
  │       │   │   ├─ "Sensitivity Curves" header + hint text
  │       │   │   ├─ Sensitivity X (preset ComboBox + reset)
  │       │   │   ├─ Sensitivity Y (preset ComboBox + reset)
  │       │   │   ├─ Full-width CurveEditor pair (X and Y, 560px, ChartHeight=150, IsSigned)
  │       │   │   ├─ "Range" header + hint, Calibrate Boundary + Reset Boundary buttons
  │       │   │   ├─ Min Range X/Left (1-100, DzSlider + % edit + digit edit + reset)
  │       │   │   ├─ Max Range X/Right (1-100, DzSlider + % edit + digit edit + reset)
  │       │   │   ├─ Min Range Y/Down (1-100, DzSlider + % edit + digit edit + reset)
  │       │   │   └─ Max Range Y/Up (1-100, DzSlider + % edit + digit edit + reset)
  │       │   └─ Col 1 (Auto): Live preview panel (MinWidth=216)
  │       │       ├─ Stick position preview (212×212 Border)
  │       │       │   ├─ 200×200 Ellipse background
  │       │       │   ├─ Grid lines (crosshair + quadrant dashes)
  │       │       │   ├─ Deadzone overlays (shape-dependent):
  │       │       │   │   ├─ Axial: yellow cross arms + red center rectangle
  │       │       │   │   ├─ Radial/ScaledRadial: red ellipse
  │       │       │   │   ├─ Sloped/SlopedScaled: yellow wedges (SlopedWedgeGeometryConverter)
  │       │       │   │   └─ Hybrid: yellow wedges + red circle center
  │       │       │   ├─ Anti-deadzone ring (thin ember `#80FF6B2C` ellipse at the
  │       │       │   │  output-floor radius, collapsed while both axes sit at 0)
  │       │       │   ├─ Trail + peak-hold canvas (StickTrailBehavior, StickPeakHoldBehavior)
  │       │       │   ├─ Cold-blue raw dot (9px, `#FF58B6E4`, RawPosX/Y, NormToCanvasConverter)
  │       │       │   └─ Ember output dot (11px, `EmberHotBrush`, LiveX/Y)
  │       │       ├─ Legend row (RAW / OUT / DZ chips) + PeakReadout
  │       │       ├─ RAW / OUT instrument rows (InDisplay, RawDisplay, telemetry mono)
  │       │       └─ CurveEditor pair (X-axis + Y-axis, ChartSize=96 each)
  │       │           ├─ CurveEditor X: CurveString=SensitivityCurveX, DeadZone/MaxRange/LiveInputX
  │       │           └─ CurveEditor Y: CurveString=SensitivityCurveY, DeadZone/MaxRange/LiveInputY
  │       └─ Steering section (#94): mode ComboBox (Linear / Winding / Angle X / Angle Y),
  │          winding rows (IsWindingMode), angle deadzone rows (IsAngleMode)
  ├─ LockFeedbackCard: steering lock feedback (rumble, trigger vibration, lightbar,
  │   adaptive-trigger resistance, pulse length, lightbar color and timing)
  └─ FlickStickCard: Flick Stick tuning, Keyboard + Mouse slots only (#225)

Deadzone Shape Options (ComboBox index):

Index Shape Deadzone Overlay
0 Scaled Radial Red ellipse (IsRadialShape)
1 Radial Red ellipse (IsRadialShape)
2 Axial Yellow cross arms + red center rectangle (IsAxialShape)
3 Hybrid Yellow wedges + red circle (IsHybridShape)
4 Sloped Scaled Axial Yellow wedges (HasSlopedWedges)
5 Sloped Axial Yellow wedges (HasSlopedWedges)

Per-Slider Row Pattern: Each parameter row follows this layout:

[DzLabel 270px] [Slider 200px] [TextBox 56px] [%] [DigitEdit 56px] [ResetButton]
Slider and TextBox both bind to the same property (e.g., DeadZoneX) with Mode=TwoWay. Digit edit binds to a separate *Digit property for raw axis values. Reset buttons use per-property commands (e.g., ResetDeadZoneXCommand).

Independent Axis Range Sliders: - MaxRangeXNeg / MaxRangeX. Left/Right boundaries for X axis (1-100%). - MaxRangeY / MaxRangeYNeg. Down/Up boundaries for Y axis (1-100%).

Triggers Tab (Tab 4). Detailed

ScrollViewer
  └─ ItemsControl (ItemsSource=TriggerConfigs, DataType=TriggerConfigItem)
      └─ per-trigger StackPanel:
          ├─ Title + "Reset All" button
          └─ Grid (single column) holding one StackPanel
              ├─ Range: RangeSlider (dual-thumb, DeadZone/MaxRange 0-100%)
              │   + two TextBox edits (dz.max) + two digit edits + reset
              ├─ Anti-Deadzone (DzSlider 0-100% + % edit + digit edit + reset)
              ├─ Separator
              ├─ "Sensitivity Curve" header + hint text
              ├─ Preset ComboBox (120px, `CurvePresetChoices`, the instance accessor
              │   over the static `CurvePresetNames`) + reset
              ├─ CurveEditor (full width, IsFullWidth=True, ChartHeight=150, IsSigned=False)
              │   └─ CurveString=SensitivityCurve, DeadZone/MaxRange/LiveInput bindings
              └─ Live instrument panel (inset Border, Grid)
                  ├─ RAW bar (ProgressBar 0-1, RawNorm, InstrumentBarRaw) + RawDisplay text
                  ├─ OUT bar (ProgressBar 0-1, LiveValue, InstrumentBarOut) + OutDisplay text
                  └─ TriggerTravelArc (RawValue=RawNorm, OutValue=LiveValue, spans both rows)

RangeSlider. Dual-thumb control for deadzone floor and max range ceiling: - LowerValue="{Binding DeadZone}". Deadzone threshold. - UpperValue="{Binding MaxRange}". Max range ceiling. - The range between thumbs represents the active trigger zone.

Force Feedback Tab (Tab 5). Detailed

ScrollViewer
  └─ StackPanel
      ├─ "Force Feedback" page header (E877) + subtitle
      ├─ PeripheralHapticsRoute (ember TextBlock, collapsed unless the selected device is a haptic
      │   mouse, keyboard or the Razer Sensa row, #494): where its rumble goes and whether that
      │   path answers, from PeripheralRouteText.Haptics
      ├─ FalconCard (collapsed unless the selected device is a Novint Falcon, #521): header +
      │   Reset All (ResetFalconAllCommand), Feel ComboBox (FalconFeel), the spring
      │   (FalconSpring, Joystick only), Kick Button (FalconKickButton), kick and shake
      │   strength (FalconKick / FalconShake), Brace and Preload buttons (FalconBraceButton /
      │   FalconPreloadButton, Aim only), gravity compensation (FalconGravity) and the force
      │   limit in newtons (FalconForceCap), each with its own reset
      ├─ "Force Feedback / Rumble / Haptics" card header + "Reset All" button
      ├─ Overall Gain slider (0-100%, ForceOverallGain)
      ├─ Left Motor Strength slider (0-100%, LeftMotorStrength)
      ├─ Right Motor Strength slider (0-100%, RightMotorStrength)
      ├─ "Fold Trigger Rumble into Main Motors" CheckBox (FfbTriggerFoldChk, TriggerRumbleFold)
      ├─ FfbSwapMotorsRow: "Swap Left and Right Motors" CheckBox (SwapMotors), collapsed for a
      │   haptic mouse, keyboard or the Razer Sensa row
      ├─ "Test Rumble" Button (TestRumbleCommand)
      ├─ "Motor Activity" header
      ├─ Left Motor bars (ProgressBar 0-1): RawLeftMotorDisplay (the game's request,
      │   with haptics played as audio on a DualSense persona, #522)
      │   over DeviceLeftMotorDisplay (the selected device's output after its settings)
      ├─ Right Motor bars: RawRightMotorDisplay over DeviceRightMotorDisplay
      ├─ "Steering Angle Rumble" card (SteeringAngleRumbleEnabled, axis LX / LY / RX / RY,
      │   motor Split by Direction / Left Motor / Right Motor (#510), strength 0-100,
      │   deadzone 0-25, each with a reset, plus Reset All)
      ├─ "Constant Force" card (F0AD icon + "Reset All" ResetConstantForceCommand + description)
      │   ├─ "Apply Constant Force" CheckBox (ConstantForceEnabled)
      │   ├─ Drag pad (ConstantForcePadBorder, mouse handlers, SignedNormToCanvasConverter)
      │   └─ X / Y sliders + F2 edits + per-axis reset (ConstantForceX / ConstantForceY)
      ├─ Audio Bass Rumble section
      │   ├─ "Audio Rumble" header (E767 icon) + "Reset All" button + description
      │   ├─ Enable CheckBox (AudioRumbleEnabled)
      │   ├─ Sensitivity slider (1-20, AudioRumbleSensitivity, format F1)
      │   ├─ Bass Cutoff slider (20-200 Hz, AudioRumbleCutoffHz, format F0)
      │   ├─ Left Motor slider (0-100%, AudioRumbleLeftMotor)
      │   ├─ Right Motor slider (0-100%, AudioRumbleRightMotor)
      │   └─ Level meter (ProgressBar 0-1, AudioRumbleLevelMeter)
      └─ "Trigger Routing" card (E72A icon + "Reset All" ResetTriggerRouteCardCommand + description)
          ├─ Left: source ComboBox, mode ComboBox, scale slider, activator display +
          │   record button (LeftTriggerRouteActivatorRecordCommand, EmberIconButtonHot)
          │   and activator-mode ComboBox, each with its own reset
          └─ Right: the same five rows on the RightTriggerRoute* bindings

All Audio Rumble controls bind IsEnabled="{Binding AudioRumbleEnabled}". Grayed out when off.

PeripheralHapticsRoute takes its sentence from the Pad_ForceFeedback_Route* strings: the HID++ unit the rumble plays on, haptic feedback turned off for it in Logi Options+, a device that has not answered yet, the iFeel mouse the rumble plays on as a buzz, the SteelSeries GG tactile handler, the virtual controller whose Rival sets every tactile Rival's rumble, the Razer Sensa path, or the software that is not answering yet. A Razer Sensa row on the ARM64 build reads Common_NotAvailableOnArm64, since that build cannot load the Interhaptics engine. Every other line is empty while the engine is stopped (PeripheralOutputs.Running) and on a slot that is not live (PeripheralOutputs.SlotLive). The ARM64 line shows either way.

Lighting Tab (Tab 7). Peripheral Rows (#494)

A mouse, keyboard or vendor lighting row assigned to the slot gets the Lighting tab a DualSense gets, with the same modes. SyncTabVisibility() sets hasPeripheralLighting when the selected device has no lightbar and a lighting path links it (PeripheralOutputs.HasLighting) or its record says it has one (UserDevice.HasPeripheralLighting), so the tab stays up while the device sleeps.

Three ember lines name a DSX game mod while it holds something on the slot (#529), each collapsed when empty: GameModTriggersLine under the Adaptive Triggers tab's subtitle, GameModImpulseLine under the Impulse Triggers tab's subtitle, and GameModLightsLine in the Lighting tree below.

StackPanel
  ├─ "Lighting" page header (E781)
  ├─ LightingLightbarSubtitle + LightingPlayerIdleHint (lightbar devices only)
  ├─ GameModLightsLine (#529, ember, collapsed when empty)
  ├─ PeripheralLightingPanel (collapsed unless hasPeripheralLighting)
  │   ├─ Pad_Lighting_PeripheralSubtitle
  │   ├─ PeripheralLightingControlRow (collapsed for a vendor row or a unit row, which lighting is all it carries)
  │   │   ├─ CheckBox "Control This Device’s Lighting" (DeviceConfig.PeripheralLightingEnabled,
  │   │   │    TwoWay, ToolTip Pad_Lighting_ControlDevice_Tooltip)
  │   │   └─ SettingResetButton (DeviceConfig.ResetPeripheralLightingEnabledCommand,
  │   │        AutomationId Reset_DeviceSlotConfig_PeripheralLightingEnabled)
  │   ├─ PeripheralClaimLevelRow (collapsed unless the device offers a Lighting Hold)
  │   │   ├─ TextBlock "Lighting Hold" (Pad_Lighting_ClaimLevel)
  │   │   ├─ ComboBox PeripheralClaimLevelPicker (DeviceConfig.PeripheralClaimLevel, TwoWay through
  │   │   │    EnumIndexConverter, AutomationId PeripheralClaimLevelCombo, IsTextSearchEnabled off):
  │   │   │    "Colors Only", "Colors and Lighting Power",
  │   │   │    "Colors, Lighting Power, and On-Board Memory Mode Off" (PeripheralClaimLevelHostModeItem)
  │   │   └─ SettingResetButton (DeviceConfig.ResetPeripheralClaimLevelCommand,
  │   │        AutomationId Reset_DeviceSlotConfig_PeripheralClaimLevel)
  │   └─ PeripheralLightingRoute (ember TextBlock, collapsed when empty)
  ├─ LightbarModeCard (the lightbar's mode picker and cards. Its LightbarPreviewPanel,
  │    the DualShock 4 / DualSense art, collapses for these rows)
  └─ IndicatorLedsCard, GuideLedCard (collapsed for these rows)

The Lighting Hold row shows for a mouse or keyboard that offers a hold above colors alone (PadPage.OfferedLightingHold: the row's DeviceLinks.MaxClaimLevel while a path of its own lights it, else what its record kept). PadPage.ShowLightingHold collapses and disables the last option for a device that does not offer the second hold. The option stays in the list, so a choice of it saved earlier still shows as the selection, and a closed ComboBox cannot step onto it with an arrow key, since it steps only to an enabled item. The picker then takes the tooltip Pad_Lighting_ClaimLevel_TooltipNoHostMode, which stops before the sentences about the last option, and the two options left report a set size of two to a screen reader. Typing a letter selects nothing in this picker, because it would select the last option where that one is out of the list.

PeripheralLightingRoute is PeripheralRouteText.Lighting(device, PadIndex, lightsHere, claimLevel) (PeripheralRouteText.cs line 155), where lightsHere is true for a vendor row or a unit row and otherwise follows the switch, in both cases only on a slot that is live (PeripheralOutputs.SlotLive), and claimLevel is the Lighting Hold this controller's claim carries. The line is empty while the engine is stopped (PeripheralOutputs.Running). Its sentence comes from the Pad_Lighting_Route* strings:

Key When
RouteDirect / RouteDirectPower / RouteDirectHostMode This controller lights a device on its own, through HID++, a Razer or SteelSeries control collection, or HID LampArray, with the device named in the line. The second and third forms add what a Lighting Hold above Colors Only holds: the device does not dim its lights by itself, or its on-board memory mode is off, while it is lit
RouteThrough / RouteThroughOrDirect This controller lights the device through the vendor's software, named in the line. The second form adds that PadForge lights the device on its own while that software is not running
RouteWindowsLighting / RouteWindowsLightingBuiltIn Windows Dynamic Lighting lights the device, so PadForge leaves it alone. The line names the switch in Windows Settings, and the second form, for a laptop's own keyboard, what turning it off costs on some laptops
RouteAsleep A device the record says has lighting has not answered yet
RouteWaiting The vendor's software is not answering yet
RouteCannotLight The Logitech LED engine on this PC cannot paint this device type, or will not load while its program runs (in the ARM64 build an engine that will not load reads Common_NotAvailableOnArm64). While the engine will not load, the LIGHTSYNC row reads it on every virtual controller it is on, one that is gone included, ahead of RouteOtherControllerOnly
RouteShared Another device of this kind, on the virtual controller the line names, sets the shared path's color
RouteSharedVendorRow A vendor row, on the virtual controller the line names, sets the shared path's color
RouteOtherController / RouteOtherControllerOnly The same device on another virtual controller sets its color, or, for a device lit on its own, another row of it there does. The Only form shows while this controller's switch is off or its slot is not live. A vendor row on a slot that is not live reads the Only form with the number of the controller that lights it elsewhere, and nothing while its software is not answering
RouteOtherRow / RouteOtherRowOnly Another row of the same directly lit device on this controller holds the unit
RouteVendorRow / RouteVendorRowWaiting A vendor row's own line on a live slot: what its software lights that no assigned device claims, with examples (Pad_Lighting_VendorRowExamples_*), or that the software is not answering yet

With the switch off, or on a slot that is not live, the line names only who else lights the device, never a route of its own. SyncTabVisibility() runs again on PeripheralOutputs changes (OnPeripheralOutputsChanged), when the DeviceConfig anchor swaps to another device's config, and when PeripheralLightingEnabled changes, so the line follows the switch even for a device that is offline.

Gyro Tab (Tab 8). Detailed

The tab opens on the E7AD page header (Pad_Gyro_Header + Pad_Gyro_Subtitle), then ten CardBorder cards in this order:

Order Card x:Name Glyph Shown when
1 Grip (#392) (unnamed) E815 always
2 Pitch and Roll Simulation (#474) GyroSimulationCard E7AD DualShock 3 with gyro and accelerometer
3 Motion Passthrough GyroPassthroughCard E72A gyro rate
4 Motion Steering (#94) (unnamed) inline Path always
5 Tilt (unnamed) E99A always
6 Calibration GyroCalibrationCard F272 gyro rate
7 Sensitivity GyroSensitivityCard E9E9 gyro rate
8 Compass Yaw (#271) CompassYawCard E707 Switch 2 magnetometer
9 Response GyroResponseCard F1CB gyro rate
10 Engage (#120) GyroEngageCard E7E8 gyro rate

The tab raises for any motion sensor, so an accelerometer-only remote reaches Grip, Tilt, and Motion Steering. SyncTabVisibility() keeps two flags apart for that: hasGyro is ud.HasGyro || ud.HasAccel and drives TabGyro.Visibility, while hasGyroRate is ud.HasGyro alone and collapses the five rate cards named above. Before #392 the tab itself was gyro-gated, so an accelerometer-only Wii Remote never saw Tilt.

Grip card. One row: a MotionGripOptions ComboBox on MotionGrip (SelectedValuePath="Value", DisplayMemberPath="Display", 260px) plus ResetMotionGripCommand. The four holds are Pointing (the default), Sideways, WiiWheel, and Upright, stored as those canonical English identifiers. The grip rotates gyro, accelerometer, and gravity into the game's frame, and the D-pad follows it.

Engage card (#120). Below the Easy-Aim stick threshold, two ComboBoxes gate gyro engagement per stick and per direction. Both use SelectedValuePath="Tag" and reset buttons.

Control Binding Items (Tag)
Engage Stick GyroEngageStickSide Right / Left / Either
Engage Direction GyroEngageStickDirection Full / X / Y / XNeg / XPos / YNeg / YPos

Reset buttons run ResetGyroEngageStickSideCommand (to Right) and ResetGyroEngageStickDirectionCommand (to Full).

Mappings Tab. Per-Source Motion Sensitivity

Two per-source sensitivity rows render inside the mapping source editor, each gated on the selected source:

  • IR Pointer X/Y (#146): visible on IsIrPointerSource. Slider + TextBox bound to IrPointerSensitivity, reset via ResetIrPointerSensitivityCommand.
  • Mouse Motion X/Y (#154): visible on IsMouseMotionSource. Same IrPointerSensitivity binding with its own tooltip string.

Pointer Tab (Tab 13). IR Camera Tuning (#146)

Wii Remote IR camera tuning. TabPointer (Tag 13) is Visibility="Collapsed" by default and shown by SyncTabVisibility() only when the selected mapped device is an IR-capable Wii Remote (hasIrPointer). Two cards: Pointer Mode (PointerMode: Mouse, FPS Mouse, 4:3 Border, or 16:9 Border, plus PointerFpsSpeed) and Pointer Tuning (sensor-bar position IrSensorBarPos as Centered / Above / Below, IrSensorBarCompPercent compensation, IrSmoothingPercent smoothing), each row with a reset. The tab's subtitle (Pad_Pointer_Subtitle) sends the player to the Devices page to calibrate the remote as a light gun and says that a calibrated remote ignores the sensor-bar position and offset (#485).

Radial / touch menu editor (#9). Slot tier like Macros: menus live on the slot's MappingSet. Tag 15 is the appended TabItem's index.

Grid (3 columns)
├─ Col 0 (250px): Menu list panel
│   ├─ WrapPanel.Top: Add / Remove / Duplicate buttons
│   │   (AutomationIds "MenuAddButton" / "MenuRemoveButton")
│   └─ ListBox (Menus, SelectedItem=SelectedMenu, DisplayMemberPath="Name",
│      EmberSelectListItem container style)
├─ Col 1 (Auto): GridSplitter (4px, draggable)
└─ Col 2 (*): Empty state OR menu editor
    ├─ Empty state (visible while !HasSelectedMenu): outline flame Path
    │   + Menu_EmptyHint cold guidance text, same shape as Macros
    └─ ScrollViewer (DataContext=SelectedMenu, gated on HasSelectedMenu)
        └─ StackPanel
            ├─ Menu Name TextBox (UpdateSourceTrigger=PropertyChanged) + Enabled CheckBox
            ├─ Style ComboBox (KindOptions → KindIndex: Radial Ring / Touch Grid)
            ├─ Show on Web Controller CheckBox (ShowOnWebController, visible on
            │   ShowWebControllerRow: a Touch Grid, or the flag set, #471)
            │   + SettingResetButton
            ├─ Layer ComboBox (LayerChoices → LayerMask) + Reset (ResetLayerCommand)
            ├─ Layer-hold CheckBox (LayerHoldsOpen, visible on ShowLayerHold, #413)
            ├─ Host Input ComboBox (HostOptions → SelectedHost, label HostInputLabel)
            │   + Record button (MenuHostRecordCommand from the TabItem DataContext,
            │     glyph = HostRecordIcon) + Reset (ResetHostCommand)
            ├─ Host Input caption (HostInputCaption, always shown: the Steer With
            │   caption while the layer hold keeps the menu open, else Menu_HostInput_Caption)
            ├─ Custom X / Custom Y steer-axis rows (visible on IsCustomHost,
            │   ResetCustomXCommand / ResetCustomYCommand)
            ├─ Click Input row (visible on IsCustomHost, ResetClickCommand)
            ├─ Pad Half ComboBox (HostHalfOptions → HostHalfIndex,
            │   visible on HostIsTouchpad)
            ├─ Fire Mode ComboBox (FireOptions → FireTypeIndex) + Reset
            ├─ Cells NumberBox (1-20 → CellCount)
            │   + Center Cell CheckBox (HasCenter, radial only) + Reset
            ├─ Engage Deadzone Slider + NumberBox (1-95 → EngageDeadzonePercent) + Reset
            ├─ Menu Overlay geometry card (CardBorder, header + ResetGeometryCommand)
            │   ├─ Show Labels CheckBox (ShowLabels)
            │   ├─ Screen Position NumberBox pair (0-100 → PosXPercent / PosYPercent)
            │   ├─ Size NumberBox (10-400 → ScalePercent) + "%"
            │   └─ Opacity NumberBox (5-100 → OpacityPercent) + "%"
            ├─ Cell Bindings ItemsControl (Cells)
            │   └─ per cell: Header + icon indicator (IconImage, the emoji itself
            │       on IsGlyphIcon, or the E8B9 picture glyph on ShowIconGlyph) + icon size slider
            │       (IconScalePercent 25-200, visible on HasIcon) + Label TextBox (LostFocus)
            │       + Binding ComboBox (BindingKindOptions → BindingKind)
            │       + key picker (KeyOptions → SelectedKeyVk, visible on ShowKeyPicker)
            │       OR button picker (ButtonOptions → SelectedButtonFlag,
            │          visible on ShowButtonPicker)
            │       OR macro picker (MacroOptions → SelectedMacroName,
            │          visible on ShowMacroPicker, #390)
            │       + Choose Icon button (EB9F, MenuCellChooseIcon_Click, #390: opens
            │         IconPicker through IconPickerHost.Open, #471)
            │       + Reset (ResetCellCommand)
            └─ Icon Packages card (CardBorder, EB9F glyph, #390)
                ├─ Pad_Menus_IconPackages_Header + description
                ├─ Add (IconPackageAdd_Click) / Create (IconPackageCreate_Click) /
                │   Remove (IconPackageRemove_Click) buttons
                ├─ IconPackagesEmptyText (Pad_Menus_IconPackages_Empty)
                └─ IconPackagesList ListBox (MaxHeight 170, EmberSelectListItem,
                    per row: EB9F glyph + Name + Path)

Every setting row carries the canonical Reset button, and the host row carries Record too (the Aim Engage cluster shape). The record button binds MenuHostRecordCommand on the PadViewModel through the TabItem's DataContext because the editor panel's own DataContext is the selected MenuEditorItem.

The four cell pickers each use ComboBoxWidthBehavior.SizeToItems with a per-column WidthGroup (MenuCellKindCombos, MenuCellKeyCombos, MenuCellButtonCombos, MenuCellMacroCombos), so rows stay in column across selections and long locales do not clip.

Icon Packages block (#390). Takes the Sound Packages card's shape. A pack is one zip file with the .pficons extension holding image entries plus an optional manifest.json display name (PadForge.App/Common/IconPackageManager.cs). PadForge never extracts a pack: importing one registers its path, and cells reference entries as pficon://PackName/entry.png beside loose image paths and Steam binding-icon names. The list and its empty-state text are filled from code-behind (RefreshIconPackages sets IconPackagesList.ItemsSource from IconPackageManager.Packages), and IconPackageManager.RegistryChanged re-runs it.

HIDMaestro Profile Bar

HMaestroProfileBar is a ChipGhost preset chip inline in tier 1, shown for Xbox, PlayStation, Nintendo, and KeyboardMouse slots. SyncExtendedConfigBar() sets its visibility to (HasHMaestroProfileBar && !isExtended) || isKbm. HasHMaestroProfileBar is true for Xbox, PlayStation, Nintendo, and Extended, but Extended slots hide this compact chip and use the separate ExtendedConfigBar (with its own ExtendedProfileCombo) instead. The chip carries two pickers and shows one of them:

Control AutomationId Binding
Profile ComboBox HMaestroProfileCombo ProfileId, items from AvailableProfiles (HMaestro profile catalog). Hidden on a Keyboard + Mouse slot
Surfaces ComboBox KbmSurfacesCombo KbmSurfaces, items from KbmConfig.AvailableKbmSurfaces (#408). Shown only on a Keyboard + Mouse slot

The profile drives identity (VID/PID/product string) and layout (axes/buttons/POVs/touchpad/rumble) for the HM virtual. Each picker has a SettingResetButton beside it.

Extended Config Bar

ExtendedConfigBar is shown when OutputType == Extended. Stacked rows:

Row Controls Notes
1 ExtendedProfileCombo + ExtendedImportBtn Profile picker (same AvailableProfiles source) plus Import-From-Device
2 ExtendedCustomizeChk + ExtendedResetDefaultsBtn Master toggle for the rows below. Reset reverts to catalog defaults
3 ExtendedProductStringBox, ExtendedOemOverrideChk, ExtendedVidBox, ExtendedPidBox Identity overrides
4 RawStickCountBox, ExtendedTriggerCountBox, RawPovCountBox, RawButtonCountBox, ExtendedForceFeedbackChk Layout overrides. The count boxes kept their v2 Raw* names except triggers

Override rows 3 and 4 bind IsEnabled to ExtendedCustomizeChk.IsChecked, so they stay disabled until Customize is on, and toggling Customize off restores the catalog profile as-is. Each field has a reset button (ResetExtendedSetting_Click, PadPage.SettingResets.cs). _syncingExtendedConfig guard prevents recursive updates inside SyncExtendedConfigBar().

MIDI Config Bar

Visible when OutputType == Midi. Centered horizontal StackPanel:

Control Binding Range
Channel TextBox MidiConfig.Channel 1-16
CC Count TextBox MidiConfig.CcCount 0 to 128 − StartCc (clamped against StartCc)
Start CC TextBox MidiConfig.StartCc 0-127
Note Count TextBox MidiConfig.NoteCount 0 to 128 − StartNote (clamped against StartNote)
Start Note TextBox MidiConfig.StartNote 0-127
Velocity TextBox MidiConfig.Velocity 1-127
Output Port ComboBox (MidiOutputPortBox) MidiConfig.OutputPort, set in code None, the output ports, then the saved port when it is not connected

All fields have tooltips. _syncingMidiConfig guard prevents recursive updates. When CC/Note counts or start numbers change, vm.RebuildMappings() regenerates mapping rows. A running slot's controller takes every change on its next poll, on the same port (MidiVirtualController.ApplyLayout).

The Output Port group shows only while PadViewModel.IsLegacyMidi is true, since the Windows MIDI Services APIs create the slot's own port. SyncMidiOutputPortBox fills it: None first, then every output port by the name it is saved under, then the saved port when it is not connected now, so the pick stays visible. A second port with the same name is listed as "Name (2)". The port list is read only when the picker opens, on a worker that gives up after 2 s, because WinMM on the new MIDI stack asks the MIDI service, which can hang. _syncingMidiPort keeps the refill from writing back a pick. Its reset button picks None.

Copy From Dialog

Opens CopyFromDialog. Picking a source slot copies the whole mapping table plus every assigned device's tuning (deadzones, sensitivity, FFB, impulse triggers, adaptive triggers, lighting, gyro, TouchpadSettings) into the target slot. Each source device matches a target device by InstanceGuid first, then ProductGuid as a fallback for the same controller model on a different physical unit. A target device with no source-side match keeps its tuning. The copied MappingSet also carries the slot's menus, Bass Shakers, SOCD, and Keep Awake settings, and CopySlotConfigsAcrossSlots clones the per-device lighting configs, plus the Extended layout, MIDI layout, or Keyboard + Mouse config when both slots share that type. In that clone, a target device the source slot does not have takes a stand-in source device's config and keeps its own Control This Device’s Lighting switch (#494).


DevicesPage

Files: DevicesPage.xaml, DevicesPage.xaml.cs

All detected input devices with raw input state visualization.

Layout Structure

Grid (Margin="24,16")
├─ Row 0 (Auto): Header
│   ├─ Icon (E772) + Title
│   ├─ Refresh Button (E72C, RefreshCommand)
│   ├─ Pair Button (E702, PairCommand → PairDeviceDialog)
│   └─ Online/Total count display (telemetry mono)
└─ Row 1 (*): Main content (Grid, 2 columns)
    ├─ Col 0 (*): Facet chips + device card ListBox + drag-assign hint
    │   ├─ Row 0: Type facet chips (ALL / GAMEPAD / JOYSTICK / WHEEL / KEYBOARD /
    │   │    MOUSE / OTHER, ClickBorder radio chips, Click="FacetChip_Click", count per chip)
    │   ├─ Row 1: ListBoxItem with custom ControlTemplate (4px accent left bar on selection)
    │   │   └─ Card Border (CornerRadius=8, Padding="12,10")
    │   │       ├─ Row 0, Col 0: liveness flame Path (LivenessFlameBase) + DeviceName (SemiBold, 13px)
    │   │       ├─ Row 0-1, Col 1: Slot badges (WrapPanel of numbered badges).
    │   │       │    No badges means unassigned. There is no fallback pill
    │   │       ├─ Row 0-1, Col 2: Remove device Button (E711 × icon)
    │   │       └─ Row 1: DeviceType + VID:PID (HasVidPid) + CapabilitiesSummary + battery (HasBattery)
    │   └─ Row 2: Devices_DragAssignHint text
    └─ Col 1 (340px): Detail panel (Border with ScrollViewer)
        ├─ DeviceName headline (CardTitle, wrapping)
        ├─ Device Dossier (#175 competitor item 7): eyebrow + copy Button
        │   (CopyDossier_Click, E8C8) over a recessed telemetry-mono card whose
        │   token rows run PRODUCT / TYPE / CAPS / APP GUID / SDL GUID /
        │   PATH (HidHideInstancePath) / PATH (DossierConnectionPath) / VID:PID /
        │   LINK / SERIAL / BATT. Rows whose fact is absent collapse. LINK reads
        │   "BT" and shows only on IsBluetoothLink
        ├─ Capability chip strip (HasCapabilityIcons): rumble (E877, doubles as the
        │   identify button via IdentifyChip_Click, #293), gyro (E7AD), touchpad (EFA5)
        ├─ Submit Mapping Button (joysticks only, opens GitHub issue template)
        ├─ Register / Manage NFC Tags Button (RegisterNfcTag_Click, ShowRegisterNfcTag, #150)
        ├─ Manage Voice Macros Button (ManageVoicePhrases_Click, ShowManageVoicePhrases, #317)
        ├─ Learn Handheld Buttons Button (LearnHandheldButton_Click,
        │   ShowLearnHandheldButton, #343)
        ├─ Switch Driver Buttons (DriverBind_Click / DriverRestore_Click,
        │   ShowDriverBind / ShowDriverRestore, SDL#33 Part 15)
        ├─ Read as iCade Controller / Read as Keyboard Buttons (ReadAsICade_Click /
        │   ReadAsKeyboard_Click, ShowReadAsICade / ShowReadAsKeyboard, SDL#33 Part 16)
        ├─ Read as Tekken Sticks / Read as Taiko Drums Buttons (UsioTekken_Click /
        │   UsioTaiko_Click, ShowUsioTekken / ShowUsioTaiko, SDL#33 Part 14)
        ├─ Bliss-Box line and actions (BlissBoxLine, BlissBoxPlayer_Click /
        │   DreamcastScreen_Click / PakBackup_Click / PakRestore_Click, Read Arrows
        │   One by One CheckBox with NativeArrows_Click, shown on ShowBlissBoxPlayer /
        │   ShowDreamcastScreen / ShowControllerPak / ShowNativeArrows, #469)
        ├─ Keyboard screen line and button (KeyboardScreenLine, and KeyboardScreen_Click on a
        │   button that reads KeyboardScreenButtonText, shown on ShowKeyboardScreen, #519)
        ├─ Falcon line, Calibrate Reach and its result (FalconLine, FalconCalibrate_Click,
        │   FalconCalibrationStatus as a polite live region, shown on ShowFalconCalibrate, #521)
        ├─ GameModLine and Clear Mod Effects (ClearModEffects_Click), both collapsed while
        │   GameModLine is empty (#529)
        ├─ AnalogKeyboardLine (#468, collapsed when empty)
        ├─ HandheldDaemonWarning line (HasHandheldDaemonWarning, #343)
        ├─ FlydigiServiceWarning line (HasFlydigiServiceWarning, #395)
        ├─ HeadTrackerStatus line (#355, collapsed when empty)
        ├─ GameTelemetryStatus line (#531, a polite live region, collapsed when empty)
        ├─ Separator
        ├─ VC Assignment section
        │   ├─ WrapPanel of ToggleButtons (ActiveSlotItems, ToggleSlotCommand)
        │   └─ Devices_NoSlotsHint line while no slot exists
        ├─ Input Mode section (ShowInputModeSection)
        │   └─ "Force raw joystick mode" CheckBox (ForceRawJoystickMode)
        ├─ Input Hiding section (ShowInputHidingSection)
        │   ├─ "Hide from Games (HidHide)" CheckBox (HidHideEnabled, HidingToggle_Click,
        │   │   shown on ShowHidHideToggle, enabled on IsHidHideAvailable)
        │   ├─ TabletInputStatus line (ShowTabletCaptureStatus)
        │   └─ "Consume Mapped Inputs (Hooks)" CheckBox (ConsumeInputEnabled, ShowConsumeToggle)
        ├─ Separator (ShowInputModeOrHidingSection)
        ├─ Power section (ShowPowerSection)
        │   ├─ Idle Disconnect minutes TextBox (IdleDisconnectMinutes,
        │   │   ShowIdleDisconnect, #162)
        │   └─ Quick Charge CheckBox (QuickChargeEnabled, ShowQuickCharge,
        │       QuickCharge_Click, #372)
        ├─ Light Gun section (ShowGunCalibration, SDL#33 Part 9, Wii Remote #485)
        │   ├─ Separator (ShowGunCalibrationDivider)
        │   ├─ GunCalibrationStatus line
        │   └─ Calibrate Button (GunCalibrate_Click, CanCalibrateGun,
        │       ToolTip GunCalibrateTooltip) + reset
        │       (GunCalibration)
        ├─ Separator (ShowRawInputDivider)
        └─ Raw Input State section
            ├─ Falcon handle (#521, IsFalconDevice): a dot in a square (FalconPreviewBorder,
            │   FalconDot) for Left/Right and Up/Down, beside the named axes
            ├─ Axes (joysticks/gamepads, hidden for keyboard/mouse)
            │   └─ ItemsControl → ProgressBar per axis (0-1, name + bar + raw value)
            ├─ Buttons (joysticks/gamepads. Hidden for keyboard, mouse, MIDI, NFC,
            │   microphone, consumer-control, motion-only, and head-tracker rows,
            │   and while ShowNamedButtons)
            │   └─ WrapPanel of 24×24 circles, accent fill when pressed
            ├─ NFC named-tag preview (#150, IsNfcDevice)
            │   └─ ItemsControl → NfcTags (registered named tags)
            ├─ Consumer Control named-chip preview (#168, IsConsumerDevice)
            │   └─ ItemsControl → ConsumerButtons (named button chips)
            ├─ Keyboard layout (keyboard devices only)
            │   └─ Viewbox → Canvas (556×136) with positioned key Borders
            ├─ Mouse preview (mouse devices only)
            │   └─ Viewbox → MousePreviewControl
            ├─ MIDI preview (MIDI input devices)
            │   └─ MidiPreviewView ×2 (MidiNotesPreview, MidiCcPreview)
            ├─ D-Pad / POV hats (conditional on RawPovs.Count > 0)
            │   └─ WrapPanel of compass indicators, three to a row in the 340 pane
            │       ├─ 36×36 Ellipse background + center dot
            │       └─ Accent-colored Line with RotateTransform(AngleDegrees), hidden when IsCentered
            ├─ Gyroscope (HasGyroData, 3-column X/Y/Z grid, telemetry mono F3 format)
            ├─ Accelerometer (HasAccelData, same layout as gyro)
            ├─ Aux Accelerometer (HasAccelAuxData, same layout, the Nunchuk sensor / combined pair's left-half accel, #199)
            ├─ Aux Gyro (HasGyroAuxData, same layout, the combined pair's left Joy-Con, #252)
            ├─ Touchpad preview (HasTouchpadData, up to 5 contact dots per pad,
            │   TouchpadPreviewBorder, plus Touchpad2PreviewBorder on HasSecondTouchpadData)
            ├─ Named button chips (ShowNamedButtons: handheld hidden buttons #343,
            │   VR controllers, Logitech G-Keys rows)
            │   └─ HandheldButtons chips, or the Handheld_NoneLearned line when empty
            └─ Voice phrases (#317, ShowVoicePhrases)
                └─ ItemsControl → VoicePhrases (registered phrase chips)

Key Bindings

Binding ViewModel Description
Devices DevicesViewModel Device list collection
SelectedDevice DevicesViewModel Currently selected device row
HasSelectedDevice DevicesViewModel Controls detail panel visibility
OnlineCount / TotalCount DevicesViewModel Header device counts
RefreshCommand DevicesViewModel Refresh button
ActiveSlotItems DevicesViewModel Slot toggle button items
ToggleSlotCommand DevicesViewModel Toggle device-to-slot assignment
RemoveDeviceCommand DevicesViewModel Remove device from list
SelectedFacet DevicesViewModel Active type facet chip. FacetCountGamepad and friends fill the per-chip counts

Device Card Bindings (DeviceRowViewModel)

Binding Description
IsOnline Drives a liveness-flame Path (fills EmberBrush with a glow when online, LivenessFlameBase outline stroke when offline). The device-name text also dims when offline.
DeviceName Bold device name
SlotBadges Collection of slot assignment badges. Absence of badges encodes unassigned (#175 phase 2 item 9 removed the IsUnassigned flag and its gray fallback pill)
DeviceType Type string
VendorIdHex / ProductIdHex Hex VID:PID
CapabilitiesSummary Devices_CapsSummary_Format, e.g. "6 axes, 11 buttons, 1 POV(s)"
BatteryGlyph / BatteryText Battery indicator (#167): Segoe MDL2 Assets glyph + "78%", hidden when HasBattery is false

Detail Panel Bindings (SelectedDevice)

Binding Description
DeviceName, ProductName, DeviceType Device identity (dossier PRODUCT / TYPE rows)
CapabilitiesSummary Dossier CAPS row
InstanceGuid Dossier APP GUID row
SdlGuid, DossierConnectionPath, SerialNumber Dossier SDL GUID / LINK / SERIAL rows, each collapsed via StringToVisibility when empty
HidHideInstancePath Dossier PATH row (conditional visibility via StringToVisibility)
ShowSubmitMapping Submit mapping button visibility (joysticks only)
ShowInputModeSection Input Mode section visibility (_forceRawJoystickMode \|\| (IsGamepad && !IsInternalVirtual): a row whose flag is on shows the section whatever its type, so it can be turned off)
ShowInputHidingSection Input Hiding section visibility (ShowHidHideToggle \|\| ShowConsumeToggle)
ShowHidHideToggle HidHide row visibility (!IsInternalVirtual && !IsAggregate: a merged row has no HID instance to cloak)
ShowTabletCaptureStatus / TabletInputStatus Tablet capture status line under the HidHide row
HasFlydigiServiceWarning / FlydigiServiceWarning Flydigi Space Station service notice (#395)
ForceRawJoystickMode Force raw toggle
IsHidHideAvailable Enables/disables HidHide checkbox
HidHideEnabled HidHide toggle
ShowConsumeToggle Consume toggle visibility (mouse/keyboard devices)
ConsumeInputEnabled Consume toggle
ShowPowerSection Power section visibility. True when either row below draws
ShowIdleDisconnect / IdleDisconnectMinutes Idle-disconnect row visibility and its countdown minutes (#162)
ShowQuickCharge / QuickChargeEnabled Quick Charge row visibility and toggle (#372). Also true on a Sony record the USB cable rebound to its wired path, which is not a disconnect target and is exactly when the feature fires
ShowInputModeOrHidingSection / ShowRawInputDivider The two conditional separators around the Power section
ShowGunCalibration / ShowGunCalibrationDivider / GunCalibrationStatus / CanCalibrateGun / GunCalibrateTooltip Light Gun section visibility, the rule above it, the status line (a GunCon 2's aim range, or whether a Wii Remote is calibrated), the Calibrate button's enable state, and its tooltip by kind of gun
ShowReadAsICade / ShowReadAsKeyboard The iCade pair of buttons
ShowUsioTekken / ShowUsioTaiko The USIO layout buttons
HasCapabilityIcons / HasRumble / HasGyro / ShowTouchpadCapability Capability chip strip and its three chips
ShowLearnHandheldButton / HandheldDaemonWarning / HasHandheldDaemonWarning Learn Handheld Buttons button and the vendor-daemon notice (#343)
ShowRegisterNfcTag Register/Manage NFC Tags button visibility (#150)
ShowManageVoicePhrases Manage Voice Macros button visibility (#317): a standalone microphone row, or a DualSense / DualSense Edge over Bluetooth where the pad itself carries the phrases. Never for a peer:// path, since recognition runs on the owner
RawAxes Axis ProgressBar items
RawButtons Button circle items
IsKeyboardDevice / IsMouseDevice Switches between button circles, keyboard canvas, or mouse graphic
KeyboardKeys QWERTY keyboard layout items
RawPovs POV compass items
HasGyroData / HasAccelData / HasAccelAuxData / HasGyroAuxData Gyro / accel / aux-accel / aux-gyro section visibility
GyroX/Y/Z / AccelX/Y/Z / AccelAuxX/Y/Z Motion sensor values (aux accel is #199, aux gyro is #252)
HasTouchpadData / HasSecondTouchpadData / TouchpadLabel Touchpad preview visibility for each pad, and the caption

The raw-state rows above bind to the page DataContext (DevicesViewModel), which republishes the selected device's live state. The NFC, Voice, Consumer, and handheld previews do the same: IsNfcDevice / NfcTags, ShowVoicePhrases / VoicePhrases (#317, the voice twin of the NFC tag rows), IsConsumerDevice / ConsumerButtons, and IsHandheldDevice (#343). HeadTrackerStatus and GameTelemetryStatus are page-scoped too. Everything under the dossier and the sections below it, including ShowRegisterNfcTag, ShowQuickCharge, and IdleDisconnectMinutes, is SelectedDevice-scoped and binds through the SelectedDevice. prefix.

Selection Bar

Custom ListBoxItem ControlTemplate: - SelectionBar. 4px Border with accent brush on left edge, CornerRadius="2". - Toggled by IsSelected trigger. - Content offset 6px right to accommodate the bar.

Event Handlers (Code-Behind)

Handler Trigger Action
RemoveDevice_Click Button.Click Selects device, executes RemoveDeviceCommand
FacetChip_Click ClickBorder.Click Sets SelectedFacet from the chip's Tag, filtering the device list
HidingToggle_Click CheckBox.Click Shows the warning flyout when enabling on a keyboard, mouse, or consumer-control device, clears LastRawStateDeviceGuid for rebuild, calls NotifyDeviceHidingChanged
ShowHidingWarningFlyout (internal) WPF UI Flyout with warning icon, message, Proceed/Cancel buttons. Reverts checkbox immediately, re-checks only on Proceed.
SubmitMapping_Click Button.Click Opens browser to GitHub issue template with device info pre-filled
CopyDossier_Click Button.Click Copies the device dossier's token rows to the clipboard
RegisterNfcTag_Click Button.Click Opens RegisterNfcTagDialog for the selected NFC reader (#150)
ManageVoicePhrases_Click Button.Click Opens RegisterVoicePhraseDialog for the selected microphone-carrying device (#317)
LearnHandheldButton_Click Button.Click Opens LearnHandheldButtonDialog for the machine's hidden buttons (#343)
IdentifyChip_Click ClickBorder.Click Forwards to IdentifyDevice_Click, which calls InputService.IdentifyDevice(SelectedDevice.InstanceGuid) to vibrate the pad (#293)
QuickCharge_Click CheckBox.Click Persists the Quick Charge toggle. Click, not Checked, so the binding-driven refresh on a selection change never writes (#372)
IdleDisconnect_LostFocus TextBox.LostFocus Applies the clamped idle-disconnect minutes (#162)
DeviceCard_MouseDown PreviewMouseLeftButtonDown Records drag start position. Skips if inside a Button
DeviceCard_MouseMove PreviewMouseMove Initiates DragDrop.DoDragDrop with DeviceInstanceGuid data when threshold exceeded
DeviceCard_MouseUp PreviewMouseLeftButtonUp Resets drag state

Device Drag to Sidebar

Device cards support drag via mouse events. Drag data is a DataObject with key "DeviceInstanceGuid" and value device.InstanceGuid. Drop on a sidebar controller card assigns the device to that slot.


KBMPreviewView

Files: KBMPreviewView.xaml, KBMPreviewView.xaml.cs

Keyboard and mouse preview for Keyboard+Mouse virtual controller slots, shown on the PadPage Preview tab.

Layout

Two horizontal Canvas areas:

  • KeyboardCanvas. QWERTY layout built from KeyboardKeyItem.BuildLayout(). Each key is a Border + TextBlock, absolutely positioned. Keys highlight with accent color when pressed in the output state.
  • MouseCanvas. Stylized mouse graphic: contoured LMB/RMB paths, scroll wheel pill, movement circle with deflection dot, scroll arrows, and X1/X2 side buttons.

Interaction

All elements are clickable for click-to-record. Fires ControllerElementRecordRequested with the target name (e.g., KbmKey41, KbmMBtn0, KbmMouseX). Hover highlights use ember (HoverBrush #FFA24D). Recording targets flash at 400ms with orange (FlashBrush #FFA500). Pressed keys light ember (KeyPressedBrush #FF6B2C).

Rendering

Uses CompositionTarget.Rendering with a _dirty flag. Per frame: - Keyboard keys: reads KbmOutputSnapshot.GetKey() per VK index, sets accent background on pressed keys. - Mouse buttons: reads GetMouseButton() for LMB/RMB/MMB/X1/X2. - Movement dot: maps MouseDeltaX/MouseDeltaY to deflection within the movement circle. - Scroll arrows: lights up/down arrows based on ScrollDelta sign.

Theme-Aware Brushes

Pre-cached, frozen static readonly dark and light brush variants for key backgrounds, borders, and text, picked by theme when used. OnRendering compares the app theme each frame and rebuilds the layout when it changed, so no per-frame DynamicResource lookup happens.

Tooltip Helper

MappingLabel() resolves target setting names to human-readable labels from the mapping table, falling back to the raw name. X1/X2 side button Rectangle elements are promoted to named fields for flash support.


MidiPreviewView

Files: MidiPreviewView.xaml, MidiPreviewView.xaml.cs

MIDI note and CC visualization for MIDI virtual controller slots, shown on the PadPage Preview tab. The same control serves the Devices page twice (MidiNotesPreview, MidiCcPreview) for a physical MIDI input device's raw state.

Layout

Single Canvas (MidiCanvas), rebuilt when MidiSlotConfig properties change (start note, note count, start CC, CC count):

  • CC Sliders. Vertical bars, one per CC. Background rectangle + fill rectangle proportional to value (0-127) + CC number label.
  • Piano Keyboard. Standard chromatic layout: white keys full-height underneath, black keys shorter on top (higher Z-index). White keys show note name + octave (e.g., "C4", "D4").

Interaction

CC sliders and piano keys are clickable for click-to-record (fires ControllerElementRecordRequested with MidiCC{index} or MidiNote{index}). Hover highlights and 400ms flash timer match other preview views.

Rendering

Uses CompositionTarget.Rendering with a _dirty flag. Per frame: - CC sliders: reads MidiOutputSnapshot.CcValues[], scales fill height to 0-100%. - Piano keys: reads MidiOutputSnapshot.Notes[] boolean array, applies the pressed brush (ember-hot #FFA24D for white keys, ember-deep #C43D0C for black keys) on active notes.

Theme-Aware Brushes

Pre-cached, frozen static readonly dark and light brush variants for CC bar fills, piano key surfaces, and label text, picked by theme when used. The canvas is rebuilt when the theme changes, which avoids per-frame DynamicResource overhead.

Layout Rebuild

The entire canvas is cleared and rebuilt on any MidiSlotConfig property change. No partial layout updates.


VRPreviewView

Files: VRPreviewView.xaml, VRPreviewView.xaml.cs

Live preview for a VR slot (#49), shown on the PadPage Preview tab. One VR slot drives both SteamVR hands, so the art is the pair side by side rather than a single controller body.

Layout

Viewbox over a single Canvas (VrCanvas), built once on Loaded from the 2D art in 2DModels/VRCONTROLLER/. Base art is 975×726, and the twelve elements are positioned in those pixels:

Element Targets
VRController_L_Stick / _R_Stick VrLStick / VrRStick (click plus four directions)
VRController_L_A / _R_A VrLA / VrRA (inner press disc, outer touch ring)
VRController_L_B / _R_B VrLB / VrRB
VRController_L_System / _R_System VrLSystem / VrRSystem (single-target)
VRController_L_Trigger / _R_Trigger VrLTrigger / VrRTrigger (body axis, tip band click)
VRController_L_Grip / _R_Grip VrLGrip / VrRGrip

Rendering

Element tinting follows the Rectangle + ImageBrush OpacityMask idiom the branded 2D packs use: the cutout supplies the shape and one brush supplies the color, so lit, hover, and flash all drive the same layer. Triggers and grips fill bottom-up through a RectangleGeometry clip rather than scaling opacity. Stick caps translate up to 14px with deflection, and the stick's tint layer and region highlight share that transform. Frames come from CompositionTarget.Rendering against a cached VrRawState.

Interaction

Same click-to-record contract as the other previews, including Bind(vm) / Unbind() and ControllerElementRecordRequested. Every element except System carries more than one target, so each one gets a region highlight: the element's own overlay clipped to the region under the pointer, at the drawn packs' 0.4 hover opacity.


MousePreviewControl

Files: MousePreviewControl.xaml, MousePreviewControl.xaml.cs

Read-only mouse graphic for mouse-type devices on the Devices page detail pane.

Layout

Built once on Loaded into Canvas (MouseCanvas). Same mouse shape as KBMPreviewView but without click-to-record:

  • LMB/RMB. Contoured Path elements flanking the scroll wheel.
  • Scroll wheel. Rounded Rectangle between buttons, with up/down arrow Polygon indicators.
  • Movement circle. Ellipse with deflection dot tracking live mouse delta.
  • X1/X2 side buttons. Small Rectangle elements on the left edge.

Theme-Aware Brushes

Pre-cached, frozen static readonly dark and light brush variants for mouse body, button fills, and indicator colors. BuildMouse() reruns when the theme changes, consistent with KBMPreviewView and MidiPreviewView.

Rendering

Uses CompositionTarget.Rendering (no dirty flag. Every frame). Reads from DevicesViewModel: - Buttons: RawButtons[0..4].IsPressed mapped to LMB, MMB, RMB, X1, X2. - Movement: MouseMotionX/MouseMotionY (normalized) to dot deflection. - Scroll: MouseScrollIntensity drives arrow fill, opacity, and scale. Arrows grow and brighten with scroll magnitude.


SettingsPage

Files: SettingsPage.xaml, SettingsPage.xaml.cs

Application settings in vertical CardBorder sections.

Card order is pinned by Settings_CardsRunInTheDecidedOrder in PadForge.Tests/PageOrderContractTests.cs, which asserts each card title's binding appears once and after the one before it. Nearly every setting row carries a SettingResetButton bound to ResetSettingCommand with the property name as CommandParameter.

Layout Structure

ScrollViewer (Padding="24,0")
  └─ StackPanel (Margin="0,16,0,16")
      ├─ Page header (E713 gear icon + title)
      ├─ Language card (CardBorder)
      │   ├─ Icon F2B7 + "Language" title + description
      │   └─ ComboBox (AvailableLanguages, SelectedValuePath="Culture", TextSearch.TextPath="NativeName", Width=340.
      │        Each row shows NativeName with EnglishNameIfDifferent beside it in the tertiary brush and declares its own language)
      ├─ Appearance card
      │   ├─ Icon E790 + "Appearance" title + description
      │   ├─ "Theme" label
      │   ├─ ComboBox (System Default / Light / Dark, SelectedIndex=SelectedThemeIndex)
      │   └─ "Show Welcome Tour" Button (Settings_ShowTour, ShowTour_Click) → re-runs the first-run spotlight tour
      ├─ Window card
      │   ├─ Icon E737 + title + description
      │   ├─ Minimize to tray (MinimizeToTray)
      │   ├─ Close to tray (CloseToTray, reset through ResetCloseToTrayCommand)
      │   ├─ Always show in system tray (AlwaysShowTrayIcon, #439)
      │   ├─ Start minimized (StartMinimized)
      │   └─ Start at login (StartAtLogin)
      ├─ Updates card (#457)
      │   ├─ Icon E896 + title + description
      │   ├─ Check for updates automatically (CheckForUpdatesAutomatically)
      │   ├─ Install updates automatically (InstallUpdatesAutomatically, shown only while
      │   │   the automatic check is on)
      │   ├─ Include pre-releases (IncludePreReleaseUpdates)
      │   ├─ UpdateStatusText (hidden while empty, ember while IsUpdateAvailable)
      │   ├─ ProgressBar (UpdateProgress 0-100, shown while IsUpdateDownloading)
      │   └─ Check Now (CheckForUpdatesNowCommand), plus Install and Restart
      │       (InstallUpdateCommand, EmberAccentButton) and Release Notes
      │       (OpenReleaseNotesCommand), both shown only while IsUpdateAvailable
      ├─ Input Engine card
      │   ├─ Icon E9F5 + title + description
      │   ├─ Auto-start toggle (AutoStartEngine)
      │   ├─ Background polling toggle (EnablePollingOnFocusLoss)
      │   ├─ Flydigi Enhanced Protocol toggle (FlydigiEnhancedProtocol)
      │   ├─ Read Logitech G-Keys toggle (GKeysEnabled, #454) + GKeysStatus line
      │   ├─ Read Analog Keyboards toggle (AnalogKeyboardsEnabled, #468) + AnalogKeyboardsStatus line
      │   ├─ Read Bliss-Box Adapters toggle (BlissBoxEnabled, #469) + BlissBoxStatus line
      │   ├─ Polling interval: NumberBox 1-16ms (PollingRateMs) + "ms"
      │   ├─ PollingOverrideNote (ember, shown only while the active profile
      │   │   overrides the global value, #365)
      │   └─ HM inactivity timeout: NumberBox 0-3600s
      │      (HmInactivityDestroyTimeoutSeconds, 0 = never)
      ├─ Assignment Prompts card
      │   ├─ Icon E8DE + title + description
      │   ├─ Offer on a new device (AssignOfferNewDevice)
      │   └─ Offer on an empty slot (AssignOfferEmptySlot)
      ├─ Handheld PC Buttons card (#343)
      │   ├─ Icon E7F8 + title + description
      │   └─ Enable toggle (HandheldButtonsEnabled). On, the Devices page gains
      │      the machine's Hidden Buttons row and its System Motion row
      ├─ Keyboard Screens card (#519)
      │   ├─ Drive Keyboard Screens toggle (KeyboardScreensEnabled) + KeyboardScreensStatus,
      │   │   lines for the feature as a whole. Each screen's line and dialog are on its
      │   │   Devices row
      │   └─ S.T.R.I.K.E. 7 library buttons (DownloadStrike7LibraryCommand,
      │      PickStrike7LibraryCommand, InstallVc2008RuntimeCommand)
      ├─ Novint Falcon card (#521)
      │   ├─ Drive Novint Falcon toggle (NovintFalconEnabled) + FalconStatus: the library
      │   │   that loaded, and why no Falcon opened while none did. It names no Falcon
      │   └─ Choose dhd64.dll… (PickFalconLibraryCommand), Install Falcon Driver
      │      (InstallFalconDriverCommand, shown on FalconCanInstallDriver) and Get Force
      │      Dimension's SDK (OpenFalconDownloadCommand)
      ├─ Battery Alerts card (#293)
      │   ├─ Icon E83F + title + description
      │   ├─ "Notify When Battery Runs Low" toggle (BatteryNotifyEnabled)
      │   ├─ "Notify at or Below" NumberBox 5-50 % (BatteryNotifyThreshold)
      │   ├─ "Also Vibrate the Controller" toggle (BatteryNotifyVibrate)
      │   └─ "Test Notification" Button (TestBatteryNotify_Click)
      ├─ HidHide Driver card
      │   ├─ Icon ED1A + title + description
      │   ├─ Status: flame + HidHideStatusText + HidHideVersion
      │   ├─ Install/Uninstall buttons (visibility-toggled by IsHidHideInstalled)
      │   ├─ Hide devices toggle (EnableInputHiding, only when installed)
      │   ├─ Keep cloaks between launches toggle (KeepHidHideCloaksBetweenLaunches, only when installed)
      │   └─ Whitelist section (only when installed):
      │       ├─ Title + description
      │       ├─ ListBox of HidHideWhitelistPaths (telemetry mono, 12px)
      │       └─ Add/Remove buttons
      ├─ HIDMaestro Driver card
      │   ├─ Icon E7FC + title + description
      │   └─ Status: ember flame lit while the driver is in the driver store + "Installed" or
      │      "Not Installed" + HIDMaestroVersion, and Uninstall, which opens UninstallHIDMaestroDialog
      ├─ BthPS3 Bluetooth Driver card
      │   ├─ Icon E702 + title + description
      │   └─ Status: ember flame lit while either BthPS3 driver is a service + "Installed" or
      │      "Not Installed" + BthPS3Version, and Uninstall, which opens UninstallBthPS3Dialog
      ├─ Windows MIDI Services card
      │   ├─ Icon E8D6 + title + description
      │   ├─ Status: flame (lit while IsMidiAvailable) + MidiServicesStatusText + MidiServicesDetailText
      │   │   (collapsed when empty, telemetry face while MidiDetailIsVersion)
      │   ├─ Install button (InstallMidiRuntimeCommand, shown while CanInstallMidiRuntime)
      │   └─ Uninstall button (UninstallMidiServicesCommand, shown while IsMidiRuntimeInstalled)
      ├─ SteamVR card (#49)
      │   ├─ Icon F119 + title + description
      │   ├─ Status: flame (lit while IsSteamVrInstalled) + SteamVrStatusText (Not Installed,
      │   │   Installed, or the runtime / driver / live-controller state)
      │   ├─ Install directory TextBox + reset + Browse button (SteamVrBrowse_Click →
      │   │   SteamVrInstallDir), shown only while SteamVR is not installed
      │   └─ Install button (InstallSteamVrCommand, while not installed) and Uninstall button
      │       (UninstallSteamVrCommand, while ShowSteamVrUninstall: installed and PadForge-owned)
      ├─ Community Configs card (#9)
      │   ├─ Icon E716 (EmberBrush) + title + description (the endpoint / privacy statement)
      │   ├─ Enable Community Configs checkbox (EnableCommunityConfigLookup)
      │   ├─ Show Legacy Workshop Configs checkbox (ShowLegacyWorkshopConfigs, visible only when enabled)
      │   └─ Clear Cached Configs + Check Imported Profiles for Updates buttons
      ├─ Settings File card
      │   ├─ Icon E8A5 + title + description
      │   ├─ SettingsFilePath (telemetry mono, wrapping)
      │   ├─ Save / Reload / Reset to Defaults / Open Folder buttons
      │   └─ "Unsaved changes" warning (orange, HasUnsavedChanges)
      └─ Diagnostics card
          ├─ Icon E9D9 + title + description
          ├─ Grid (140px label + value):
          │   ├─ App Version (ApplicationVersion = BuildIdentity.Display, e.g. "4.5.3 (r3682@176208e)")
          │   ├─ .NET Runtime (RuntimeVersion)
          │   └─ SDL Version (SdlVersion)
          ├─ "Keep a Diagnostics Log" toggle (DiagnosticsLoggingEnabled) (#303)
          ├─ Log folder path readout (DiagnosticsFolderPath)
          └─ "Save Snapshot" + "Open Log Folder" buttons (SaveDiagSnapshot_Click / OpenDiagFolder_Click)

Key Bindings

Binding Target Description
AvailableLanguages ComboBox ItemsSource Language options with NativeName display. The row style sets each row's Language through CultureLanguageConverter, so a Chinese, Japanese or Korean name is drawn in its own font (#504).
SelectedLanguage ComboBox SelectedItem Current language
SelectedThemeIndex ComboBox SelectedIndex 0=System, 1=Light, 2=Dark
AutoStartEngine CheckBox Auto-start engine on launch
EnablePollingOnFocusLoss CheckBox Continue polling when app loses focus
PollingRateMs NumberBox (1-16) Polling interval in ms
HmInactivityDestroyTimeoutSeconds NumberBox (0-3600) Seconds of device inactivity before the VC is torn down. 0 means never
EnableInputHiding CheckBox Master input hiding toggle (HidHide card)
KeepHidHideCloaksBetweenLaunches CheckBox Leave cloaks in place across app restarts
MinimizeToTray CheckBox Minimize to system tray
CloseToTray / AlwaysShowTrayIcon CheckBox Close to the tray instead of exiting, and keep the tray icon visible while the window is open (#439)
StartMinimized CheckBox Start app minimized
StartAtLogin CheckBox Start at Windows login
CheckForUpdatesAutomatically / InstallUpdatesAutomatically / IncludePreReleaseUpdates CheckBox Updates card toggles (#457). See Updates and Updates Internals
UpdateStatusText / IsUpdateAvailable / IsUpdateDownloading / UpdateProgress string, bool, bool, number Updates card status line, button visibility, and download progress
CheckForUpdatesNowCommand / InstallUpdateCommand / OpenReleaseNotesCommand ICommand Check Now, Install and Restart, Release Notes
FlydigiEnhancedProtocol / GKeysEnabled / GKeysStatus CheckBox, CheckBox, string Input Engine card: Flydigi vendor protocol, Logitech G-Keys input, and the G-Keys status line
AnalogKeyboardsEnabled / AnalogKeyboardsStatus CheckBox, string Input Engine card: Read Analog Keyboards (#468) and its line, which names no keyboard. A keyboard's own line is on its Devices row
BlissBoxEnabled / BlissBoxStatus CheckBox, string Input Engine card: Read Bliss-Box Adapters (#469) and its line, which names no port. Each port's row names its controller
KeyboardScreensEnabled / KeyboardScreensStatus CheckBox, string Keyboard Screens card (#519): the switch and the feature's lines
DownloadStrike7LibraryCommand / PickStrike7LibraryCommand / InstallVc2008RuntimeCommand ICommand The S.T.R.I.K.E. 7 library buttons
NovintFalconEnabled / FalconStatus / FalconCanInstallDriver CheckBox, string, bool Novint Falcon card (#521): the switch, the library line and why no Falcon opened, and Install Falcon Driver's visibility
PickFalconLibraryCommand / InstallFalconDriverCommand / OpenFalconDownloadCommand ICommand Choose dhd64.dll…, Install Falcon Driver, Get Force Dimension's SDK
BatteryNotifyEnabled / BatteryNotifyThreshold / BatteryNotifyVibrate CheckBox, NumberBox (5-50), CheckBox Battery Alerts card: low-battery toast on/off, the percent threshold, and the optional controller vibration (#293)
IsHidHideInstalled bool Controls status flame, button visibility, whitelist section
InstallHidHideCommand / UninstallHidHideCommand ICommand Driver install/uninstall
HidHideWhitelistPaths Collection Whitelist ListBox items
SelectedWhitelistPath object Selected whitelist item
AddWhitelistPathCommand / RemoveWhitelistPathCommand ICommand Whitelist management
IsMidiAvailable / IsMidiRuntimeInstalled / MidiDetailIsVersion bool Windows MIDI Services card: the flame, the Uninstall button's visibility, and the detail line's telemetry face
MidiServicesStatusText / MidiServicesDetailText string The card's status line and the line under it
CanInstallMidiRuntime / InstallMidiRuntimeCommand / UninstallMidiServicesCommand bool, ICommand, ICommand The Install button's visibility, and the runtime's install and uninstall
IsSteamVrInstalled / IsSteamVrOwned / SteamVrInstallDir bool, bool, string SteamVR card status, whether PadForge created the Steam-free install, and its directory (#49)
SteamVrStatusText / ShowSteamVrUninstall string, bool SteamVR status line, and the Uninstall gate (IsSteamVrInstalled && IsSteamVrOwned)
InstallSteamVrCommand / UninstallSteamVrCommand ICommand SteamVR install/uninstall
SaveCommand / ReloadCommand / ResetCommand / OpenSettingsFolderCommand ICommand Settings file operations
EnableCommunityConfigLookup / ShowLegacyWorkshopConfigs CheckBox Steam Workshop opt-in + legacy sub-toggle (#9)
ClearWorkshopCacheCommand / CheckWorkshopUpdatesCommand ICommand Workshop cache clear and imported-profile update check
HasUnsavedChanges bool Orange warning visibility
DiagnosticsLoggingEnabled / DiagnosticsFolderPath CheckBox, string Diagnostics card: continuous engine-event log toggle and the folder it writes to (#303)
ApplicationVersion string Diagnostics App Version row. MainWindow sets it to BuildIdentity.Display
ResetSettingCommand RelayCommand<string> Per-row reset for the named setting

Code-Behind

Five handlers beyond the constructor. Everything else lives in the ViewModel.

Handler Trigger Action
TestBatteryNotify_Click Battery Alerts card button Calls InputService.TestBatteryNotification() to fire the low-battery pipeline with a synthetic event (#293)
SaveDiagSnapshot_Click Diagnostics card button DiagnosticsLogControl.SaveSnapshot() dumps the in-memory engine event ring to a timestamped file and selects it in Explorer (#303)
OpenDiagFolder_Click Diagnostics card button Creates the diagnostics folder if needed and opens it in Explorer (#303)
ShowTour_Click Appearance card button Calls MainWindow.StartFirstRunTour() to re-run the welcome tour (#175)
SteamVrBrowse_Click SteamVR card button OpenFolderDialog for the Steam-free SteamVR install directory, writes SettingsViewModel.SteamVrInstallDir (#49)

ProfilesPage

Files: ProfilesPage.xaml, ProfilesPage.xaml.cs

Per-app profile management.

Layout Structure

ScrollViewer (Padding="24,0")
  └─ StackPanel (Margin="0,16,0,16")
      ├─ Page header (E8F1 people icon + title)
      └─ CardBorder
          ├─ Icon E8B7 + "Management" title + description
          ├─ Auto-switch CheckBox (EnableAutoProfileSwitching)
          ├─ FOREGROUND live readout (visible only while auto-switch is on):
          │   mono token + lit flame when IsForegroundMatched + ForegroundExeName,
          │   plus the no-rules hint when NoProfileHasExecutables
          ├─ External Control CheckBox (EnableExternalControl, #366), off by default
          ├─ Drop-zone Grid (AllowDrop, ProfileList_DragEnter/DragOver/DragLeave/Drop
          │   for .pfprofile import, ProfileDropOverlay cue)
          │   └─ Profile card ListBox (ProfileListBox, WrapPanel of steel cards,
          │       MinHeight=92, MaxHeight=452, MouseDoubleClick loads)
          │       └─ ItemTemplate:
          │           ├─ Profile Name (SemiBold) + "Built in" tag on the default
          │           ├─ Executable names (FirstExecutableName, SecondExecutableName,
          │           │   ExtraExecutablesSuffix, each collapsed when empty)
          │           └─ Type count badges (horizontal StackPanel):
          │               ├─ Xbox badge: Xbox SVG + XboxCount (collapsed when 0)
          │               ├─ PlayStation badge: PS SVG + PlayStationCount (collapsed when 0)
          │               ├─ Extended badge: Joystick SVG + ExtendedCount (collapsed when 0)
          │               ├─ MIDI badge: E8D6 glyph + MidiCount (collapsed when 0)
          │               ├─ KB+M badge: E961 glyph + KbmCount (collapsed when 0)
          │               ├─ Nintendo badge: Switch SVG + NintendoCount (collapsed when 0)
          │               ├─ VR badge: F119 glyph + VrCount (collapsed when 0)
          │               ├─ "No slots" fallback (visible when HasNoSlots=True)
          │               └─ Auto-switch rule chip (ChipCold, AutoSwitchRuleSummary, visible
          │                   when HasExecutables and auto-switch is on)
          └─ Action buttons: New / Save As / Load / Edit / Export / Import /
             Browse Starters / Browse Community / Delete

Key Bindings

Binding Target Description
EnableAutoProfileSwitching CheckBox Enables foreground app monitoring
EnableExternalControl CheckBox Opens the named pipe that lets a launcher or script activate a profile (#366). It sits with the auto-switch and shortcut controls because it is a third way a profile activates, not a global engine service. The SettingsViewModel setter mirrors into SettingsManager.EnableExternalControl, which InputService watches to start or stop ExternalControlService. Switching profiles by hand releases the hold a script placed
ForegroundExeName / IsForegroundMatched / NoProfileHasExecutables TextBlock, flame, hint The FOREGROUND live readout. SettingsViewModel.ActiveProfileInfo is set by the services but is not bound on this page. The status bar's ProfilePill carries the active-profile name
ProfileItems ListBox ItemsSource Profile list
SelectedProfile ListBox SelectedItem Selected profile. SelectedProfile.IsDefault disables Edit and Delete
NewProfileCommand Button Create new profile
SaveAsProfileCommand Button Save current config as profile
EditProfileCommand Button Edit profile name/exes
LoadProfileCommand Button Load selected profile
ExportProfileCommand / ImportProfileCommand Button .pfprofile export and import
BrowseStarterProfilesCommand Button Opens StarterProfilesDialog (#256)
BrowseCommunityConfigsCommand Button Opens WorkshopBrowseDialog (#9)
DeleteProfileCommand Button Delete selected profile

Profile Item Bindings

Binding Description
Name Profile name (SemiBold)
IsDefault Marks the built-in Default profile. Blocks Edit and Delete
Executables Comma-separated exe list backing the card. HasExecutables gates the auto-switch rule chip
AutoSwitchRuleSummary Profiles_AutoSwitchRule_Format over the exe list, shown in the cold auto-switch chip
FirstExecutableName / SecondExecutableName / ExtraExecutablesSuffix The card renders at most two exe names plus a "+N more" suffix, each collapsed via StringToVisibility
XboxCount / PlayStationCount / ExtendedCount / MidiCount / KbmCount / NintendoCount / VrCount Per-type counts (badge collapsed when 0)
HasNoSlots Shows "No slots" badge when all seven type counts are zero

Controller Shortcuts Card

Added below the profile management card. Provides per-shortcut combo recording and mode selection.

CardBorder (Margin="0,20,0,0")
  └─ StackPanel
      ├─ Icon E71B + "Shortcuts" title + description
      ├─ ItemsControl (ItemsSource="{Binding ProfileShortcuts}")
      │   └─ ItemTemplate (Grid, 5 columns):
      │       ├─ Col 0 (3*, 120-328 px): Mode ComboBox (SwitchModes: Next / Previous /
      │       │    Specific / ToggleWindow / ToggleVCsDisabled) + reset
      │       ├─ Col 1 (2*, 120-178 px): Profile ComboBox (ProfileChoices) + reset,
      │       │    visible only in Specific mode (IsSpecificMode)
      │       ├─ Col 2 (3*, 120-328 px): Device ComboBox (DeviceChoices) + reset
      │       ├─ Col 3: ButtonComboDisplay TextBlock (fills remaining, marquee-enabled)
      │       └─ Col 4: Action buttons (Learn/Clear/Delete)
      │           ├─ Learn: Click="ShortcutLearn_Click", icon toggles Record/Stop
      │           ├─ Clear: Command="{Binding ClearCommand}", icon E75C
      │           └─ Delete: Command="{Binding DeleteCommand}", icon E74D
      └─ "Add Shortcut" Button (Click="AddShortcut_Click")

Event Handlers (Code-Behind)

Handler Trigger Action
ProfileList_MouseDoubleClick ListBox.MouseDoubleClick Executes LoadProfileCommand
ProfileList_DragEnter / DragOver / DragLeave / Drop Drop-zone Grid Accepts a dropped .pfprofile file for import and drives the ProfileDropOverlay cue
ShortcutLearn_Click Learn button Click Starts 5-second combo recording for the row's ProfileShortcutViewModel
ProfileChoices_DropDownOpened / DeviceChoices_DropDownOpened ComboBox.DropDownOpened Rebuilds the shortcut row's profile and device lists on open
AddShortcut_Click "Add Shortcut" button Click Creates a new GlobalMacroData, wraps in ProfileShortcutViewModel, adds to list

Shortcut Recording State (Code-Behind)

Field Type Description
_recordingShortcut ProfileShortcutViewModel Currently recording shortcut, or null
_recordTimer DispatcherTimer (33 ms) Fires RecordTimer_Tick during recording
_lastRecordedEntries TriggerButtonEntry[] Entries captured so far (saved on stop)
_recordAxisBaselines Dictionary<Guid, int[]> Per-device axis snapshots at record start. Deflections exceeding AxisRecordDeltaThreshold (0.25) register as axis triggers
RecordTimeoutSeconds const double (5) Auto-stop timeout

ProfileSwitchOverlay

Files: ProfileSwitchOverlay.xaml, ProfileSwitchOverlay.xaml.cs

Win11 volume OSD–style flyout that appears above the taskbar during profile switches. Shows the profile name, then transitions through initializing/active/offline states.

Window Properties

Non-activating, topmost, transparent-background overlay: - WindowStyle="None", AllowsTransparency="True", Topmost="True" - ShowInTaskbar="False", ShowActivated="False", Focusable="False" - WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW applied via SetWindowLong in OnLoaded - WM_MOUSEACTIVATE intercepted to return MA_NOACTIVATE. Clicks pass through

Layout Structure

Grid (ClipToBounds="True")           ← clips slide animation at taskbar edge
  └─ Grid x:Name="FlyoutPanel" (Margin="10,10,10,14")
      ├─ ShadowBorder (CornerRadius=8, DropShadowEffect BlurRadius=15)
      └─ ContentBorder (CornerRadius=8, Background=#2D2E2E, Border=#141516)
          └─ Grid (Margin="16,13,16,13")
              └─ StackPanel (Horizontal)
                  ├─ StatusIcon (Segoe Fluent Icons, 16px)
                  └─ StatusText (Segoe UI Variable Text, 14px)

All color values pixel-measured from the native Win11 volume OSD at 2560x1600 / 150% DPI. Dark and light themes applied dynamically via ApplyTheme() using ApplicationThemeManager.GetAppTheme().

Slide Animation

FlyoutPanel.RenderTransform is a TranslateTransform. The outer Grid uses ClipToBounds="True" to hide the panel while it slides:

Method Direction Duration Easing
SlideIn() Y: 80 → 0 300 ms CubicEase (EaseOut)
SlideOut(Action onCompleted) Y: 0 → 80 250 ms CubicEase (EaseIn)

SlideIn() snaps _slideTransform.Y = SlideTravel (80) synchronously, then defers the animation to DispatcherPriority.Loaded so WPF does not coalesce start and end values into a single frame.

State Machine

The overlay progresses through up to four phases after ShowProfileName(name) is called:

Profile Name (2s) → Initializing (polling) → Active (2s) → Offline (2s) → Hide
                                                         └─→ Hide (if no offline)
Phase Icon Text Timer
Profile \uE8F1 (people) Profile name _dismissTimer 2 s → start init monitor
Initializing \uE895 (sync) "Initializing" _initMonitorTimer 33 ms polling CheckInitState
Active \uE73E (checkmark, accent color) "Forging" _dismissTimer 2 s → check offline
Offline \uE7BA (warning, #FFB900 amber) "One or more controllers offline" _dismissTimer 2 s → slide out + hide

During the Initializing phase, StatusIcon runs a DoubleAnimation opacity flash (1.0 → 0.3, 600 ms, AutoReverse, RepeatBehavior.Forever).

Public API

Method / Property Description
CheckInitState Func<(bool anyInitializing, bool allReady)>. Set by InputService
CheckAnyOffline Func<bool>. Set by InputService
ShowProfileName(string name) Resets state, shows profile name, starts the state machine
ShowVCsToggle(bool enabled) One-shot toast for the master virtual-controller toggle, outside the profile state machine. Checkmark \uE73E in Fluent success green #37C852 when enabled, cancel \uE7E8 in Fluent critical red #E81B1C when disabled, then a 2 s dismiss. Called from InputService.ShowVCsToggleOverlay
StopTimers() Stops both _dismissTimer and _initMonitorTimer. Called during shutdown

Positioning

ShowFlyout() centers the window horizontally within SystemParameters.WorkArea and positions the bottom edge at WorkArea.Bottom. The 14 px bottom margin on FlyoutPanel provides the gap above the taskbar.


AboutPage

Files: AboutPage.xaml, AboutPage.xaml.cs

Application identity, description, technologies, and license.

Layout Structure

ScrollViewer (Padding="24,0")
  └─ StackPanel (Margin="0,16,0,16")
      ├─ Page header (E946 info icon + title)
      ├─ App identity card (centered, 24px padding)
      │   ├─ PadForge-icon.png (120px)
      │   ├─ "PadForge" (28px, Bold, Display face)
      │   ├─ Subtitle (14px)
      │   └─ Tagline (12px)
      ├─ "An Invitation" section header (About_Testimony, E734 icon)
      ├─ Testimony card (Scripture + reference + doxology, italic)
      ├─ "Overview" section header (E7C3 icon)
      ├─ Description card (wrapping text, line height 22)
      ├─ "Built With" section header (E74C checkmark icon)
      ├─ Technologies card (Grid, 164px label + description, 133 rows in alphabetical
      │   order, case-insensitive, leading symbols ignored):
      │   ├─ 3doverstock
      │   ├─ AntiMicroX
      │   ├─ Arcade I/O references
      │   ├─ AudioEndPointLibrary
      │   ├─ AutoEq
      │   └─ ...128 more open-source attributions through ZstdSharp (HIDMaestro, SDL3,
      │       .NET 10, HidHide, NAudio, WPF UI, the Kaldi / OpenFst / OpenBLAS /
      │       CLAPACK row, and others)
      ├─ "License" section header (E8D7 icon)
      └─ License card (12px wrapping text, secondary brush)

The rows sort by label, ignoring case and a leading symbol, so .NET 10 sits between nefcon and Nintendo_Switch_Reverse_Engineering and $Q Recognizer between psmoveapi and QMK Firmware. Grid.Row runs 0 (3doverstock) through 132 (ZstdSharp). Row 60, "Kaldi, OpenFst, OpenBLAS, and CLAPACK" (About_Kaldi / About_KaldiDesc), credits the speech decoder, transducer library, and linear algebra inside libvosk.dll. A new row takes its alphabetical place, and every Grid.Row after it moves down one.

Code-Behind

Constructor only. All text from localized string bindings.


Dialog Windows

CopyFromDialog

Files: CopyFromDialog.xaml, CopyFromDialog.xaml.cs

Modal dialog to copy from another slot (FluentWindow, 420x420). Lists one entry per other slot whose mapping table has rows (InputService.SlotHasAnyMapping or the donor PadSetting.HasAnyMapping), plus one entry per unmapped device that still carries a PadSetting with mappings. The target slot itself is skipped. The chosen slot's mapping table is applied wholesale to the target, moved by role where the two slots are on different wires of the same pads (InputService.LandingOf), and every assigned device's per-device tuning carries along through InputService.BuildPerDeviceSettingsSnapshot + ApplyPerDeviceSettingsToSlot, matched by InstanceGuid (perfect round-trip) or ProductGuid (same model, different unit).

ProfileDialog

Files: ProfileDialog.xaml, ProfileDialog.xaml.cs

Modal dialog to create/edit profiles (FluentWindow, 500x470). Fields: profile name, executable list (comma-separated).

StarterProfilesDialog

Files: StarterProfilesDialog.xaml, StarterProfilesDialog.xaml.cs

The bundled starter-profile browser (#256). FluentWindow (700x620, min 560x480) opened from the Profiles page "Browse Starters" button. Picking a starter materializes it as a new profile.

RegisterNfcTagDialog

Files: RegisterNfcTagDialog.xaml, RegisterNfcTagDialog.xaml.cs

Capture-and-name flow for NFC tags (#150). The class extends Wpf.Ui.Controls.FluentWindow (ExtendsContentIntoTitleBar, WindowBackdropType="Mica", 520x520). Opened from the Devices page for an NFC reader. Tapping a tag captures its UID, the user names it, and it is added to NfcTagRegistry, which surfaces it as a bindable named button on the NFC device. The dialog also lists registered tags with a Remove action.

While open, the dialog subscribes to two capture paths: NfcReaderService.TagDetected (from NfcReaderService.Active) for a standalone reader, and NfcTagRegistry.ControllerTagDetected for a controller-borne reader, with NfcTagRegistry.RegistrationCaptureActive raised for the dialog's lifetime. OnTagDetected fires on the reader's monitor thread and marshals to the UI thread via Dispatcher.BeginInvoke before normalizing the UID and enabling RegisterBtn. RegisterButton_Click calls NfcTagRegistry.Register(uid, name), then refreshes the list. Both subscriptions are torn down on Closed.

Element Binding / Handler Purpose
StatusText localized status strings Tap prompt, captured, or no-reader message
UidText captured UID (telemetry mono) Live UID readout
NameBox NameBox_KeyDown Tag name entry. Enter registers
RegisterBtn RegisterButton_Click Registers the captured tag
TagListBox NfcTagRegistry.Tags Registered tags with per-row Remove (RemoveButton_Click)

LearnHandheldButtonDialog

Files: LearnHandheldButtonDialog.xaml, LearnHandheldButtonDialog.xaml.cs

Learner for a handheld PC's hidden buttons (#343), sharing the NFC dialog's head chrome. FluentWindow (ExtendsContentIntoTitleBar, WindowBackdropType="Mica", 560x640, ResizeMode="NoResize") opened from the Devices page LearnHandheldButton_Click.

A learn run is a three-phase timed pass driven by _phaseTimer over HandheldLearnSession: Idle (the noise floor), Press, then Release, each phase lasting the session's own IdleMs / PressMs / ReleaseMs. FinishLearn reads the candidates and the chord keys the session collected, and the user names the winner and registers it. There is no per-model table: the machine teaches the app what its buttons are.

Element Binding / Handler Purpose
MachineText / DaemonText code-behind The detected handheld and the vendor-daemon notice
StartBtn StartButton_Click Begins the three-phase pass. Disabled when the row has been retired mid-dialog
StatusText phase strings Which phase is running, or why none can
ChordText / CandidateBox learn results The chord the press produced, and the candidate list when the pass finds more than one
NameBox NameBox_KeyDown Display name. Enter registers
RegisterBtn RegisterButton_Click Adds the learned button to HandheldButtonRegistry
ButtonListBox / EmptyText RefreshList Learned buttons with per-row Remove (RemoveButton_Click), and the empty-state line
Export / Import ExportButton_Click / ImportButton_Click Carries a learned set between machines

RegisterVoicePhraseDialog

Files: RegisterVoicePhraseDialog.xaml, RegisterVoicePhraseDialog.xaml.cs

Voice macro management (#317), modeled on RegisterNfcTagDialog and sharing its head chrome. FluentWindow (ExtendsContentIntoTitleBar, WindowBackdropType="Mica", 640x620, ResizeMode="NoResize") opened from the Devices page ManageVoicePhrases_Click. Three bands: the settings row (EnabledBox, ModeBox, ConfidenceSlider clamped 0.5 to 0.99 with a ConfidenceText readout), the live HeardText readout prefixed with the microphone that heard the phrase, and the type-and-name registration row with a phrase list carrying per-row Remove.

There is no source picker. Phrases live on the devices that carry the microphones, and every reachable microphone runs its own session.

The dialog subscribes to the static VoiceMacroService.PhraseHeard event, hops to the UI thread with Dispatcher.BeginInvoke, and lights the matching PhraseRow for 1400 ms. Only a firing recognition lights a row: the engine maps every utterance to its nearest phrase, so lighting on any event made rows claim matches the confidence floor had already refused. The readout line still shows every attempt with its confidence. Each row owns its own FlashTimer, restarted per hit so overlapping recognitions extend the light instead of truncating it. Closed and the close button both run Unsubscribe, which stops every row timer and detaches the handler.

Element Binding / Handler Purpose
EnabledBox / ModeBox / ConfidenceSlider Setting_Changed Writes VoiceMacroService.Enabled / ListeningMode / MinConfidence, then marks settings dirty
HeardText VoiceMacroService.PhraseHeard [source] text (confidence), fired or ignored
PhraseBox / NameBox PhraseBox_KeyDown Phrase and display name. Enter registers
RegisterBtn RegisterButton_Click VoicePhraseRegistry.Register(phrase, name), then refreshes
PhraseListBox _rows (PhraseRow) Registered phrases with per-row Remove (RemoveButton_Click)

ConfirmDialog

Files: ConfirmDialog.xaml, ConfirmDialog.xaml.cs

Generic modal confirm. FluentWindow with the ember-tick display-face header, a message body, and OK / Cancel buttons. Used ahead of destructive actions across the app.

ManageProfilesDialog

Files: ManageProfilesDialog.xaml, ManageProfilesDialog.xaml.cs

HIDMaestro profile catalog manager. FluentWindow (640x660). Section 1 lists imported profiles (name + id) with Import From File / Export / Delete (Delete wears the destructive chrome and confirms first). A row HIDMaestro left out of the pickers carries a third line in SystemFillColorCautionBrush, ImportedProfileRow.Problem: HIDMaestro's reason and JSON path for a refused file, or the missing HID descriptor. Section 2 imports a profile from a connected device. An import HIDMaestro would refuse is not stored, and the status line gives the reason (ImportFailedText).

UninstallHIDMaestroDialog

Files: UninstallHIDMaestroDialog.xaml, UninstallHIDMaestroDialog.xaml.cs

The removal of HIDMaestro from the PC (#518), in ConfirmDialog's chrome: the ember-tick title, the body, and a footer where Cancel holds the default and the initial focus. Confirm(owner, preview, out includeUsbip) runs the preview it is handed (HIDMaestroRemoval.Preview on a worker) on load and on each change of the usbip-win2 checkbox, showing a progress line meanwhile, then lists each item as location, kind and, for one kept or not read, the reason (RowsFor). A blocked preview names the programs in the cold mono face, and a preview with nothing to take says so. Refresh reads the PC again. Uninstall opens only when CanRemove holds. ShowReport(owner, report, statusLine) reuses the window with Close alone to list what a failed removal could not take. A preview that ends after the window closed, or after a newer one started, is dropped (_generation).

UninstallBthPS3Dialog

Files: UninstallBthPS3Dialog.xaml, UninstallBthPS3Dialog.xaml.cs

The removal of BthPS3 from the PC, built as UninstallHIDMaestroDialog is, without its usbip-win2 option. Confirm(owner, preview) runs the preview it is handed (BthPS3Removal.Preview on a worker) on load and on Refresh, then lists each item as location, kind (BthPS3Removal.KindText) and, for one kept, the reason in the HIDMaestro removal's Stays: form (RowsFor). The line above the list (NoteFor) says what holds the removal (a connected controller, or a check that could not run), that DsHidMini keeps the drivers, or that nothing is left. Uninstall opens only when CanRemove holds. ShowReport(owner, report, statusLine) reuses the window with Close alone to list what a removal could not take.

PairDeviceDialog

Files: PairDeviceDialog.xaml, PairDeviceDialog.xaml.cs

Wii Remote pairing flow (#116). FluentWindow (WiiPair_Title) that walks the user through the Bluetooth sync-button handshake.

PickSoundDialog

Files: PickSoundDialog.xaml, PickSoundDialog.xaml.cs

Sound file chooser for macro sound actions and sound packages. FluentWindow (460x480).

ColorPickerControl

Files: ColorPickerControl.xaml, ColorPickerControl.xaml.cs

HSV color picker UserControl. A 200x200 saturation/value square over a hue base layer, plus a hue strip. Feeds the lightbar and RGB color rows. The hue base layer is rebuilt from code-behind on hue change.

ShiftActivatorDialog

Files: ShiftActivatorDialog.xaml, ShiftActivatorDialog.xaml.cs

Shift-layer editor (Pad_Shift_DialogTitle, 600x760). Records the activator combo and configures the layer, including RGB color rows cloned from the Lighting tab's chrome. The square icon button left of the name box shows the layer's icon, a picture (IconPickerImage, 22x22) when the reference resolves and the glyph (IconPickerGlyph) otherwise, both through MenuIconResolver.ResolveLayerIcon. It opens the shared IconPicker (#471) with Reset to Default (⇧) as its clear button. The emoji catalog (EmojiCatalog, EmojiCategory) stays in this file and the picker reads it.

ShiftLayerFlyout

Files: ShiftLayerFlyout.xaml, ShiftLayerFlyout.xaml.cs

Win11 volume-OSD-style flyout that reuses ProfileSwitchOverlay's pixel-measured chrome (bg #2D2E2E, border #141516, corner radius 8, layered shadow border plus content border so ClearType survives). Stays visible while a shift layer is engaged on the currently-viewed slot, slides out when the slot returns to Base. ShowLayer draws the layer's icon through MenuIconResolver.ResolveLayerIcon: a picture reference that resolves fills StatusImage (20x20, uniform, in the glyph's spot, #471), a single glyph such as an emoji shows as text in StatusIcon, and anything else, an empty reference included, shows ⇧.

TouchpadGestureRecorderDialog

Files: TouchpadGestureRecorderDialog.xaml, TouchpadGestureRecorderDialog.xaml.cs

Touchpad gesture recorder (#88). FluentWindow (640x660) that captures a stroke and saves it as a named gesture template. Inherits theme from Application.Resources (no local ThemesDictionary) so it tracks the user's light/dark choice.

TouchpadOverlay

Files: TouchpadOverlay.xaml, TouchpadOverlay.xaml.cs

Transparent, topmost Window that mirrors the touchpad surface with live finger dots and a fixed 32px click bar in the bottom row. Repositionable via the surface drag region, stylus press-and-hold / feedback suppressed.

Files: MenuOverlayWindow.xaml, MenuOverlayWindow.xaml.cs

On-screen radial / touch menu HUD (#9). A click-through, never-activated Window (WindowStyle="None", AllowsTransparency, Topmost, ShowInTaskbar="False", ShowActivated="False", SizeToContent="WidthAndHeight") following the ShiftLayerFlyout precedent: WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT applied on Loaded, plus a WndProc hook answering WM_MOUSEACTIVATE with MA_NOACTIVATE, so it never steals focus from the game and never eats a click.

Owned by InputService, which pulls InputManager.ActiveMenuOverlay on the ~30 Hz UI timer (created lazily on first engage, hidden when no menu is engaged or the Dashboard's EnableMenuOverlay is off) and calls UpdateFromSnapshot. Content is built in code on a bare Canvas: annular wedge Path geometries for radial rings (a single slot renders as the full donut), rounded Rectangle cells for grids. Geometry rebuilds only when the menu identity or its GeometrySig (kind, cell count, center, labels, scale, position, opacity, name, item labels) changes. The per-tick work is the hover restyle: the hovered cell fills with SystemAccentColorPrimaryBrush when resolvable, ember orange otherwise. MaxRenderCells (64) caps the visual build. A hand-hacked config past that still hovers and commits, the window just refuses to build an unbounded visual. The window is centered at the menu's PosXPercent / PosYPercent on the primary work area (50/50 = centered), clamped fully on screen. Theme-aware through dark / light brush pairs re-applied on every rebuild.

IconPicker

Files: IconPicker.xaml, IconPicker.xaml.cs

The shared icon picker (#471), a UserControl shown in a Popup below the button that opened it (PlacementMode.Bottom, StaysOpen="False", so a click elsewhere closes it). Menu cell rows and the shift-layer dialog both open it through IconPickerHost.Open(anchor, currentIcon, clearLabel, apply, showPackage). Two tabs, both drawing every choice as itself:

  • Emoji (IconPicker_Emoji): a category bar, whose tooltips and accessible names are the localized Emoji_Category_* strings through EmojiCategory.DisplayName, and a grid over ShiftActivatorDialog.EmojiCatalog.
  • Images (IconPicker_Images): every registered icon package's entries as 40x40 image buttons with their file names as accessible names, grouped by package (IconGroup / IconChoice) under a trimmed header, IconPicker_NoPackages when no package is registered, and Browse Files… at its foot. Thumbnails decode into an 80 by 80 box (BoundedBitmap) from one read of each archive (IconPackageManager.ReadIcons), an entry at a time, so only one entry's bytes are held. A package gives the picker its first 2,048 image entries (MaxPickerIcons) and at most 128 MB of entry bytes (MaxPickerBytes, counting entries left out for size), and the picker holds at most 4,096 thumbnails across every package (IconPicker.MaxThumbnails), since a profile import registers every package it carries. The read that reaches that bound stops there, and no later package is read until the registry changes. The picker keeps the name of each package it has read, so the next opening reads none of them again, and an entry past a bound or one that does not decode is simply absent. A package it could not read, missing or locked by another program, is not kept, so the next opening tries it again. Thumbnails are kept apart from the resolver's full-size images and dropped when the registry changes.

The choice the caller holds now wears the active tab's ember (IsCurrent), and the picker opens on its tab and its emoji category. The clear button at the bottom takes the caller's label: No Icon for a cell, Reset to Default (⇧) for a layer. On open, keyboard focus goes to the open tab, since WPF moves none into a popup. Tab cycles inside the picker, and Escape closes it. A pick raises IconChosen (an empty string clears) and closes the popup, and a pick or Escape returns focus to the button that opened it. Browse Files… closes it and runs IconPickerHost.BrowseFromDisk, which offers images the resolver draws (PNG, JPEG, GIF, BMP) and .pficons packages: an image file stores through IconPickerHost.StoredImagePath, exe-relative under the app folder and .\name for a file directly beside the exe, a .pficons file is registered, a one-entry package is chosen at once, and a package with several entries reopens the picker on that package's pictures.

RemoteLinkPairDialog

Files: RemoteLinkPairDialog.xaml, RemoteLinkPairDialog.xaml.cs

Remote Link consent screen (#138). FluentWindow (460 wide) that shows the short authentication string (SasText, compared out of band against the peer screen), the peer IdentityText, a consent-warning InfoBar, and a GamepadOnlyCheck checkbox. Pair accepts. Reject, or closing the window, declines.

RemoteLinkPasswordDialog

Files: RemoteLinkPasswordDialog.xaml, RemoteLinkPasswordDialog.xaml.cs

Remote Link password prompt (#138). FluentWindow (440 wide) with two PasswordBox fields (entry + confirm) and an error InfoBar. OK submits, Enter in either box submits too.

RemoteAssignmentsDialog

Files: RemoteAssignmentsDialog.xaml, RemoteAssignmentsDialog.xaml.cs

Remote device assignment (#138). FluentWindow (Mica, 620x500, min 480x360) titled RemoteLink_AssignmentTitle_Format with the peer's name, opened by InputService from a paired PC's Assign Shared Devices button. DevicesBox lists the devices this PC shares with that peer. Picking one queries the other PC through LinkAssignmentChannel.QueryAsync, and SlotsList shows its virtual controllers as check boxes, with ProfileText naming the other PC's active profile. Checking or clearing a slot sends SetAsync with the last reply's revision, so a change made on the other PC in between comes back as Stale for review. StatusText carries the busy, denied (the other PC has not allowed remote assignment changes for this PC), timeout, and no-slot messages. RefreshButton reloads the device list and the query.

WorkshopBrowseDialog

Files: WorkshopBrowseDialog.xaml, WorkshopBrowseDialog.xaml.cs

The Steam Workshop config browser (#9). FluentWindow (Mica, 1280x760, min 1080x640) with a three-state flow: cold-forge opt-in panel, game-search shelf, game room (config cards plus the translation manifest pane). Search debounces 500 ms. Preset chips re-run the translation live. Art crossfades 240 ms through steel and honors the Windows animation setting. On Save it hands the materialized profile to MainWindow's AddWorkshopProfile import sink. The config card's pad art is WorkshopControllerPreview (Views/WorkshopControllerPreview.xaml), a UserControl wrapping a Viewbox over a code-built Canvas so callouts stay in the art's own pixel space, which is what ControllerOverlayLayout's coordinates are expressed in. Full anatomy on Steam Workshop Config Import Internals.

CustomTelemetryFormatsDialog

Files: CustomTelemetryFormatsDialog.xaml, CustomTelemetryFormatsDialog.xaml.cs

The custom telemetry formats (#531), opened by the Dashboard's Custom Formats… button. FluentWindow (Mica, 1040x680, not resizable, dragged by its inert chrome since it has no title bar) titled Telemetry_Formats_Title. FormatList on the left lists the formats with Add, Duplicate and Delete buttons. EditorPanel on the right edits the selected one: its name, its packets (binary or JSON), the length and byte order for binary packets, the Required Key a JSON packet must carry, the optional gear values, and its Fields rows (name, target, offset or key, type, scale, min, max), with Add Field and a remove button per row. The dialog edits copies (TelemetryFormatEdit), so Cancel keeps the saved list. Save first pushes a focused text box's value, then shows the first problem in ProblemText and stays open, or sets Result and closes. A duplicate name is a problem, since the picker and the profiles name a format by it. New and duplicated formats take the first free name from UniqueName. Detail on Game Telemetry Internals.


Value Converters

All converters in PadForge.App/Converter/ (PadForge.Converters namespace). All but one are registered as StaticResource in App.xaml (lines 1057-1076). UppercaseConverter (key UpperConverter) is registered in ControllerIcons.xaml instead.

Converter Key Input Output Description
DzShapeNameConverter DzShapeNameConverter int (shape index) string Deadzone shape index to localized display name for the Sticks header subtitle.
EqBandTypeNameConverter EqBandTypeNameConverter EqBandType string Localized EQ band-type name for the Audio tab band picker. Defaults to Peak, the enum's zero (#347).
BoolToVisibilityConverter BoolToVisibilityConverter bool Visibility true = Visible, false = Collapsed. Parameter "Invert" reverses. Supports ConvertBack.
HexToBrushConverter HexToBrushConverter string (ARGB/RGB hex) SolidColorBrush Parses #FF8E44AD / #8E44AD to a brush. Empty/invalid falls back to muted gray #555555 (frozen). Used by the shift-layer tab colored dots.
NormToCanvasConverter NormToCanvasConverter double (0-1) double Canvas position, dot-centered. Parameter: "canvasDim" or "canvasDim,dotSize" (default dot 14).
SignedNormToCanvasConverter SignedNormToCanvasConverter double (-1..+1) double Signed canvas position: -1 = left/top edge, 0 = center, +1 = right/bottom. For the Constant Force grid.
PercentToSizeConverter PercentToSizeConverter int/double (0-100) double Percentage to pixel size. Parameter = max (sign preserved for offsets). For the deadzone ring.
CrossGeometryConverter CrossGeometryConverter MultiBinding(DeadZoneX, DeadZoneY) Geometry Cross-shaped path for the axial deadzone overlay.
SlopedWedgeGeometryConverter SlopedWedgeGeometryConverter MultiBinding(DeadZoneX, DeadZoneY) Geometry Triangular wedge paths for the sloped deadzone overlay.
NullToCollapsedConverter NullToCollapsedConverter object Visibility Non-null = Visible, null = Collapsed.
StringToVisibilityConverter StringToVisibility string Visibility Non-null/non-empty = Visible, else Collapsed.
EnumIndexConverter EnumIndexConverter enum int Bridges an enum property and ComboBox.SelectedIndex (enum values assumed sequential 0..N). Supports ConvertBack.
EnumEqualityVisibilityConverter EnumEqualityVisibilityConverter enum Visibility Visible when the enum name matches the parameter (case-insensitive, pipe-separated list allowed). Switches per-mode UI sections.
BoolToTriggerShapeKindConverter BoolToTriggerShapeKindConverter bool (IconRightSide) LabeledShapeKind false = TriggerLeft, true = TriggerRight.
RgbToBrushConverter RgbToBrushConverter MultiBinding(R, G, B) bytes SolidColorBrush Lightbar preview swatch on the Lighting tab. Falls back to black.
IndexToLetterConverter IndexToLetterConverter int (0-25) string 0..25 maps to a..z. Index >= 26 falls back to s[N]. Macro formula chips.
IndexLessThanVisibilityConverter IndexLessThanVisibilityConverter int (count), param int (index) Visibility Visible when param < count. Reveals a per-letter formula chip when its index is within the variable count.
OneBasedIndexConverter OneBasedIndexConverter int (0-based) string Renders a 0-based index as its 1-based number. Touchpad-tab pad selector.
ExeIconConverter ExeIconConverter string (exe path) ImageSource Extracts the exe's shell icon for profile cards (cached per path, frozen). Returns null when the path is empty/missing.
CultureLanguageConverter CultureLanguageConverter CultureInfo XmlLanguage The language a row of the language picker declares: the culture's own for Chinese, Japanese and Korean, en-US for every other (UiLanguage.Declared).
UppercaseConverter UpperConverter string string Uppercases header strings for the ember eyebrow treatment. Registered in ControllerIcons.xaml, not App.xaml.

Resource Dictionaries and Theming

App.xaml Resources

The #175 ember restyle grew App.xaml well past the old two-dictionary shell. Merged dictionaries (in order):

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ui:ThemesDictionary Theme="Dark"/>     <!-- WPF UI theme, design-time default -->
            <ui:ControlsDictionary/>                <!-- WPF UI control styles -->
            <ResourceDictionary>                    <!-- font families (#175 typography) -->
                <FontFamily x:Key="TelemetryFontFamily">Cascadia Code, Consolas, Segoe UI</FontFamily>
                <FontFamily x:Key="BodyFontFamily">Segoe UI Variable Text, Segoe UI</FontFamily>
                <FontFamily x:Key="DisplayFontFamily">Segoe UI Variable Display, Segoe UI</FontFamily>
            </ResourceDictionary>
            <ResourceDictionary Source="/Resources/ControllerIcons.xaml"/>
            <ResourceDictionary>                    <!-- ember identity tokens (#175) -->
                <!-- Ember*/Cold*/Steel* brushes, WaitBrush, EmberSegGradient,
                     FlameOuterGeometry, LivenessFlameBase, MiniTypeButton styles -->
            </ResourceDictionary>
        </ResourceDictionary.MergedDictionaries>

        <!-- App-level styles (all in the root dictionary): -->
        <!-- implicit ComboBox / Button / CheckBox / TextBox / uictrl:TextBox /
             ToolTip / uictrl:FluentWindow / uictrl:DynamicScrollBar restyles -->
        <!-- keyed: EmberIconButton(+Hot), EmberAccentButton, EmberPrimaryButton,
             EmberDestructiveButton, EmberSelectListItem, EmberSlider,
             InstrumentBarRaw / InstrumentBarOut, EmberFocusVisual, EntranceFade -->
        <!-- 20 global converter registrations (lines 1057-1076) -->
    </ResourceDictionary>
</Application.Resources>

Ember identity tokens (ColdBrush, EmberBrush, SteelGroundBrush, CrucibleCardBrush, and their relatives) carry the #175 color language: cold = the physical side (devices, sources, telemetry), ember = the virtual side (outputs, anything live), steel = the ground the two sit on. Green/gold/red stay health-only (WaitBrush is the only sanctioned gold). EmberThemeProbe / EmberTheme.ApplyAccent swap the theme-scoped pairs on a light/dark flip.

Theme is applied at runtime via Wpf.Ui.Appearance.ApplicationThemeManager.Apply(...) (Light / Dark) or ApplySystemTheme(). The Theme="Dark" attribute on ThemesDictionary is the design-time default.

App-level control styles (#175):

Key (or implicit target) What it does
implicit ComboBox / Button / CheckBox / TextBox / uictrl:TextBox 30-32px grid metrics, ember text selection, faint neutral hover glow
implicit ToolTip / uictrl:FluentWindow Body font, left-aligned tooltip text, dialog font inheritance
implicit uictrl:DynamicScrollBar Always-visible track + thumb (stock template fades them in on hover)
EmberIconButton / EmberIconButtonHot 28x28 icon micro-buttons, cold ring for input actions, ember ring for output-writing actions
EmberAccentButton / EmberPrimaryButton Ember rim + text affirmative verb (plain Button / uictrl:Button)
EmberDestructiveButton Ember-deep rim + text for destructive confirms
EmberSelectListItem Retemplated ListBoxItem: ember-tint wash + #66FF6B2C rim on select
EmberSlider Retemplated single-thumb slider with an ember decrease-side fill
InstrumentBarRaw / InstrumentBarOut RAW (cold-deep flat) / OUT (ember gradient + glow) telemetry ProgressBar retemplates
EmberFocusVisual (+ DefaultControlFocusVisualStyle, FocusVisualStyleKey) Ember keyboard-focus ring app-wide
EntranceFade 150ms fade + 6px settle on Loaded for page/card roots

ControllerIcons.xaml

File: PadForge.App/Resources/ControllerIcons.xaml

DrawingImage icons (sidebar, dashboard, profiles):

Key Source Description
XboxControllerIcon svgrepo.com (32x32) Xbox logo icon
DS4ControllerIcon svgrepo.com (32x32) PlayStation logo icon
NintendoControllerIcon Switch logo (32x32 box) Nintendo family icon
ExtendedControllerIcon svgrepo.com, viewBox -4 -2 24 24 Joystick icon
GenericControllerIcon svgrepo.com (512x512, scaled) Generic gamepad with D-pad and face buttons
KeyboardMouseControllerIcon placeholder path Placeholder so resource lookup succeeds. KB+M actually renders via an MDL2 glyph in code-behind. There is no MidiControllerIcon. MIDI uses the E8D6 MDL2 glyph directly.

Fills use DynamicResource TextFillColorPrimaryBrush. The GenericControllerIcon cutouts (D-pad, face buttons) use SolidBackgroundFillColorSecondaryBrush.

Hover-glow effects (frozen DropShadowEffect, shared by every hover trigger and code-behind site): EmberHoverGlow, EmberHoverGlowSmall, ColdHoverGlow, NeutralHoverGlow. Static Effect setters only, never animated (Effect animation from style triggers crashes at startup).

PadPage tab-icon geometries (Sticks/Triggers tab headers, derived from Zacksly Xbox Series icons, CC BY 3.0): ZackslyStickOuterRingGeometry, ZackslyStickInnerDiscGeometry, ZackslyTriggerOuterBodyGeometry, ZackslyTriggerOuterBodyRightGeometry, plus TouchpadStickIconGeometry (Segoe Fluent Icons F108 silhouette).

Shared card and section styles:

Key TargetType Properties
CardBorder Border CardBackgroundFillColorDefaultBrush background, 8px corner radius, 16px padding, 12px bottom margin, 1px #253049 stroke that heats to steel-lift on hover (theme-scoped via EmberThemeProbe)
CardTitle TextBlock 15px Display face, SemiBold
CardSectionTitle TextBlock CardTitle + 8px bottom margin (divider-separated section title inside a card)
CardSubsectionTitle TextBlock SemiBold at body size, 6px bottom margin (one rank below CardTitle)
CardDescription TextBlock 12px Body face, TextFillColorSecondaryBrush, wrap enabled, 8px bottom margin
SectionGlyph TextBlock Ember MDL2 glyph, 13px, for page section headers
SectionTitle TextBlock 10px Telemetry mono eyebrow, SemiBold, tertiary
ChipCold / ChipEmber / ChipGhost Border Artifact chips: cold = physical fact, ember = virtual fact, ghost = neutral

Theme Switching

SettingsViewModel.SelectedThemeIndex maps to: - 0 = System Default (follows Windows setting) - 1 = Light - 2 = Dark

Applied in MainWindow.OnThemeChanged via Wpf.Ui.Appearance.ApplicationThemeManager.Apply(ApplicationTheme.Light|Dark) or ApplySystemTheme(), followed by EmberTheme.ApplyAccent() and UpdateSteelLayer(). The KBM, MIDI, and mouse previews compare ApplicationThemeManager.GetAppTheme() on each rendered frame and rebuild their visuals when it changed. Only the Add Controller popup and the profile switcher popup subscribe to ApplicationThemeManager.Changed, to repaint their backgrounds while open.

Ember Identity Brushes

Defined inline in the App.xaml ember-tokens dictionary. Values below are the dark defaults. EmberTheme.ApplyAccent swaps the theme-paired ones on a light/dark flip.

Brush Key Color Usage
EmberBrush / EmberHotBrush / EmberDeepBrush #FF6B2C / #FFA24D / #C43D0C Virtual side: outputs, live state, accent verbs
ColdBrush / ColdDeepBrush / ColdMutedBrush #58B6E4 / #2E6A8F / #9E58B6E4 Physical side: devices, sources, telemetry
SteelGroundBrush / SteelRaisedBrush / SteelLineBrush / SteelLineSoftBrush #0B0E14 / #1B2333 / #253049 / #1C2536 Ground, raised, hairline, soft hairline
CrucibleCardBrush #111623 gradient Slot-card ground. EmberTheme swaps it per theme, and light falls back to the card fill. There is no SteelCardBrush key
EmberTintBrush / ColdTintBrush #1AFF6B2C / #1A58B6E4 Checked-tab and chip washes
EmberTextBrush #FF6B2C Raw-ember text that deepens on white. Theme-paired
WaitBrush #E8B434 The only sanctioned gold: cooling flames, awaiting-devices

The RangeSliderThumbFill brush from the v3 layout no longer exists.


Common XAML Patterns

Card-Based Layout

Settings and driver pages use this card pattern:

<Border Style="{StaticResource CardBorder}">
    <StackPanel>
        <StackPanel Orientation="Horizontal" Margin="0,0,0,4">
            <TextBlock Text="&#xE790;" FontFamily="Segoe MDL2 Assets" FontSize="16"
                       VerticalAlignment="Center" Margin="0,0,8,0"/>
            <TextBlock Text="Card Title" Style="{StaticResource CardTitle}"/>
        </StackPanel>
        <TextBlock Text="Description text." Style="{StaticResource CardDescription}"/>
        <!-- Card content -->
    </StackPanel>
</Border>

Liveness Flame Indicator

Online/offline state renders as a flame Path, not a colored dot. The LivenessFlameBase style (App.xaml) draws an outline-only flame at rest. Each site adds one DataTrigger on IsOnline that fills it ember with a glow:

<Path>
    <Path.Style>
        <Style TargetType="Path" BasedOn="{StaticResource LivenessFlameBase}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsOnline}" Value="True">
                    <Setter Property="Fill" Value="{DynamicResource EmberBrush}"/>
                    <Setter Property="Stroke" Value="{x:Null}"/>
                    <Setter Property="Effect">
                        <Setter.Value>
                            <DropShadowEffect Color="#FF6B2C" BlurRadius="8" ShadowDepth="0" Opacity="0.5"/>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Path.Style>
</Path>

The engine-state indicator in the status bar uses the same flame with DataTriggers on EngineStateKey instead (ember when Running, gold when Idle/Stopping).

Segoe MDL2 Asset Icons

Segoe MDL2 Assets font glyphs instead of image resources:

<TextBlock Text="&#xE713;" FontFamily="Segoe MDL2 Assets" FontSize="20"/>

Codes used: E713 settings, E790 personalization, E9F5 processing, E737 star, ED1A shield, E7FC gamepad, E8A5 save, E9D9 bug, E8F1 group, E8B7 library, E8B9 photo, F158 3D, E946 info, E772 devices, E7E8 power, E740 full screen, E710 add, E711 close, E72A forward, E72C undo, E700 global nav (hamburger), E8D6 music, EC4F MIDI, E961 keyboard, E774 globe, EDA4 touchpad, F119 VR headset, F2B7 language, E8C8 copy, E8D7 document, E7C3 page, E74C checkmark, E71B link, E75C clear, E74D delete, F404 home, E7BA warning, and the section and card glyphs named on this page: E702 Pair, E707 Compass Yaw, E716 Community Configs, E767 volume, E77B Head Tracking, E781 Lighting and the Lighting vendor rows, E7F8 Handheld PC Buttons, E815 Grip, E83F Battery Alerts, E877 Force Feedback, the rumble chip and the Haptics vendor row, E8DE Assignment Prompts, E969 Remote Link, E99A Tilt, E9E9 Gyro Sensitivity, EB9F Icon Packages, EFA5 touchpad chip, F0AD Constant Force, F1CB Gyro Response, F272 Gyro Calibration. EC05 (broadcast) and ED5D (driver) left with the Dashboard's driver status strip and appear nowhere in the app.

WPF UI NumberBox

Numeric input with inline spin buttons:

<ui:NumberBox Value="{Binding PollingRateMs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
              Minimum="1" Maximum="16"
              SpinButtonPlacementMode="Inline" Width="200"/>

Every NumberBox binds with UpdateSourceTrigger=PropertyChanged. WPF UI registers Value to write back on LostFocus, and the spin buttons step the value without focusing the box, so a box bound without the trigger saved nothing from its arrows until it was clicked into and out of again (#511). The control sets Value only on Enter, on leaving the box, on a paste and on a step, so typing still saves once. A box bound to a port adds Delay=1000, because a port change restarts its server: a held arrow then restarts it once, a second after the last step. NumberBoxCommitTests holds every box in the app to both rules.

Localized String Bindings

All user-facing text uses localized bindings via Strings.Instance:

<TextBlock Text="{Binding Settings_Title, Source={x:Static strings:Strings.Instance}}"/>

Enables runtime language switching without restart.

MarqueeBehavior for Overflow Text

Long text (device names, GUIDs, paths) uses MarqueeBehavior.IsEnabled="True" inside a ClipToBounds="True" Border for scrolling overflow.


Code-Behind Patterns

Bind/Unbind Pattern

All six visualization views (3D, 2D, Schematic, MIDI, KBM, VR) share this interface:

public void Bind(PadViewModel vm)    // Subscribe to PropertyChanged, hook rendering, load model
public void Unbind()                 // Stop flash, unhook rendering, clear VM reference

Lets PadPage switch views cleanly when output type or preference changes.

CompositionTarget.Rendering with Dirty Flag

Per-frame visual updates via CompositionTarget.Rendering, gated by a _dirty flag:

private bool _dirty;

private void OnRendering(object sender, EventArgs e)
{
    if (!_dirty || _vm == null) return;
    _dirty = false;
    // Update visuals...
}

private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    _dirty = true;  // Coalesce multiple property changes into one render frame
}

Batches multiple property changes into one visual update per frame, avoiding redundant work.

DispatcherTimer Flash Animation

"Map All" recording flow across all views:

private DispatcherTimer _flashTimer;
private string _flashTarget;
private bool _flashOn;

private void UpdateFlashTarget(string target)
{
    // Start or stop flash timer based on target
    // Timer callback toggles highlight/default materials at 400ms interval
}

The 3D, 2D, schematic, KBM, and MIDI views tick at 400 ms. The VR preview ticks at 450 ms.

Event Relay Pattern

Code-behind raises events that MainWindow.xaml.cs wires to services, keeping views decoupled:

// DashboardPage.xaml.cs
public event EventHandler<int> DeleteSlotRequested;
private void DeleteSlot_Click(object sender, RoutedEventArgs e)
{
    if (sender is Button btn && btn.Tag is int slotIndex)
        DeleteSlotRequested?.Invoke(this, slotIndex);
}

// MainWindow.xaml.cs (wiring, abridged)
DashboardPageView.DeleteSlotRequested += (s, slotIndex) =>
    Dispatcher.BeginInvoke(new Action(() =>
    {
        bool hadActiveVc = _inputService.IsHmVcAt(slotIndex);
        var info = _deviceService.DeleteSlot(slotIndex);
        _inputService.OnSlotDeleted(slotIndex, info.Type, info.OldGroupPosition,
            deletedSlotHadActiveVc: hadActiveVc);
    }));

Syncing Guard Pattern

Prevents recursive updates when programmatically setting control values:

private bool _syncingExtendedConfig;

private void SyncExtendedConfigBar()
{
    if (DataContext is not PadViewModel vm) return;

    bool isExtended = vm.OutputType == Engine.VirtualControllerType.Extended;
    bool isKbm = vm.OutputType == Engine.VirtualControllerType.KeyboardMouse;

    HMaestroProfileBar.Visibility = ((vm.HasHMaestroProfileBar && !isExtended) || isKbm)
        ? Visibility.Visible
        : Visibility.Collapsed;
    HMaestroProfileCombo.Visibility = isKbm ? Visibility.Collapsed : Visibility.Visible;
    KbmSurfacesCombo.Visibility = isKbm ? Visibility.Visible : Visibility.Collapsed;
    ExtendedConfigBar.Visibility = isExtended ? Visibility.Visible : Visibility.Collapsed;

    if (isExtended)
    {
        _syncingExtendedConfig = true;
        SyncExtendedFields(vm);  // populates profile combo + Customize-gated overrides
        _syncingExtendedConfig = false;
    }
}

private void ExtendedCustomize_Toggled(object sender, RoutedEventArgs e)
{
    if (_syncingExtendedConfig) return;  // Skip when syncing programmatically
    if (DataContext is not PadViewModel vm || vm.ExtendedConfig == null) return;
    vm.ExtendedConfig.Customize = ExtendedCustomizeChk.IsChecked == true;
}

See Also

  • Architecture Overview: Application shell, page hosting, WPF UI theme
  • ViewModels: PadViewModel, DashboardViewModel, DevicesViewModel, SettingsViewModel
  • Services Layer: InputService, SettingsService, DeviceService wired in MainWindow.xaml.cs
  • 2D Overlay System: ControllerModel2DView, ControllerSchematicView, KBMPreviewView, MidiPreviewView, VRPreviewView
  • 3D Model System: ControllerModelView (HelixToolkit 3D viewport)
  • Settings and Serialization: PadSetting descriptors driving mapping grid UI
  • Peripheral Outputs Internals: The links, claims and backend states the Lighting and Force Feedback route lines read
  • Virtual Controllers: Output type selection UI for Xbox, PlayStation, Nintendo, Extended, KB+M, MIDI, VR (all HM-backed types are produced by HMaestroVirtualController, VR by HMaestroVRController). The Add Controller popup builds a Nintendo button (Switch logo, AutomationId AddNintendoBtn, capacity via MaxNintendoSlots) between PlayStation and Extended, and a VR button (F119 glyph, AutomationId AddVrBtn, capacity via MaxVrSlots = 1) at the tail, the VirtualControllerGroups.InOrder visual order.
  • Driver Installation Internals: HidHide install/uninstall and the Windows MIDI Services runtime's install and uninstall, triggered from SettingsPage (HIDMaestro is embedded. OpenXInput's xinput1_4.dll unpacks into the single-file extraction directory under %TEMP%\.net\PadForge, which App.OnStartup adds to the DLL search path)

Last updated for PadForge 5.2.0.