-
Notifications
You must be signed in to change notification settings - Fork 6
XAML Views
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.
Files: MainWindow.xaml, MainWindow.xaml.cs
The application shell. Contains the NavigationView sidebar, page content area, status bar, and driver overlay.
Two-row Grid:
-
Row 0 (star):
NavigationViewcontaining 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>PadForge does not use WPF Frame-based navigation. All pages are instantiated once in the XAML Grid and visibility-swapped:
-
NavView_SelectionChangedreads the selected item'sTagstring. - All page containers set to
Visibility.Collapsed. - The matching page set to
Visibility.Visible. - For controller slots (tag
"Pad:{index}"), the PadPage'sDataContextis set to the correspondingPadViewModel.
This approach preserves control state (scroll position, selected tabs, text field contents) across navigation since pages are never destroyed.
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.
Users can drag controller cards to reorder virtual controller slots:
-
OnCardDragStart—PreviewMouseLeftButtonDownrecords start position. -
OnNavViewDragMove—PreviewMouseMovechecks distance threshold, thenBeginCardDrag()creates aCardDragAdorner(ghost preview) andInsertionLineAdorner(drop indicator line). -
UpdateDragPosition— Updates adorner positions, computes target insertion index. -
EndCardDrag—PreviewMouseLeftButtonUpcompletes the swap. CallsSettingsManager.SwapSlots()andEnsureTypeGroupOrder().
Devices can be dragged from the Devices page card list to a sidebar controller card:
-
DevicesPageinitiatesDragDrop.DoDragDrop()withDeviceRowViewModelas the data payload. - Sidebar
NavigationViewItemhandlers (DragOver,Drop) accept the drop and assign the device to that slot.
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 fromPads[].OutputType. - All types share the global limit:
MaxXbox360Slots = MaxDS4Slots = MaxVJoySlots = MaxMidiSlots = MaxKeyboardMouseSlots = 16.
Bottom Border with four columns:
-
Status text —
StatusTextbinding, trimmed withCharacterEllipsis. -
Device count —
ConnectedDeviceCountwith "device(s)" suffix. -
Polling frequency —
PollingFrequencyformatted as"{0:F0} Hz". -
Engine indicator — Green/gray dot (
BoolToColorConverter) +EngineStatusText.
Semi-transparent overlay shown during driver install/uninstall operations:
-
ProgressRingspinner + text message. - Blocks all UI interaction (ZIndex=1000,
Grid.RowSpan="2"). - Shown/hidden by
RunDriverOperationAsync().
MainWindow.xaml.cs is the service wiring hub (~2600 lines). Constructor:
- Creates
MainViewModelas root ViewModel; setsDataContext. - Sets child
DataContext: Dashboard, Devices, Settings, Profiles pages. - Creates services:
SettingsService,InputService,RecorderService,DeviceService. - Wires all ViewModel events to services:
-
StartEngineRequested/StopEngineRequestedtoInputService.Start()/Stop(). -
SaveRequested/ReloadRequested/ResetRequestedtoSettingsService. - Driver install/uninstall commands to
DriverInstallermethods wrapped inRunDriverOperationAsync. -
TestRumbleRequested/TestLeftMotorRequested/TestRightMotorRequestedper pad. - Recording flow events per pad/mapping row.
- Profile management events (New, SaveAs, Edit, Load, Delete, RevertToDefault).
- Device assignment events via
DeviceService.
-
| 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 |
Files: DashboardPage.xaml, DashboardPage.xaml.cs
Overview page with engine toggle, slot summary cards, DSU/Web settings, and driver status.
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
| 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 |
| 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 |
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" |
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.
| AutomationId | Element | Purpose |
|---|---|---|
EnableWebControllerCheckBox |
CheckBox | Web controller enable toggle |
| 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 |
Slot cards support drag to reorder (same adorner system as sidebar). Includes:
-
CardDragAdorner— ghost preview rendered fromRenderTargetBitmapsnapshot. -
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)andSlotMoveRequested(SourcePadIndex, TargetVisualPos).
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.
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
| 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. |
Horizontal bar of RadioButton controls styled via TabStripButton. Each RadioButton:
- Uses
GroupName="PadTab"for mutual exclusion. - Stores the tab index in its
Tagproperty (0-5). -
Clickhandler callsvm.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.
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).
The Controller tab header displays a dynamic type icon via nested DataTriggers:
-
Xbox360→XboxControllerIcon(Image) -
DualShock4→DS4ControllerIcon(Image) -
VJoy→VJoyControllerIcon(Image) -
Midi→E8D6glyph (music note, TextBlock, Image collapsed) -
KeyboardMouse→E961glyph (keyboard, TextBlock, Image collapsed)
The TypeInstanceLabel binding shows the per-type instance number (e.g., "Xbox 360 1").
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.
| 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) |
| 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
|
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:
- Unbinds all five views (3D, 2D, Schematic, MIDI, KBM).
- Subscribes the active view's
ControllerElementRecordRequestedevent. - 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.
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 withIsAxisDeviceObjects from selected device. - Uses
AxisPickerItemwrapper class withInputIndexand localizedDisplayName.
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 (
ColorAnimationfrom#002196F3to#602196F3, 400ms, AutoReverse, Forever) whenIsRecording=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:
-
InvertCheckBox —IsInvertedbinding. -
HalfCheckBox —IsHalfAxisbinding.
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%).
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.
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.
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.
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" button opens CopyFromDialog to copy mappings from another slot's device.
Files: DevicesPage.xaml, DevicesPage.xaml.cs
Lists all detected input devices with raw input state visualization.
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)
| 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 |
| 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" |
| 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 |
Custom ListBoxItem ControlTemplate:
-
SelectionBar— 4px wideBorderwith accent brush on left edge,CornerRadius="2". - Visibility toggled by
IsSelectedtrigger. - Content offset 6px right to accommodate the bar.
| 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 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.
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.
Two Canvas areas arranged horizontally:
-
KeyboardCanvas — Full QWERTY keyboard layout built programmatically from
KeyboardKeyItem.BuildLayout(). Each key is aBorderwith aTextBlocklabel, 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.
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.
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/MouseDeltaYto deflection within the movement circle. - Scroll arrows: lights up/down arrows based on
ScrollDeltasign.
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.
Files: MidiPreviewView.xaml, MidiPreviewView.xaml.cs
MIDI note and CC visualization for MIDI virtual controller slots, shown on the Controller tab of the PadPage.
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).
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.
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, appliesPressedBrush(blue tint for white keys, darker blue for black keys) on active notes.
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.
Files: MousePreviewControl.xaml, MousePreviewControl.xaml.cs
Read-only mouse graphic used on the Devices page detail pane for mouse-type input devices.
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
Pathelements flanking the scroll wheel. -
Scroll wheel pill —
Rectanglewith rounded corners between the buttons, plus up/down arrowPolygonindicators. -
Movement circle —
Ellipsewith a deflection dot that tracks live mouse delta. -
X1/X2 side buttons — Small
Rectangleelements on the left edge of the mouse body.
Uses CompositionTarget.Rendering (no dirty flag — renders every frame). Reads from the DevicesViewModel DataContext:
- Button presses:
RawButtons[0..4].IsPressedmapped to LMB, MMB, RMB, X1, X2. - Movement:
MouseMotionX/MouseMotionY(normalized) mapped to dot deflection within the circle. - Scroll:
MouseScrollIntensitydrives arrow fill color, opacity, and scale transform for intensity feedback — arrows grow and brighten proportionally to scroll magnitude.
Files: SettingsPage.xaml, SettingsPage.xaml.cs
Application settings organized in vertical card sections using CardBorder style.
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)
| 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 |
Minimal — constructor only calls InitializeComponent(). All logic is in the ViewModel.
Files: ProfilesPage.xaml, ProfilesPage.xaml.cs
Per-app profile management.
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
| 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 |
| 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 |
| Handler | Trigger | Action |
|---|---|---|
ProfileList_MouseDoubleClick |
ListBox.MouseDoubleClick | Executes LoadProfileCommand
|
Files: AboutPage.xaml, AboutPage.xaml.cs
Static information page showing application identity, description, technologies, and license.
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)
Minimal — constructor only calls InitializeComponent(). All text content comes from localized string bindings.
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.
Files: ProfileDialog.xaml, ProfileDialog.xaml.cs
Modal dialog for creating/editing profiles. Fields: profile name, executable list (comma-separated).
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. |
<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>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 |
SettingsViewModel.SelectedThemeIndex maps to:
- 0 = System Default (follows Windows setting)
- 1 = Light
- 2 = Dark
Applied via ThemeManager.Current.ApplicationTheme in OnThemeChanged handler.
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 |
Settings and driver pages use a consistent card pattern:
<Border Style="{StaticResource CardBorder}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="" 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>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"/>The app uses Segoe MDL2 Assets font for icon glyphs rather than image resources:
<TextBlock Text="" 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).
Used for numeric input with inline spin buttons:
<ui:NumberBox Value="{Binding PollingRateMs, Mode=TwoWay}"
Minimum="1" Maximum="16"
SpinButtonPlacementMode="Inline" Width="120"/>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.
Long text (device names, GUIDs, paths) uses MarqueeBehavior.IsEnabled="True" attached property inside a ClipToBounds="True" Border for scrolling overflow text.
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 referenceThis pattern allows PadPage to switch between views cleanly when the output type or user preference changes.
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.
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)
}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);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 ...
}