Skip to content

XAML Views

hifihedgehog edited this page Mar 19, 2026 · 36 revisions

XAML Views Reference

All views live in PadForge.App/Views/ under the PadForge.Views namespace (except MainWindow.xaml which is in PadForge.App/). The app uses ModernWpfUI for Windows 10/11 Fluent Design styling.


Application Shell (MainWindow)

Files: MainWindow.xaml, MainWindow.xaml.cs

The application shell. Contains the NavigationView sidebar, page content area, status bar, and driver overlay.

XAML Structure

Two-row Grid:

  • Row 0 (star): NavigationView containing all page containers.
  • Row 1 (auto): Status bar Border.
  • Overlay (ZIndex=1000): Driver operation overlay (blocks UI during install/uninstall).
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>    <!-- NavigationView -->
        <RowDefinition Height="Auto"/> <!-- Status bar -->
    </Grid.RowDefinitions>

    <ui:NavigationView x:Name="NavView" PaneDisplayMode="Left" PaneTitle="PadForge" OpenPaneLength="207"
                       IsBackButtonVisible="Collapsed" IsSettingsVisible="True">
        <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>

    <Border Grid.Row="1"> <!-- Status bar --> </Border>
    <Grid x:Name="DriverOverlay" Grid.RowSpan="2" Panel.ZIndex="1000"> <!-- ... --> </Grid>
</Grid>

Navigation Model

PadForge does not use WPF Frame-based navigation. All pages are instantiated once in the XAML Grid and visibility-swapped:

  1. NavView_SelectionChanged reads the selected item's Tag string.
  2. All page containers set to Visibility.Collapsed.
  3. The matching page set to Visibility.Visible.
  4. For controller slots (tag "Pad:{index}"), the PadPage's DataContext is set to the corresponding PadViewModel.

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

Sidebar Construction

The sidebar is built programmatically in BuildNavigationItems(). Fixed items:

Tag Content Icon
Dashboard Dashboard Home SymbolIcon
Devices Devices Gamepad SymbolIcon
Profiles Profiles People SymbolIcon
About About Help SymbolIcon (footer)
Settings (built-in) Built-in gear icon

Dynamic controller cards are inserted between "Devices" and "Profiles" via RebuildControllerSection(). Each card is a NavigationViewItem with custom Content containing:

  • Power/type glyph (Xbox/DS4/vJoy icon)
  • Slot label ("Controller 1", etc.)
  • Device name subtitle
  • Delete button (visible on hover)

RebuildControllerSection() is called when slots are created, deleted, or reordered. It uses a _rebuildingControllerSection guard to prevent re-entrancy during NavigationView selection changes.

Sidebar Drag Reordering

Users can drag controller cards to reorder virtual controller slots:

  1. OnCardDragStartPreviewMouseLeftButtonDown records start position.
  2. OnNavViewDragMovePreviewMouseMove checks distance threshold, then BeginCardDrag() creates a CardDragAdorner (ghost preview) and InsertionLineAdorner (drop indicator line).
  3. UpdateDragPosition — Updates adorner positions, computes target insertion index.
  4. EndCardDragPreviewMouseLeftButtonUp completes the swap. Calls SettingsManager.SwapSlots() and EnsureTypeGroupOrder().

Cross-Panel Device Drag-Drop

Devices can be dragged from the Devices page card list to a sidebar controller card:

  • DevicesPage initiates DragDrop.DoDragDrop() with DeviceRowViewModel as the data payload.
  • Sidebar NavigationViewItem handlers (DragOver, Drop) accept the drop and assign the device to that slot.

Add Controller Popup

A Popup with buttons for Xbox 360, DualShock 4, vJoy, MIDI, and Keyboard+Mouse. Per-type buttons disabled at capacity:

  • Opacity 0.35 and "(max N)" tooltip when limit reached.
  • HasAnyControllerTypeCapacity() counts per-type from Pads[].OutputType.
  • All types share the global limit: MaxXbox360Slots = MaxDS4Slots = MaxVJoySlots = MaxMidiSlots = MaxKeyboardMouseSlots = 16.

Status Bar

Bottom Border with four columns:

  1. Status textStatusText binding, trimmed with CharacterEllipsis.
  2. Device countConnectedDeviceCount with "device(s)" suffix.
  3. Polling frequencyPollingFrequency formatted as "{0:F0} Hz".
  4. Engine indicator — Green/gray dot (BoolToColorConverter) + EngineStatusText.

Driver Overlay

Semi-transparent overlay shown during driver install/uninstall operations:

  • ProgressRing spinner + text message.
  • Blocks all UI interaction (ZIndex=1000, Grid.RowSpan="2").
  • Shown/hidden by RunDriverOperationAsync().

Composition Root (Code-Behind)

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

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

Timer Architecture

Timer Interval Purpose
DispatcherTimer 33ms (~30Hz) Calls InputService.UpdateUI() to push engine state into ViewModels
_driverStatusTimer 5s Polls ViGEm/vJoy driver status for hot-plug detection
CompositionTarget.Rendering ~60fps Used by 3D/2D/Schematic views for per-frame visual updates

DashboardPage

Files: DashboardPage.xaml, DashboardPage.xaml.cs

Overview page with engine toggle, slot summary cards, DSU/Web settings, and driver status.

Layout Structure

ScrollViewer (Padding="24,16")
  └─ StackPanel
       ├─ Page header (icon + title)
       ├─ "Input Engine" section header
       ├─ CardBorder: Engine status card (Grid, 4 columns)
       │   ├─ Col 0: Power toggle button (E7E8 icon, color-coded)
       │   ├─ Col 1: EngineStatus text
       │   ├─ Col 2: PollingFrequencyText
       │   └─ Col 3: Online/Total devices count
       ├─ "Virtual Controllers" section header
       ├─ ItemsControl (WrapPanel) → SlotSummaries
       │   └─ DataTemplate: slot card Border (220px wide)
       │       ├─ Row 0: Power btn + Gamepad icon + Slot # + Type buttons (Xbox/PS/vJoy/KBM/MIDI) + Instance label
       │       ├─ Row 1: DeviceName (marquee overflow)
       │       └─ Row 2: StatusText + mapped/connected counts
       ├─ "Add Controller" card (MouseLeftButtonUp → AddControllerRequested)
       ├─ "Motion Server" section (DSU)
       │   └─ CardBorder: Enable toggle, port NumberBox, status dot, footer
       ├─ "Web Controller" section
       │   └─ CardBorder: Enable toggle, port NumberBox, status dot, footer
       └─ "Drivers" section
           └─ CardBorder (Grid, 4 rows × 2 cols)
               ├─ Row 0: HidHide status
               ├─ Row 1: MIDI Services status
               ├─ Row 2: ViGEmBus status
               └─ Row 3: vJoy status

Key Bindings

Binding ViewModel Description
EngineStateKey DashboardViewModel Color-codes engine toggle: "Running"=green, "Idle"=yellow, default=red
EngineStatus DashboardViewModel Status text next to engine button
PollingFrequencyText DashboardViewModel e.g. "998 Hz"
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
EnableWebController DashboardViewModel Web controller enable checkbox
WebControllerPort DashboardViewModel Web controller port
HidHideStatusText / ViGEmStatusText / VJoyStatusText / MidiServicesStatusText DashboardViewModel Driver status text

Slot Card DataTemplate Bindings (SlotSummary)

Binding Type Description
PadIndex int Used as Tag for button click routing
SlotNumber string Global slot display number
IsEnabled bool Controls power toggle color
OutputType VirtualControllerType Selects which type button is highlighted (Opacity 1.0 vs 0.3)
TypeInstanceLabel string Per-type instance number
DeviceName string Physical device name (marquee on overflow)
StatusText string e.g. "Active", "Disabled"
MappedDeviceCount / ConnectedDeviceCount int Mapped/connected counts
IsInitializing bool Triggers green opacity flash animation

Power Toggle State Machine

The power icon (E7E8) uses multi-condition DataTriggers:

Condition Color Tooltip
IsEnabled=False Red #FFF44336 "Disabled"
IsEnabled=True Green #FF4CAF50 "Active"
IsEnabled=True + ConnectedDeviceCount=0 Yellow #FFFFC107 "Awaiting Controllers"
IsEnabled=True + IsViGEmInstalled=False Yellow "ViGEm Not Installed"
IsEnabled=True + EngineStateKey="Stopped" Yellow "Engine Stopped"
IsInitializing=True Green (flashing) "Initializing"

Type Switch Buttons

Each slot card has 5 type buttons (Xbox, PS, vJoy, KBM, MIDI). The active type shows at Opacity 1.0; inactive at 0.3. Driver-dependent types (Xbox/DS4 require ViGEm, vJoy requires vJoy) are disabled at 0.15 opacity when the driver is not installed.

UI Automation

AutomationId Element Purpose
EnableWebControllerCheckBox CheckBox Web controller enable toggle

Event Handlers (Code-Behind)

Handler Event Action
EngineToggle_Click Button.Click Raises EngineToggleRequested
AddControllerCard_Click Border.MouseLeftButtonUp Raises AddControllerRequested
DeleteSlot_Click Button.Click Raises DeleteSlotRequested(slotIndex)
PowerToggle_Click Button.Click Raises SlotEnabledToggled(slotIndex, !IsEnabled)
XboxType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Xbox360) — guarded by IsViGEmInstalled
DS4Type_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, DualShock4) — guarded by IsViGEmInstalled
VJoyType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, VJoy) — guarded by IsVJoyInstalled
KeyboardMouseType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, KeyboardMouse)
MidiType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Midi)
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

Dashboard Card Drag Reordering

Slot cards support drag to reorder (same adorner system as sidebar). Includes:

  • CardDragAdorner — ghost preview rendered from RenderTargetBitmap snapshot.
  • InsertionLineAdorner — vertical accent-colored 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 is blocked; only same-type reordering allowed.
  • Events: SlotSwapRequested(PadIndexA, PadIndexB) and SlotMoveRequested(SourcePadIndex, TargetVisualPos).

PadPage

Files: PadPage.xaml, PadPage.xaml.cs

Configuration page for a single virtual controller slot. Contains a custom tab strip, optional config bars, and 6 tab panels.

Layout Structure

Grid (3 rows)
├─ Row 0 (Auto): Tab strip + device selector (Border with WrapPanel)
│   ├─ RadioButton "Controller" (Tag=0, AutomationId="TabController")
│   ├─ RadioButton "Macros" (Tag=1)
│   ├─ TabStripSeparator
│   ├─ ComboBox (MappedDevices dropdown with online status dots)
│   ├─ TabStripSeparator
│   ├─ RadioButton "Mappings" (Tag=2, AutomationId="MappingsTab")
│   ├─ RadioButton "Sticks" (Tag=3, x:Name="TabSticks")
│   ├─ RadioButton "Triggers" (Tag=4, x:Name="TabTriggers")
│   └─ RadioButton "Force Feedback" (Tag=5, x:Name="TabForceFeedback")
├─ Row 1 (Auto): VJoy config bar OR MIDI config bar (conditionally visible)
│   ├─ VJoyConfigBar (Visibility=Collapsed unless OutputType==VJoy)
│   └─ MidiConfigBar (Visibility=Collapsed unless OutputType==Midi)
├─ Invisible MappingsCountIndicator (for UI Automation)
└─ Row 2 (*): TabControl (hidden header via ControlTemplate)
    ├─ TabItem 0: Controller
    ├─ TabItem 1: Macros
    ├─ TabItem 2: Mappings
    ├─ TabItem 3: Sticks
    ├─ TabItem 4: Triggers
    └─ TabItem 5: Force Feedback

Custom Styles (UserControl.Resources)

Style Key TargetType Properties
DzLabel TextBlock Width=200, centered, theme foreground. Used for slider row labels.
DzSlider Slider Min=0, Max=100, Width=200, SnapToTick=True, TickFrequency=0.1.
OffsetSlider Slider Extends DzSlider with Min=-100 (for center offset).
DzValue TextBlock Right-aligned, medium brush, margin 4,0. Read-only value display.
DzValueEdit TextBox Width=48, right-aligned, editable percentage value.
DzDigitEdit TextBox Width=52, right-aligned, tooltip "Raw axis value", black foreground.
DzPercent TextBlock "%" suffix text, medium brush.
ResetButton Button Glyph E72C (undo arrow), FontSize=10, tooltip "Reset". Per-row reset.
ResetAllButton Button Padding=8,3, FontSize=11, left-aligned. Section-level reset.
TabStripButton RadioButton Custom ControlTemplate with 2px bottom border accent on checked, hover highlight. GroupName="PadTab".
TabStripSeparator TextBlock Right-arrow glyph E76C, 10px, used between tab groups.

Custom Tab Strip

Horizontal bar of RadioButton controls styled via TabStripButton. Each RadioButton:

  • Uses GroupName="PadTab" for mutual exclusion.
  • Stores the tab index in its Tag property (0-5).
  • Click handler calls vm.SelectedConfigTab = idx.

A hidden TabControl header via custom ControlTemplate (only PART_SelectedContentHost ContentPresenter) provides the actual content switching. The TabControl's SelectedIndex is bound to SelectedConfigTab.

Tab Visibility Rules

Tabs are conditionally hidden based on output type:

Tab Xbox/DS4/vJoy KB+Mouse MIDI
Controller Visible Visible Visible
Macros Visible Visible Visible
Mappings Visible Visible Visible
Sticks Visible Visible (Mouse X/Y + Scroll) Hidden
Triggers Visible Hidden Hidden
Force Feedback Visible Hidden Hidden

When a hidden tab was selected, the view auto-switches to the Controller tab (index 0). SyncTabVisibility() also hides/shows the motor activity bars (MotorBarsGrid).

Controller Tab Type Icon

The Controller tab header displays a dynamic type icon via nested DataTriggers:

  • Xbox360XboxControllerIcon (Image)
  • DualShock4DS4ControllerIcon (Image)
  • VJoyVJoyControllerIcon (Image)
  • MidiE8D6 glyph (music note, TextBlock, Image collapsed)
  • KeyboardMouseE961 glyph (keyboard, TextBlock, Image collapsed)

The TypeInstanceLabel binding shows the per-type instance number (e.g., "Xbox 360 1").

Multi-Device Selector

Inline ComboBox in the tab strip, bound to MappedDevices with SelectedMappedDevice. Each item shows an online status dot (green #4CAF50 / gray #888888) and device Name. Visible regardless of device count, positioned between "Macros" and "Mappings" tabs with TabStripSeparator arrows.

UI Automation Properties

AutomationId Element Purpose
TabController RadioButton (tab 0) Controller tab identification
MappingsTab RadioButton (tab 2) Mappings tab identification
VJoyPresetCombo ComboBox vJoy preset selection
VJoyStickCountBox TextBox vJoy custom thumbstick count
VJoyTriggerCountBox TextBox vJoy custom trigger count
VJoyPovCountBox TextBox vJoy custom POV count
VJoyButtonCountBox TextBox vJoy custom button count
MappingsCountIndicator TextBlock (invisible) AutomationProperties.Name bound to Mappings.Count
DeadZoneShapeCombo ComboBox Dead zone shape selector (Sticks tab)
SensitivityXCombo ComboBox Sensitivity X preset (Sticks tab)
TriggerPresetCombo ComboBox Trigger sensitivity preset (Triggers tab)

Event Handlers (Code-Behind)

Handler Trigger Action
PadPage_Loaded UserControl.Loaded Calls ApplyViewMode, SyncTabStripSelection, SyncVJoyConfigBar, SyncMidiConfigBar
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 StackPanel.Mouse Hover opacity effect (0.7/1.0)
LeftMotor_Click StackPanel.MouseLeftButtonDown padVm.FireTestLeftMotor()
RightMotor_Click StackPanel.MouseLeftButtonDown padVm.FireTestRightMotor()
MapAllStop_Click Button.Click padVm.StopMapAll()
CalibrateCenter_Click Button.Click StickConfigItem.StartCalibration()
VJoyPresetCombo_SelectionChanged ComboBox.SelectionChanged Re-automaps slot, sets VJoyConfig.Preset, syncs custom fields
VJoyCustomValue_Changed TextBox.LostFocus Applies clamped vJoy custom values
VJoyCustomValue_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
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

Controller Tab (Tab 0) — Detailed

Grid (3 rows)
├─ Row 0 (*): Controller visualization area
│   ├─ ControllerModelView (3D, HelixToolkit)
│   ├─ ControllerModel2DView (2D overlays, Collapsed by default)
│   ├─ ControllerSchematicView (procedural vJoy, Collapsed)
│   ├─ MidiPreviewView (MIDI, Collapsed)
│   ├─ KBMPreviewView (KB+Mouse, Collapsed)
│   └─ ViewModeToggle button (top-left, E8B9↔F158 icon)
├─ Row 1 (Auto): Motor activity bars (MotorBarsGrid, 500px wide, centered)
│   ├─ Col 0: Left Motor label + bar (NormToTriggerHeightConverter, param 240)
│   └─ Col 1: Right Motor label + bar
└─ Row 2 (Auto): Map All controls
    ├─ "Map All" Button (MapAllCommand)
    ├─ "Stop" Button (visible when IsMapAllActive)
    └─ MapAllPromptText (blue, SemiBold)

View Switching Logic (ApplyViewMode):

Output Type vJoy Preset User Pref Active View Toggle Visible
KeyboardMouse KBMPreviewView No
Midi MidiPreviewView No
VJoy Custom ControllerSchematicView No
VJoy Xbox 360/DS4 2D ControllerModel2DView Yes
VJoy Xbox 360/DS4 3D ControllerModelView Yes
Xbox360 2D ControllerModel2DView Yes
Xbox360 3D ControllerModelView Yes
DualShock4 2D ControllerModel2DView Yes
DualShock4 3D ControllerModelView Yes

BindActiveModelView() wires the active view:

  1. Unbinds all five views (3D, 2D, Schematic, MIDI, KBM).
  2. Subscribes the active view's ControllerElementRecordRequested event.
  3. Calls Bind(vm) on the active view.

All five views fire ControllerElementRecordRequested with a PadSetting target name string for click-to-record.

Motor Activity Bars — Horizontal fill bars bound to LeftMotorDisplay/RightMotorDisplay (0-1 normalized), using NormToTriggerHeightConverter with param=240 for pixel width. Clickable for quick motor test (LeftMotor_Click/RightMotor_Click). Hover dims to 0.7 opacity.

Macros Tab (Tab 1) — Detailed

Grid (3 columns)
├─ Col 0 (250px): Macro list panel
│   ├─ DockPanel.Top: Add/Remove buttons
│   └─ ListBox (Macros, DisplayMemberPath="Name")
├─ Col 1 (Auto): GridSplitter (4px, draggable)
└─ Col 2 (*): Macro editor (ScrollViewer)
    └─ StackPanel (DataContext=SelectedMacro)
        ├─ Name TextBox (UpdateSourceTrigger=PropertyChanged)
        ├─ Enabled CheckBox
        ├─ Trigger Mode ComboBox (OnPress/OnRelease/WhileHeld/Always)
        ├─ Trigger Combination panel (hidden when Always mode)
        │   ├─ Trigger Source ComboBox (InputDevice/OutputController)
        │   ├─ Trigger display/recording text + Record button
        │   ├─ Recording hint text
        │   ├─ Axis threshold slider (1-100%, visible when UsesAxisTrigger)
        │   ├─ Axis direction ComboBox (Any/Positive/Negative, visible when UsesAxisTrigger)
        │   └─ Consume trigger buttons CheckBox
        ├─ Always mode description note
        ├─ Separator
        └─ Action Sequence section
            ├─ Add Action / Remove buttons
            ├─ Actions ListBox (DisplayMemberPath="DisplayText")
            └─ Action editor Border (DataContext=SelectedAction)
                ├─ Action Type ComboBox (12 types)
                └─ Type-specific panels (conditional visibility):

Macro Trigger Section:

Field Binding Visibility
Fire mode dropdown TriggerMode (SelectedValue) Always
Trigger source TriggerSource Hidden in Always mode (IsNotAlwaysMode)
Trigger display TriggerDisplayText / RecordingLiveText Hidden in Always mode
Record button RecordTriggerCommand / RecordTriggerButtonText Hidden in Always mode
Axis threshold TriggerAxisThreshold (Slider 1-100%) UsesAxisTrigger
Axis direction TriggerAxisDirectionIndex (Any/Positive/Negative) UsesAxisTrigger
Consume trigger ConsumeTriggerButtons (CheckBox) Hidden in Always mode

Action Type Editor Panels:

Action Type Visible Panel Key Controls
ButtonPress / ButtonRelease IsButtonType WrapPanel of CheckBox items from ButtonOptions
KeyPress / KeyRelease IsKeyType KeyString TextBox (Consolas font), VirtualKey ComboBox picker, Clear button
ButtonPress / KeyPress / Delay IsDurationType DurationMs TextBox + "ms" label
AxisSet IsAxisType Axis target ComboBox (LStickX/Y, RStickX/Y, LT, RT) + AxisValue TextBox
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 IsMouseButtonType Mouse button ComboBox (Left/Right/Middle/X1/X2)

Device Axis Picker (shared by SystemVolume, AppVolume, MouseMove):

  • Device ComboBox: DropDownOpened="DeviceAxisPicker_DropDownOpened" dynamically populates from devices assigned to current slot.
  • Axis index ComboBox: DropDownOpened="DeviceAxisIndexPicker_DropDownOpened" populates with IsAxis DeviceObjects from selected device.
  • Uses AxisPickerItem wrapper class with InputIndex and localized DisplayName.

Mappings Tab (Tab 2) — Detailed

Grid (2 rows)
├─ Row 0 (Auto): Toolbar
│   ├─ "Clear All" Button (ClearMappingsCommand)
│   ├─ "Copy" Button (CopySettingsCommand)
│   ├─ "Paste" Button (PasteSettingsCommand)
│   ├─ "Copy From" Button (CopyFromCommand)
│   ├─ "Map All" Button (MapAllCommand)
│   └─ Hint text (italic, medium brush)
└─ Row 1 (*): DataGrid
    ├─ Column: "Output" (TargetLabel text, 195px)
    ├─ Column: "Source" (ComboBox dropdown, 215px)
    ├─ Column: "Value" (CurrentValueText, 60px)
    ├─ Column: Record button (ToggleRecordCommand, 100px)
    ├─ Column: Clear button (ClearCommand, 100px)
    └─ Column: "Options" (Invert + Half checkboxes, 220px)

DataGrid Properties:

  • AutoGenerateColumns="False", CanUserAddRows="False", CanUserDeleteRows="False", CanUserReorderColumns="False", IsReadOnly="True".
  • Row style: transparent background with blue flash animation (ColorAnimation from #002196F3 to #602196F3, 400ms, AutoReverse, Forever) when IsRecording=True.

Source Column ComboBox:

  • ItemsSource="{Binding AvailableInputs}" — populated per mapping row with available physical inputs.
  • SelectedItem="{Binding SelectedInput, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}".
  • DisplayMemberPath="DisplayName".

Options Column:

  • Invert CheckBox — IsInverted binding.
  • Half CheckBox — IsHalfAxis binding.

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)
              │   ├─ Dead Zone Shape ComboBox (6 shapes)
              │   ├─ Dead Zone X (DzSlider + % edit + digit edit + reset)
              │   ├─ Dead Zone Y (DzSlider + % edit + digit edit + reset)
              │   ├─ Anti-Dead Zone X (DzSlider + % edit + digit edit + reset)
              │   ├─ Anti-Dead Zone Y (DzSlider + % edit + digit edit + reset)
              │   ├─ Linear (DzSlider + % edit + reset)
              │   ├─ "Sensitivity Curves" header + hint text
              │   ├─ Sensitivity X (preset ComboBox + reset)
              │   ├─ Sensitivity Y (preset ComboBox + reset)
              │   ├─ 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)
                  │   ├─ Dead zone 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
                  │   └─ Blue stick position dot (11px, NormToCanvasConverter)
                  ├─ RawDisplay text (centered, wrapping)
                  └─ CurveEditor pair (X-axis + Y-axis, 96px each)
                      ├─ CurveEditor X: CurveString=SensitivityCurveX, DeadZone/MaxRange bindings
                      └─ CurveEditor Y: CurveString=SensitivityCurveY, DeadZone/MaxRange bindings

Dead Zone Shape Options (ComboBox index):

Index Shape Dead Zone 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 a consistent layout:

[DzLabel 200px] [Slider 200px] [TextBox 48px] [%] [DigitEdit 52px] [ResetButton]

Slider and TextBox both bind to the same property (e.g., DeadZoneX) with Mode=TwoWay. The digit edit TextBox binds to a separate *Digit property for raw axis value display. 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 (2 columns)
              ├─ Col 0 (*): Controls
              │   ├─ Range: RangeSlider (dual-thumb, DeadZone/MaxRange 0-100%)
              │   │   + two TextBox edits (dz—max) + two digit edits + reset
              │   ├─ Anti-Dead Zone (DzSlider 0-100% + % edit + digit edit + reset)
              │   ├─ "Sensitivity Curve" header + hint text
              │   ├─ Preset ComboBox (CurvePresetNames) + reset
              │   └─ Live value bar (ProgressBar 0-1 + RawDisplay text)
              └─ Col 1 (Auto): CurveEditor (120px, IsSigned=False)
                  └─ CurveString=SensitivityCurve, DeadZone/MaxRange bindings

RangeSlider — Custom dual-thumb control for setting dead zone floor and max range ceiling in one visual:

  • LowerValue="{Binding DeadZone}" — the dead zone threshold.
  • UpperValue="{Binding MaxRange}" — the max range ceiling.
  • Visual feedback: the range between thumbs represents the active trigger zone.

Force Feedback Tab (Tab 5) — Detailed

ScrollViewer
  └─ StackPanel
      ├─ "Force Feedback / Rumble" header + "Reset All" button
      ├─ Overall Gain slider (0-100%, ForceOverallGain)
      ├─ Left Motor Strength slider (0-100%, LeftMotorStrength)
      ├─ Right Motor Strength slider (0-100%, RightMotorStrength)
      ├─ Swap Motors CheckBox (SwapMotors)
      ├─ "Test Rumble" Button (TestRumbleCommand)
      ├─ "Motor Activity" header
      ├─ Left Motor live bar (ProgressBar 0-1, LeftMotorDisplay)
      ├─ Right Motor live bar (ProgressBar 0-1, RightMotorDisplay)
      ├─ Separator
      └─ Audio Bass Rumble section
          ├─ "Audio Rumble" header + "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)

All Audio Rumble controls have IsEnabled="{Binding AudioRumbleEnabled}" — they are disabled (grayed) when audio rumble is off.

vJoy Config Bar

Visible when OutputType == VJoy. Horizontal centered StackPanel:

Control AutomationId Binding
Preset ComboBox (Xbox 360 / DualShock 4 / Custom) VJoyPresetCombo Index-based, VJoyPresetCombo_SelectionChanged
Thumbstick Count TextBox VJoyStickCountBox VJoyConfig.ThumbstickCount
Trigger Count TextBox VJoyTriggerCountBox VJoyConfig.TriggerCount
POV Count TextBox VJoyPovCountBox VJoyConfig.PovCount
Button Count TextBox VJoyButtonCountBox VJoyConfig.ButtonCount

Custom spinners are only visible when preset = Custom. _syncingVJoyConfig guard prevents recursive updates during programmatic sync. When preset changes, SettingsManager.ReAutoMapSlot() is called before setting the preset to ensure correct mapping rows.

MIDI Config Bar

Visible when OutputType == Midi. Horizontal centered StackPanel:

Control Binding Range
Channel TextBox MidiConfig.Channel 1-16
CC Count TextBox MidiConfig.CcCount 0-127 (interdependent with StartCc)
Start CC TextBox MidiConfig.StartCc 0-127
Note Count TextBox MidiConfig.NoteCount 0-127 (interdependent with StartNote)
Start Note TextBox MidiConfig.StartNote 0-127
Velocity TextBox MidiConfig.Velocity 0-127

All fields have tooltips explaining their function. _syncingMidiConfig guard prevents recursive updates. When CC/Note counts or start numbers change, vm.RebuildMappings() is called to regenerate mapping rows.

Copy From Dialog

"Copy From" button opens CopyFromDialog to copy mappings from another slot's device.


DevicesPage

Files: DevicesPage.xaml, DevicesPage.xaml.cs

Lists all detected input devices with raw input state visualization.

Layout Structure

Grid (Margin="24,16")
├─ Row 0 (Auto): Header
│   ├─ Icon (E772) + Title
│   ├─ Refresh Button (RefreshCommand)
│   └─ Online/Total count display
└─ Row 1 (*): Main content (Grid, 2 columns)
    ├─ Col 0 (*): Device card ListBox
    │   └─ ListBoxItem with custom ControlTemplate (4px accent left bar on selection)
    │       └─ Card Border (CornerRadius=8, Padding="12,10")
    │           ├─ Row 0, Col 0: Status dot + DeviceName (SemiBold, 13px)
    │           ├─ Row 0-1, Col 1: Slot badges (WrapPanel of numbered badges) or "Unassigned"
    │           ├─ Row 0-1, Col 2: Remove device Button (E711 × icon)
    │           └─ Row 1: DeviceType + VID:PID + CapabilitiesSummary
    └─ Col 1 (340px): Detail panel (Border with ScrollViewer)
        ├─ Device identity section
        │   ├─ DeviceName (16px, SemiBold, wrapping)
        │   ├─ Product name, Type, Capabilities
        │   ├─ Instance GUID (marquee overflow)
        │   ├─ Instance Path (marquee, conditional StringToVisibility)
        │   └─ VID:PID
        ├─ Submit Mapping Button (joysticks only, opens GitHub issue template)
        ├─ Separator
        ├─ VC Assignment section
        │   └─ WrapPanel of ToggleButtons (ActiveSlotItems, ToggleSlotCommand)
        ├─ Separator
        ├─ Input Mode section (gamepads only)
        │   └─ "Force raw joystick mode" CheckBox (ForceRawJoystickMode)
        ├─ Separator
        ├─ Input Hiding section
        │   ├─ "Hide from games (HidHide)" CheckBox (HidHideEnabled)
        │   └─ "Consume mapped inputs" CheckBox (ConsumeInputEnabled, ShowConsumeToggle)
        ├─ Separator
        └─ Raw Input State section
            ├─ Axes (joysticks/gamepads, hidden for keyboard/mouse)
            │   └─ ItemsControl → ProgressBar per axis (0-1, name + bar + raw value)
            ├─ Buttons (joysticks/gamepads, hidden for keyboard/mouse)
            │   └─ WrapPanel of 24×24 circles, accent fill when pressed
            ├─ Keyboard layout (keyboard devices only)
            │   └─ Viewbox → Canvas (556×136) with positioned key Borders
            ├─ Mouse preview (mouse devices only)
            │   └─ Viewbox → MousePreviewControl
            ├─ D-Pad / POV hats (conditional on RawPovs.Count > 0)
            │   └─ Horizontal StackPanel of compass indicators
            │       ├─ 36×36 Ellipse background + center dot
            │       └─ Accent-colored Line with RotateTransform(AngleDegrees), hidden when IsCentered
            ├─ Gyroscope (HasGyroData, 3-column X/Y/Z grid, Consolas F3 format)
            └─ Accelerometer (HasAccelData, same layout as gyro)

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

Device Card Bindings (DeviceRowViewModel)

Binding Description
IsOnline Status dot color (green/gray via BoolToColorConverter)
DeviceName Bold device name
SlotBadges Collection of slot assignment badges
IsUnassigned Shows "Unassigned" badge when no slots assigned
DeviceType Type string
VendorIdHex / ProductIdHex Hex VID:PID
CapabilitiesSummary e.g. "6 axes, 11 buttons, 1 POV"

Detail Panel Bindings (SelectedDevice)

Binding Description
DeviceName, ProductName, DeviceType Device identity
CapabilitiesSummary Capabilities line
InstanceGuid GUID (marquee)
HidHideInstancePath Instance path (conditional visibility via StringToVisibility)
ShowSubmitMapping Submit mapping button visibility (joysticks only)
IsGamepad Controls Input Mode section visibility
ForceRawJoystickMode Force raw toggle
IsHidHideAvailable Enables/disables HidHide checkbox
HidHideEnabled HidHide toggle
ShowConsumeToggle Consume toggle visibility (mouse/keyboard devices)
ConsumeInputEnabled Consume toggle
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 Gyro/accel section visibility
GyroX/Y/Z / AccelX/Y/Z Motion sensor values

Selection Highlighting

Custom ListBoxItem ControlTemplate:

  • SelectionBar — 4px wide Border with accent brush on left edge, CornerRadius="2".
  • Visibility 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
HidingToggle_Changed CheckBox.Checked/Unchecked Shows warning flyout for mouse/keyboard enable; clears LastRawStateDeviceGuid for rebuild; calls NotifyDeviceHidingChanged
ShowHidingWarningFlyout (internal) ModernWpf 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
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 initiation via mouse events. The drag data is a DataObject with key "DeviceInstanceGuid" and value device.InstanceGuid. Dropping on a sidebar controller card assigns the device to that slot.


KBMPreviewView

Files: KBMPreviewView.xaml, KBMPreviewView.xaml.cs

Interactive keyboard and mouse preview for Keyboard+Mouse virtual controller slots, shown on the Controller tab of the PadPage.

Layout

Two Canvas areas arranged horizontally:

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

Interaction

All elements are clickable for click-to-record mapping — clicking a key or mouse element fires ControllerElementRecordRequested with the target name (e.g., KbmKey41 for a keyboard key, KbmMBtn0 for LMB, KbmMouseX for horizontal mouse movement). Hover highlights use a blue brush; recording targets use a 400ms flash timer with orange brush.

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.

Tooltip Helper

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


MidiPreviewView

Files: MidiPreviewView.xaml, MidiPreviewView.xaml.cs

MIDI note and CC visualization for MIDI virtual controller slots, shown on the Controller tab of the PadPage.

Layout

A single Canvas (MidiCanvas) dynamically rebuilt when MidiSlotConfig properties change (start note, note count, start CC, CC count):

  • CC Sliders section — Vertical bar sliders, one per CC output. Each has a background rectangle, a fill rectangle that grows from the bottom proportional to the CC value (0-127), and a CC number label below.
  • Piano Keyboard section — Musically correct piano layout: white keys placed first (full height, underneath), black keys placed on top (shorter, narrower, higher Z-index) between adjacent white keys. White keys display note name + octave labels (e.g., "C4", "D4"). Note layout follows standard chromatic positions (C, C#, D, D#, E, F, F#, G, G#, A, A#, B).

Interaction

All CC slider backgrounds and piano keys are clickable for click-to-record (fires ControllerElementRecordRequested with MidiCC{index} or MidiNote{index}). Hover highlights and 400ms flash timer for recording targets, matching the pattern used by 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 PressedBrush (blue tint for white keys, darker blue for black keys) on active notes.

Layout Rebuild

The entire canvas is rebuilt (cleared and reconstructed) when any MidiSlotConfig property changes — this handles dynamic changes to note/CC count and range without partial layout updates.


MousePreviewControl

Files: MousePreviewControl.xaml, MousePreviewControl.xaml.cs

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

Layout

Built once on Loaded into a Canvas (MouseCanvas). Renders the same mouse body shape as KBMPreviewView but without click-to-record interaction:

  • LMB/RMB — Contoured Path elements flanking the scroll wheel.
  • Scroll wheel pillRectangle with rounded corners between the buttons, plus up/down arrow Polygon indicators.
  • Movement circleEllipse with a deflection dot that tracks live mouse delta.
  • X1/X2 side buttons — Small Rectangle elements on the left edge of the mouse body.

Rendering

Uses CompositionTarget.Rendering (no dirty flag — renders every frame). Reads from the DevicesViewModel DataContext:

  • Button presses: RawButtons[0..4].IsPressed mapped to LMB, MMB, RMB, X1, X2.
  • Movement: MouseMotionX/MouseMotionY (normalized) mapped to dot deflection within the circle.
  • Scroll: MouseScrollIntensity drives arrow fill color, opacity, and scale transform for intensity feedback — arrows grow and brighten proportionally to scroll magnitude.

SettingsPage

Files: SettingsPage.xaml, SettingsPage.xaml.cs

Application settings organized in vertical card sections using CardBorder style.

Layout Structure

ScrollViewer (Padding="24,16")
  └─ StackPanel
      ├─ Page header (E713 gear icon + title)
      ├─ Language card (CardBorder)
      │   ├─ Icon F2B7 + "Language" title + description
      │   └─ ComboBox (AvailableLanguages, DisplayMemberPath="NativeName", Width=250)
      ├─ Appearance card
      │   ├─ Icon E790 + "Appearance" title + description
      │   ├─ "Theme" label
      │   └─ ComboBox (System Default / Light / Dark, SelectedIndex=SelectedThemeIndex)
      ├─ Input Engine card
      │   ├─ Icon E9F5 + title + description
      │   ├─ Auto-start toggle (AutoStartEngine)
      │   ├─ Background polling toggle (EnablePollingOnFocusLoss)
      │   ├─ Polling interval: NumberBox 1-16ms (PollingRateMs)
      │   └─ Hide devices toggle (EnableInputHiding)
      ├─ Window card
      │   ├─ Icon E737 + title + description
      │   ├─ Minimize to tray (MinimizeToTray)
      │   ├─ Start minimized (StartMinimized)
      │   └─ Start at login (StartAtLogin)
      ├─ HidHide Driver card
      │   ├─ Icon ED1A + title + description
      │   ├─ Status: dot + HidHideStatusText + HidHideVersion
      │   ├─ Install/Uninstall buttons (visibility-toggled by IsHidHideInstalled)
      │   └─ Whitelist section (only when installed):
      │       ├─ Title + description
      │       ├─ ListBox of HidHideWhitelistPaths (Consolas, 12px)
      │       └─ Add/Remove buttons
      ├─ ViGEmBus Driver card
      │   ├─ Icon E7FC + title + description
      │   ├─ Status: dot + ViGEmStatusText + ViGEmVersion
      │   └─ Install/Uninstall buttons
      ├─ vJoy Driver card
      │   ├─ SVG joystick icon + title + description
      │   ├─ Status: dot + VJoyStatusText + VJoyVersion
      │   └─ Install/Uninstall buttons
      ├─ Windows MIDI Services card
      │   ├─ Icon E8D6 + title + description
      │   ├─ Status: dot + MidiServicesStatusText + MidiServicesVersion
      │   └─ Install/Uninstall buttons (Install disabled tooltip when IsMidiOsSupported=False)
      ├─ Settings File card
      │   ├─ Icon E8A5 + title + description
      │   ├─ SettingsFilePath (Consolas, 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)
              ├─ .NET Runtime (RuntimeVersion)
              └─ SDL Version (SdlVersion)

Key Bindings

Binding Target Description
AvailableLanguages ComboBox ItemsSource Language options with NativeName display
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
EnableInputHiding CheckBox Master input hiding toggle
MinimizeToTray CheckBox Minimize to system tray
StartMinimized CheckBox Start app minimized
StartAtLogin CheckBox Start at Windows login
IsHidHideInstalled bool Controls status dot, 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
IsViGEmInstalled / IsVJoyInstalled / IsMidiServicesInstalled bool Per-driver status
SaveCommand / ReloadCommand / ResetCommand / OpenSettingsFolderCommand ICommand Settings file operations
HasUnsavedChanges bool Orange warning visibility

Code-Behind

Minimal — constructor only calls InitializeComponent(). All logic is in the ViewModel.


ProfilesPage

Files: ProfilesPage.xaml, ProfilesPage.xaml.cs

Per-app profile management.

Layout Structure

ScrollViewer (Padding="24,16")
  └─ StackPanel
      ├─ Page header (E8F1 people icon + title)
      └─ CardBorder
          ├─ Icon E8F1 + "Management" title + description
          ├─ Auto-switch CheckBox (EnableAutoProfileSwitching)
          ├─ Active profile info (ActiveProfileInfo, SemiBold)
          ├─ Profile ListBox (MinHeight=60, MaxHeight=300)
          │   └─ ItemTemplate:
          │       ├─ Profile Name (SemiBold)
          │       ├─ Executables list (trimmed, collapsed when empty)
          │       └─ Type count badges (horizontal StackPanel):
          │           ├─ Xbox badge: Xbox SVG + XboxCount (collapsed when 0)
          │           ├─ DS4 badge: PS SVG + DS4Count (collapsed when 0)
          │           ├─ vJoy badge: Joystick SVG + VJoyCount (collapsed when 0)
          │           ├─ MIDI badge: E8D6 glyph + MidiCount (collapsed when 0)
          │           ├─ KBM badge: E961 glyph + KbmCount (collapsed when 0)
          │           └─ "No slots" fallback (visible when HasNoSlots=True)
          └─ Action buttons: New / Save As / Edit / Load / Delete

Key Bindings

Binding Target Description
EnableAutoProfileSwitching CheckBox Enables foreground app monitoring
ActiveProfileInfo TextBlock Current active profile name
ProfileItems ListBox ItemsSource Profile list
SelectedProfile ListBox SelectedItem Selected profile
NewProfileCommand Button Create new profile
SaveAsProfileCommand Button Save current config as profile
EditProfileCommand Button Edit profile name/exes
LoadProfileCommand Button Load selected profile
DeleteProfileCommand Button Delete selected profile

Profile Item Bindings

Binding Description
Name Profile name (SemiBold)
Executables Comma-separated exe list (collapsed when empty via DataTrigger)
XboxCount / DS4Count / VJoyCount / MidiCount / KbmCount Per-type counts (badge collapsed when 0)
HasNoSlots Shows "No slots" badge when all type counts are zero

Event Handlers (Code-Behind)

Handler Trigger Action
ProfileList_MouseDoubleClick ListBox.MouseDoubleClick Executes LoadProfileCommand

AboutPage

Files: AboutPage.xaml, AboutPage.xaml.cs

Static information page showing application identity, description, technologies, and license.

Layout Structure

ScrollViewer (Padding="24,16")
  └─ StackPanel
      ├─ Page header (E946 info icon + title)
      ├─ App identity card (centered, 24px padding)
      │   ├─ "PadForge" (28px, Bold)
      │   ├─ Subtitle (14px)
      │   └─ Tagline (12px)
      ├─ Description card (wrapping text, line height 22)
      ├─ "Built With" section header (E74C checkmark icon)
      ├─ Technologies card (Grid, 140px label + description, 9 rows):
      │   ├─ .NET 10
      │   ├─ SDL3
      │   ├─ Raw Input
      │   ├─ ViGEmBus
      │   ├─ vJoy
      │   ├─ MIDI Services
      │   ├─ HelixToolkit
      │   ├─ ModernWPF
      │   └─ MVVM Toolkit
      ├─ "License" section header (E8D7 icon)
      └─ License card (12px wrapping text, 0.75 opacity)

Code-Behind

Minimal — constructor only calls InitializeComponent(). All text content comes from localized string bindings.


Dialog Windows

CopyFromDialog

Files: CopyFromDialog.xaml, CopyFromDialog.xaml.cs

Modal dialog for copying mappings from another slot's device. Lists all slots with their mapped devices for selection.

ProfileDialog

Files: ProfileDialog.xaml, ProfileDialog.xaml.cs

Modal dialog for creating/editing profiles. Fields: profile name, executable list (comma-separated).


Value Converters

All converters live in PadForge.App/Converter/ under the PadForge.Converters namespace and are registered as StaticResource in App.xaml.

Converter Key Input Output Description
AxisToPercentConverter AxisToPercentConverter ushort (0-65535) double (0-100) or string ("50.0%") Axis value to percentage. Returns formatted string when target type is string.
BoolToColorConverter BoolToColorConverter bool SolidColorBrush true = Green #4CAF50, false = Gray #9E9E9E. Brushes are frozen.
BoolToInstallTextConverter BoolToInstallTextConverter bool string Default: "Installed"/"Not Installed". With parameter "Action": "Uninstall"/"Install".
BoolToOpacityConverter BoolToOpacityConverter bool double true = 1.0, false = 0.2. Parameter: "falseVal" or "falseVal,trueVal".
BoolToVisibilityConverter BoolToVisibilityConverter bool Visibility true = Visible, false = Collapsed. Parameter "Invert" reverses. Supports ConvertBack.
NormToCanvasConverter NormToCanvasConverter double (0-1) double Canvas position. Parameter: "canvasDim" or "canvasDim,dotSize". Dot default 14px.
NormToTriggerHeightConverter NormToTriggerHeightConverter double (0-1) double Pixel height. Parameter = max size (default 40). Used for trigger bars and motor indicators.
NormToTriggerSlideConverter NormToTriggerSlideConverter double (0-1) double Canvas.Top for sliding bar. Parameter: "containerH,barH". 0 = bottom, 1 = top.
NullToCollapsedConverter NullToCollapsedConverter object Visibility Non-null = Visible, null = Collapsed.
PercentToSizeConverter PercentToSizeConverter int/double (0-100) double Pixel size from percentage. Parameter = max size. Used for dead zone ring visualization.
PovToAngleConverter PovToAngleConverter int (centidegrees) double (degrees) 0-35999 centidegrees to 0-359.99 degrees. Returns NaN for -1 (centered).
StatusToColorConverter StatusToColorConverter string SolidColorBrush "Running"/"Online"/"Connected" = Green, "Stopped"/"Offline"/"Error" = Red, "Warning" = Orange, other = Gray.
StringToGeometryConverter StringToGeometry string Geometry Parses SVG path data string to Geometry via Geometry.Parse().
StringToVisibilityConverter StringToVisibility string Visibility Non-null and non-empty = Visible, else Collapsed.
CrossGeometryConverter CrossGeometryConverter MultiBinding(DeadZoneX, DeadZoneY) Geometry Generates cross-shaped path geometry for axial dead zone overlay.
SlopedWedgeGeometryConverter SlopedWedgeGeometryConverter MultiBinding(DeadZoneX, DeadZoneY) Geometry Generates 4 triangular wedge paths for sloped dead zone overlay.

Resource Dictionaries and Theming

App.xaml Resource Hierarchy

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ui:ThemeResources>           <!-- ModernWPF theme (Light/Dark/HighContrast) -->
                <ui:ThemeResources.ThemeDictionaries>
                    <!-- Per-theme brush overrides: RangeSliderThumbFill, PopupBackgroundBrush -->
                </ui:ThemeResources.ThemeDictionaries>
            </ui:ThemeResources>
            <ui:XamlControlsResources />  <!-- ModernWPF control styles -->
            <ResourceDictionary Source="/Resources/ControllerIcons.xaml"/>
        </ResourceDictionary.MergedDictionaries>

        <!-- 14+ global converter registrations -->
    </ResourceDictionary>
</Application.Resources>

ControllerIcons.xaml

File: PadForge.App/Resources/ControllerIcons.xaml

Contains:

DrawingImage icons (used in sidebar, dashboard, profiles):

Key Source Description
XboxControllerIcon svgrepo.com (32x32) Xbox logo icon
DS4ControllerIcon svgrepo.com (32x32) PlayStation logo icon
VJoyControllerIcon svgrepo.com (24x24) Joystick icon
MidiControllerIcon svgrepo.com MIDI / music note icon
GenericControllerIcon svgrepo.com (512x512, scaled) Generic gamepad with D-pad and face buttons

All icons use DynamicResource SystemControlForegroundBaseHighBrush as fill for automatic theme adaptation.

Shared card styles:

Key TargetType Properties
CardBorder Border ChromeMediumLow background, 8px corner radius, 16px padding, 12px bottom margin
CardTitle TextBlock 14px font, SemiBold weight
CardDescription TextBlock 12px font, 0.6 opacity, wrap enabled

Theme Switching

SettingsViewModel.SelectedThemeIndex maps to:

  • 0 = System Default (follows Windows setting)
  • 1 = Light
  • 2 = Dark

Applied via ThemeManager.Current.ApplicationTheme in OnThemeChanged handler.

Custom Per-Theme Brushes

Defined in ThemeDictionaries within App.xaml:

Brush Key Light Dark Usage
RangeSliderThumbFill #F0F0F0 #F0F0F0 RangeSlider custom thumb fill
PopupBackgroundBrush #FFE0E0E0 #FF3A3A3A Add controller popup background

Common XAML Patterns

Card-Based Layout

Settings and driver pages use a consistent 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>

Status Indicator Dots

Used throughout for online/offline and installed/not-installed states:

<Ellipse Width="8" Height="8"
         Fill="{Binding SomeBool, Converter={StaticResource BoolToColorConverter}}"
         VerticalAlignment="Center" Margin="0,0,8,0"/>

Segoe MDL2 Asset Icons

The app uses Segoe MDL2 Assets font for icon glyphs rather than image resources:

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

Common codes used: E713 (settings gear), E790 (personalization), E9F5 (processing), E737 (star), ED1A (shield), E7FC (gamepad), E8A5 (save), E9D9 (bug), E8F1 (group), E8B9 (photo), F158 (3D), E946 (info), E772 (devices), E7E8 (power), E710 (add), E711 (close), E72C (undo), E76C (right arrow), E8D6 (music), E961 (keyboard), EC05 (broadcast), E774 (globe), ED5D (driver), F2B7 (language), E8D7 (document), E74C (checkmark), F404 (home), E7BA (warning).

ModernWPF NumberBox

Used for numeric input with inline spin buttons:

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

Localized String Bindings

All user-facing text uses localized string bindings via the static Strings.Instance pattern:

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

This enables runtime language switching without app restart.

MarqueeBehavior for Overflow Text

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


Code-Behind Patterns

Bind/Unbind Pattern

All five visualization views (3D, 2D, Schematic, MIDI, KBM) follow the same interface:

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

This pattern allows PadPage to switch between views cleanly when the output type or user preference changes.

CompositionTarget.Rendering with Dirty Flag

Views use CompositionTarget.Rendering for per-frame visual updates, 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
}

This batches multiple ViewModel property changes (button presses, axis movements) into a single visual update per render frame, avoiding redundant work.

DispatcherTimer Flash Animation

Used for "Map All" recording flow across all visualization 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 (2D/3D) or 170ms (Schematic)
}

Event Relay Pattern

Code-behind classes raise events that MainWindow.xaml.cs wires to services. This keeps views decoupled from service implementations:

// 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)
DashboardPageView.DeleteSlotRequested += (s, idx) => DeleteSlot(idx);

Syncing Guard Pattern

Used to prevent recursive updates when programmatically setting control values:

private bool _syncingVJoyConfig;

private void SyncVJoyConfigBar()
{
    _syncingVJoyConfig = true;
    VJoyPresetCombo.SelectedIndex = (int)vm.VJoyConfig.Preset;
    // ... set other controls ...
    _syncingVJoyConfig = false;
}

private void VJoyPresetCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (_syncingVJoyConfig) return;  // Skip when syncing programmatically
    // ... handle user-initiated change ...
}

Clone this wiki locally