-
Notifications
You must be signed in to change notification settings - Fork 6
XAML Views
All views live in PadForge.App/Views/ (PadForge.Views namespace), except MainWindow.xaml in PadForge.App/. Styled with ModernWpfUI for Fluent Design.
Files: MainWindow.xaml, MainWindow.xaml.cs
Application shell: 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>No WPF Frame-based navigation. All pages are instantiated once 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}"), PadPage'sDataContextis set to the matchingPadViewModel.
This preserves control state (scroll position, selected tabs, text fields) across navigation since pages are never destroyed.
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 NavigationViewItem contains:
- Power/type glyph (Xbox/DS4/vJoy icon)
- Slot label ("Controller 1", etc.)
- Device name subtitle
- Delete button (visible on hover)
Called on slot create, delete, or reorder. Uses a _rebuildingControllerSection guard to prevent re-entrancy during selection changes.
Drag controller cards to reorder virtual controller slots:
-
OnCardDragStart—PreviewMouseLeftButtonDownrecords start position. -
OnNavViewDragMove—PreviewMouseMovechecks threshold, thenBeginCardDrag()creates aCardDragAdorner(ghost preview) andInsertionLineAdorner(drop indicator). -
UpdateDragPosition— Updates adorner positions, computes target index. -
EndCardDrag—PreviewMouseLeftButtonUpcompletes the swap viaSettingsManager.SwapSlots()andEnsureTypeGroupOrder().
Drag devices from the Devices page to a sidebar controller card:
-
DevicesPageinitiatesDragDrop.DoDragDrop()withDeviceRowViewModelas payload. - Sidebar
NavigationViewItemhandlers (DragOver,Drop) accept the drop and assign the device.
Popup with buttons for Xbox 360, DualShock 4, vJoy, MIDI, and Keyboard+Mouse. Per-type buttons disabled at capacity (opacity 0.35, "(max N)" tooltip). HasAnyControllerTypeCapacity() counts per-type from Pads[].OutputType. All types share the limit of 16.
Bottom Border, 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 during driver install/uninstall:
-
ProgressRingspinner + text message. - Blocks all UI (ZIndex=1000,
Grid.RowSpan="2"). - Shown/hidden by
RunDriverOperationAsync().
MainWindow.xaml.cs is the service wiring hub (~2600 lines). Constructor:
- Creates
MainViewModelas root; setsDataContext. - Sets child
DataContexton Dashboard, Devices, Settings, Profiles pages. - Creates services:
SettingsService,InputService,RecorderService,DeviceService. - Wires ViewModel events to services:
-
StartEngineRequested/StopEngineRequestedtoInputService.Start()/Stop(). -
SaveRequested/ReloadRequested/ResetRequestedtoSettingsService. - Driver install/uninstall to
DriverInstallerviaRunDriverOperationAsync. -
TestRumbleRequested/TestLeftMotorRequested/TestRightMotorRequestedper pad. - Recording flow events per pad/mapping row.
- Profile management (New, SaveAs, Edit, Load, Delete, RevertToDefault).
- Device assignment 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
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 |
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" |
5 type buttons per slot card (Xbox, PS, vJoy, KBM, MIDI). Active type at Opacity 1.0; inactive at 0.3. Driver-dependent types (Xbox/DS4 need ViGEm, vJoy needs vJoy driver) show 0.15 opacity when the driver is missing.
| 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 |
Drag to reorder (same adorner system as sidebar):
-
CardDragAdorner— ghost preview fromRenderTargetBitmapsnapshot. -
InsertionLineAdorner— accent-colored vertical line at insertion point. - Three zones per card: left 25% = insert before, middle 50% = swap, right 25% = insert after.
- Type-group validation: cross-type drag blocked; same-type only.
- Events:
SlotSwapRequested(PadIndexA, PadIndexB)andSlotMoveRequested(SourcePadIndex, TargetVisualPos).
Files: PadPage.xaml, PadPage.xaml.cs
Per-slot configuration: 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 RadioButton bar styled via TabStripButton. Each button uses GroupName="PadTab" for mutual exclusion, stores the tab index in Tag (0-5), and sets vm.SelectedConfigTab = idx on click.
A TabControl with hidden header (custom ControlTemplate showing only PART_SelectedContentHost) provides content switching. SelectedIndex is bound to SelectedConfigTab.
Tabs hidden by 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 |
If the selected tab is hidden, the view auto-switches to Controller (index 0). SyncTabVisibility() also toggles motor activity bars (MotorBarsGrid).
Controller tab header shows 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)
TypeInstanceLabel shows the per-type instance number (e.g., "Xbox 360 1").
Inline ComboBox bound to MappedDevices / SelectedMappedDevice. Each item shows an online status dot (green #4CAF50 / gray #888888) and device Name. Always visible, 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(): Unbinds all five views, subscribes the active view's ControllerElementRecordRequested event, then calls Bind(vm). All views fire ControllerElementRecordRequested with a PadSetting target name for click-to-record.
Motor Activity Bars — Horizontal fill bars bound to LeftMotorDisplay/RightMotorDisplay (0-1 normalized), converted via NormToTriggerHeightConverter (param=240). Clickable for motor test. 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:
DropDownOpenedpopulates from devices assigned to current slot. - Axis index ComboBox:
DropDownOpenedpopulates withIsAxisDeviceObjects from selected device. - Uses
AxisPickerItemwrapper 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}"— per-row 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 this 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. Digit edit binds to a separate *Digit property for raw axis values. Reset buttons use per-property commands (e.g., ResetDeadZoneXCommand).
Independent Axis Range Sliders:
-
MaxRangeXNeg/MaxRangeX— Left/Right boundaries for X axis (1-100%). -
MaxRangeY/MaxRangeYNeg— Down/Up boundaries for Y axis (1-100%).
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 — Dual-thumb control for dead zone floor and max range ceiling:
-
LowerValue="{Binding DeadZone}"— dead zone threshold. -
UpperValue="{Binding MaxRange}"— max range ceiling. - 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 bind IsEnabled="{Binding AudioRumbleEnabled}" — grayed out when off.
Visible when OutputType == VJoy. Centered horizontal 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 visible only when preset = Custom. _syncingVJoyConfig guard prevents recursive updates. On preset change, SettingsManager.ReAutoMapSlot() runs before setting the preset to ensure correct mappings.
Visible when OutputType == Midi. Centered horizontal 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. _syncingMidiConfig guard prevents recursive updates. When CC/Note counts or start numbers change, vm.RebuildMappings() regenerates mapping rows.
Opens CopyFromDialog to copy mappings from another slot's device.
Files: DevicesPage.xaml, DevicesPage.xaml.cs
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— 4pxBorderwith accent brush on left edge,CornerRadius="2". - 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 via mouse events. Drag data is a DataObject with key "DeviceInstanceGuid" and value device.InstanceGuid. Drop on a sidebar controller card assigns the device to that slot.
Files: KBMPreviewView.xaml, KBMPreviewView.xaml.cs
Keyboard and mouse preview for Keyboard+Mouse virtual controller slots, shown on the PadPage Controller tab.
Two horizontal Canvas areas:
-
KeyboardCanvas — QWERTY layout built from
KeyboardKeyItem.BuildLayout(). Each key is aBorder+TextBlock, absolutely positioned. Keys highlight with accent color when pressed in the output state. - MouseCanvas — Stylized mouse graphic: contoured LMB/RMB paths, scroll wheel pill, movement circle with deflection dot, scroll arrows, and X1/X2 side buttons.
All elements are clickable for click-to-record — fires ControllerElementRecordRequested with the target name (e.g., KbmKey41, KbmMBtn0, KbmMouseX). Hover highlights use blue; recording targets flash at 400ms with orange.
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 mapping table, falling back to the raw name. X1/X2 side button Rectangle elements are promoted to named fields for flash support.
Files: MidiPreviewView.xaml, MidiPreviewView.xaml.cs
MIDI note and CC visualization for MIDI virtual controller slots, shown on the PadPage Controller tab.
Single Canvas (MidiCanvas), rebuilt when MidiSlotConfig properties change (start note, note count, start CC, CC count):
- CC Sliders — Vertical bars, one per CC. Background rectangle + fill rectangle proportional to value (0-127) + CC number label.
- Piano Keyboard — Standard chromatic layout: white keys full-height underneath, black keys shorter on top (higher Z-index). White keys show note name + octave (e.g., "C4", "D4").
CC sliders and piano keys are clickable for click-to-record (fires ControllerElementRecordRequested with MidiCC{index} or MidiNote{index}). Hover highlights and 400ms flash timer match other preview views.
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 cleared and rebuilt on any MidiSlotConfig property change — no partial layout updates.
Files: MousePreviewControl.xaml, MousePreviewControl.xaml.cs
Read-only mouse graphic for mouse-type devices on the Devices page detail pane.
Built once on Loaded into Canvas (MouseCanvas). Same mouse shape as KBMPreviewView but without click-to-record:
-
LMB/RMB — Contoured
Pathelements flanking the scroll wheel. -
Scroll wheel — Rounded
Rectanglebetween buttons, with up/down arrowPolygonindicators. -
Movement circle —
Ellipsewith deflection dot tracking live mouse delta. -
X1/X2 side buttons — Small
Rectangleelements on the left edge.
Uses CompositionTarget.Rendering (no dirty flag — every frame). Reads from DevicesViewModel:
- Buttons:
RawButtons[0..4].IsPressedmapped to LMB, MMB, RMB, X1, X2. - Movement:
MouseMotionX/MouseMotionY(normalized) to dot deflection. - Scroll:
MouseScrollIntensitydrives arrow fill, opacity, and scale — arrows grow and brighten with scroll magnitude.
Files: SettingsPage.xaml, SettingsPage.xaml.cs
Application settings in vertical CardBorder sections.
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 |
Constructor only; all logic in 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
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)
Constructor only; all text from localized string bindings.
Files: CopyFromDialog.xaml, CopyFromDialog.xaml.cs
Modal dialog to copy mappings from another slot's device. Lists all slots with mapped devices.
Files: ProfileDialog.xaml, ProfileDialog.xaml.cs
Modal dialog to create/edit profiles. Fields: profile name, executable list (comma-separated).
All converters in PadForge.App/Converter/ (PadForge.Converters namespace), registered as StaticResource in App.xaml.
| Converter | Key | Input | Output | Description |
|---|---|---|---|---|
AxisToPercentConverter |
AxisToPercentConverter |
ushort (0-65535) |
double (0-100) or string ("50.0%") |
Axis to percentage. Returns formatted string when target is string. |
BoolToColorConverter |
BoolToColorConverter |
bool |
SolidColorBrush |
true = Green #4CAF50, false = Gray #9E9E9E. Frozen brushes. |
BoolToInstallTextConverter |
BoolToInstallTextConverter |
bool |
string |
"Installed"/"Not Installed". 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" (default 14px). |
NormToTriggerHeightConverter |
NormToTriggerHeightConverter |
double (0-1) |
double |
Pixel height. Parameter = max size (default 40). For trigger/motor bars. |
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 |
Percentage to pixel size. Parameter = max. For dead zone ring. |
PovToAngleConverter |
PovToAngleConverter |
int (centidegrees) |
double (degrees) |
0-35999 to 0-359.99 degrees. NaN for -1 (centered). |
StatusToColorConverter |
StatusToColorConverter |
string |
SolidColorBrush |
"Running"/"Online"/"Connected" = Green, "Stopped"/"Offline"/"Error" = Red, "Warning" = Orange, else Gray. |
StringToGeometryConverter |
StringToGeometry |
string |
Geometry |
SVG path data to Geometry via Geometry.Parse(). |
StringToVisibilityConverter |
StringToVisibility |
string |
Visibility |
Non-null/non-empty = Visible, else Collapsed. |
CrossGeometryConverter |
CrossGeometryConverter |
MultiBinding(DeadZoneX, DeadZoneY) |
Geometry |
Cross-shaped path for axial dead zone overlay. |
SlopedWedgeGeometryConverter |
SlopedWedgeGeometryConverter |
MultiBinding(DeadZoneX, DeadZoneY) |
Geometry |
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
DrawingImage icons (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 for 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.
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 this 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 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 Assets font glyphs instead of image resources:
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="20"/>Codes used: E713 settings, E790 personalization, E9F5 processing, E737 star, ED1A shield, E7FC gamepad, E8A5 save, E9D9 bug, E8F1 group, 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.
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 bindings via Strings.Instance:
<TextBlock Text="{Binding Settings_Title, Source={x:Static strings:Strings.Instance}}"/>Enables runtime language switching without restart.
Long text (device names, GUIDs, paths) uses MarqueeBehavior.IsEnabled="True" inside a ClipToBounds="True" Border for scrolling overflow.
All five visualization views (3D, 2D, Schematic, MIDI, KBM) share this interface:
public void Bind(PadViewModel vm) // Subscribe to PropertyChanged, hook rendering, load model
public void Unbind() // Stop flash, unhook rendering, clear VM referenceLets PadPage switch views cleanly when output type or preference changes.
Per-frame visual updates via CompositionTarget.Rendering, gated by a _dirty flag:
private bool _dirty;
private void OnRendering(object sender, EventArgs e)
{
if (!_dirty || _vm == null) return;
_dirty = false;
// Update visuals...
}
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
_dirty = true; // Coalesce multiple property changes into one render frame
}Batches multiple property changes into one visual update per frame, avoiding redundant work.
"Map All" recording flow across all views:
private DispatcherTimer _flashTimer;
private string _flashTarget;
private bool _flashOn;
private void UpdateFlashTarget(string target)
{
// Start or stop flash timer based on target
// Timer callback toggles highlight/default materials at 400ms (2D/3D) or 170ms (Schematic)
}Code-behind raises events that MainWindow.xaml.cs wires to services, keeping views decoupled:
// DashboardPage.xaml.cs
public event EventHandler<int> DeleteSlotRequested;
private void DeleteSlot_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is int slotIndex)
DeleteSlotRequested?.Invoke(this, slotIndex);
}
// MainWindow.xaml.cs (wiring)
DashboardPageView.DeleteSlotRequested += (s, idx) => DeleteSlot(idx);Prevents 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 ...
}