-
Notifications
You must be signed in to change notification settings - Fork 6
XAML Views
Every view file PadForge ships: the main window shell, page hierarchy, custom controls, value converters, and how themes switch at runtime.
v3 (2026-04-26): Rewritten for v3. The HIDMaestro SDK surface, OpenXInput shim, thread-pool lifecycle, and bubble-up cascade live on HIDMaestro Deep Dive. If anything here drifts from the live source, the live source wins.
All views live in PadForge.App/Views/ (PadForge.Views namespace), except MainWindow.xaml in PadForge.App/. Styled with WPF UI 4.2 (Lepo.Wpf.Ui) for Fluent 2 design.
- Application Shell (MainWindow)
- DashboardPage
- PadPage
- DevicesPage
- KBMPreviewView
- MidiPreviewView
- MousePreviewControl
- SettingsPage
- ProfilesPage
- ProfileSwitchOverlay
- AboutPage
- Dialog Windows
- Value Converters
- Resource Dictionaries and Theming
- Common XAML Patterns
- Code-Behind Patterns
Files: MainWindow.xaml, MainWindow.xaml.cs
Application shell: app branding bar, NavigationView sidebar, page content area, status bar, and driver overlay.
A custom branding bar replaces the traditional title bar. It contains the hamburger menu button and the PadForge icon + name, rendered inside the title bar chrome region using ExtendViewIntoTitleBar. The hamburger button uses WindowChrome.IsHitTestVisibleInChrome so it remains clickable within the non-client area.
SyncBarBackgrounds pixel-samples the NavigationView pane surface to produce an exact color match between the branding bar and the sidebar below it. A RenderTransform with TranslateTransform.Y = -12 on the first sidebar item closes the visual gap between the branding bar and the navigation items. The content Grid uses Margin="0,12,0,0" to compensate for this transform.
Three-row Grid:
- Row 0 (auto): App branding bar (hamburger + icon + title).
-
Row 1 (star):
NavigationViewcontaining all page containers. -
Row 2 (auto): Status bar
Border. - Overlay (ZIndex=1000): Driver operation overlay (blocks UI during install/uninstall).
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Branding bar -->
<RowDefinition Height="*"/> <!-- NavigationView -->
<RowDefinition Height="Auto"/> <!-- Status bar -->
</Grid.RowDefinitions>
<Border x:Name="BrandingBar" Grid.Row="0"> <!-- Hamburger + PadForge icon/name --> </Border>
<ui:NavigationView x:Name="NavView" Grid.Row="1" PaneDisplayMode="Left" OpenPaneLength="253"
IsBackButtonVisible="Collapsed" IsSettingsVisible="False">
<Grid Margin="0,12,0,0">
<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="2"> <!-- Status bar --> </Border>
<Grid x:Name="DriverOverlay" Grid.RowSpan="3" Panel.ZIndex="1000"> <!-- ... --> </Grid>
</Grid>No WPF Frame-based navigation. All pages are instantiated once and visibility-swapped:
-
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(). NavigationView items use 48px height and 15px font size to match the Windows design language. Fixed items (in order):
| Tag | Content | Icon |
|---|---|---|
Dashboard |
Dashboard |
F404 FontIcon (home) |
Profiles |
Profiles |
E8F1 FontIcon (people) |
Devices |
Devices |
E772 FontIcon (USB) |
Settings |
Settings |
E713 FontIcon (gear, footer) |
About |
About |
E946 FontIcon (info, footer) |
Dynamic controller cards are appended after "Devices" (index 3 onward) via RebuildControllerSection(). Each NavigationViewItem contains:
- Power/type glyph (Xbox / PlayStation / Extended / KB+M / MIDI 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, PlayStation, Extended, 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. Colored dot (
EngineStatusBrushbinding) +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 (~3200 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 HidHide and Windows MIDI Services status for hot-plug detection. HIDMaestro is embedded so it has no install/uninstall poll |
CompositionTarget.Rendering |
~60fps | Used by all visualization views (3D, 2D, Schematic, MIDI, KBM, MousePreview) 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,0")
└─ StackPanel (Margin="0,16,0,16")
├─ 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 / PlayStation / Extended / KB+M / 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, 2 rows × 2 cols)
├─ Row 0: HidHide status
└─ Row 1: MIDI Services status
| Binding | ViewModel | Description |
|---|---|---|
EngineStateKey |
DashboardViewModel |
Color-codes engine toggle: "Running"=green, "Idle"=yellow, "Stopped"=red (red circle indicator) |
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 / MidiServicesStatusText
|
DashboardViewModel |
Driver status text shown on the Dashboard. HIDMaestro is embedded; its status appears on the Settings page only |
| 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 + IsHIDMaestroInstalled=False
|
Yellow | "HIDMaestro Not Installed" |
IsEnabled=True + EngineStateKey="Stopped"
|
Yellow | "Engine Stopped" |
IsInitializing=True |
Green (flashing) | "Initializing" |
5 type buttons per slot card (Xbox, PlayStation, Extended, KB+M, MIDI) using a custom TypeSwitchButton style. Dark gray rounded background on hover, transparent border. Active type at Opacity 1.0; inactive at 0.3. Unavailable types (missing prerequisite, e.g. MIDI without Windows MIDI Services) show Cursor.No and a tooltip explaining the requirement; clicks are guarded in code-behind. The power button also uses the TypeSwitchButton style for visual consistency.
| 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, Microsoft). HIDMaestro is embedded so no install gate |
DS4Type_Click |
Button.Click | Raises SlotTypeChangeRequested(slotIndex, PlayStation)
|
ExtendedType_Click |
Button.Click | Raises SlotTypeChangeRequested(slotIndex, Extended)
|
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. Adds 4 physical pixels to bitmap dimensions to prevent clipping at high DPI. -
InsertionLineAdorner. Accent-colored vertical line at insertion point. - Three zones per card: left 25% = insert before, middle 50% = swap, right 25% = insert after.
- Type-group validation: cross-type drag blocked; same-type only.
-
Sidebar rebuild suppression:
RebuildControllerSection()is suppressed while a card drag is in progress to avoid visual disruption. - 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): Extended config bar OR MIDI config bar (conditionally visible)
│ ├─ ExtendedConfigBar (Visibility=Collapsed unless OutputType==Extended)
│ └─ 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,0,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 and by source-device capability:
| Tab | Xbox / PlayStation / Extended | KB+Mouse | MIDI | Capability gate |
|---|---|---|---|---|
| Controller | Visible | Visible | Visible | always |
| Macros | Visible | Visible | Visible | always |
| Mappings | Visible | Visible | Visible | always |
| Sticks | Visible | Visible (Mouse X/Y + Scroll) | Hidden | always within Xbox/PS/Extended |
| Triggers | Visible | Hidden | Hidden | always within Xbox/PS/Extended |
| Force Feedback | Visible | Hidden | Hidden | always within Xbox/PS/Extended |
| Impulse Triggers | Visible if hasImpulseTriggers
|
Hidden | Hidden | source device has trigger motors (Xbox One+/Elite/Series or DualSense) |
| Adaptive Triggers | Visible if hasAdaptiveTriggers
|
Hidden | Hidden | source device is a DualSense or DualSense Edge |
| Lighting | Visible if hasLightbar
|
Hidden | Hidden | source device has a lightbar (DS4 or DualSense family) |
| Gyro | Visible if hasGyro
|
Hidden | Hidden | source device has a gyro sensor |
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:
-
Microsoft→XboxControllerIcon(Image) -
PlayStation→DS4ControllerIcon(Image) -
Extended→ExtendedControllerIcon(Image) -
Midi→E8D6glyph (music note, TextBlock, Image collapsed) -
KeyboardMouse→E961glyph (keyboard, TextBlock, Image collapsed)
The DrawingImage resource keys (XboxControllerIcon, DS4ControllerIcon) retain their v2 names for backward compatibility with the icon dictionary; the matched DataTrigger values are the v3 enum names. TypeInstanceLabel shows the per-type instance number (e.g., "2" for the second Xbox slot).
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 |
HMaestroProfileCombo |
ComboBox | HIDMaestro profile selection (Xbox / PlayStation / Extended slots) |
ExtendedStickCountBox |
TextBox | Extended slot thumbstick count override |
ExtendedTriggerCountBox |
TextBox | Extended slot trigger count override |
ExtendedPovCountBox |
TextBox | Extended slot POV count override |
ExtendedButtonCountBox |
TextBox | Extended slot button count override |
MappingsCountIndicator |
TextBlock (invisible) |
AutomationProperties.Name bound to Mappings.Count
|
DeadZoneShapeCombo |
ComboBox | Deadzone 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, SyncExtendedConfigBar, 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() |
ProfileCombo_PreviewKeyDown |
ComboBox.PreviewKeyDown | Forwards Enter/Esc on HMaestroProfileCombo and ExtendedProfileCombo to commit / dismiss |
ExtendedCustomValue_Changed |
TextBox.LostFocus | Applies clamped Extended layout overrides (sticks/triggers/POVs/buttons) |
ExtendedCustomValue_KeyDown |
TextBox.KeyDown(Enter) | Same as LostFocus apply |
MidiConfig_Changed |
TextBox.LostFocus | Applies clamped MIDI config, rebuilds mappings if counts change |
MidiConfig_KeyDown |
TextBox.KeyDown(Enter) | Same as LostFocus apply |
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 Extended/HID layout, 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 | HIDMaestro Profile | User Pref | Active View | Toggle Visible |
|---|---|---|---|---|
| KeyboardMouse | . | . | KBMPreviewView | No |
| Midi | . | . | MidiPreviewView | No |
| Extended | Custom HID layout | . | ControllerSchematicView | No |
| Extended | Xbox / DS4 catalog | 2D | ControllerModel2DView | Yes |
| Extended | Xbox / DS4 catalog | 3D | ControllerModelView | Yes |
| Xbox | . | 2D | ControllerModel2DView | Yes |
| Xbox | . | 3D | ControllerModelView | Yes |
| PlayStation | . | 2D | ControllerModel2DView | Yes |
| PlayStation | . | 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)
│ ├─ Deadzone Shape ComboBox (6 shapes)
│ ├─ Deadzone X (DzSlider + % edit + digit edit + reset)
│ ├─ Deadzone Y (DzSlider + % edit + digit edit + reset)
│ ├─ Anti-Deadzone X (DzSlider + % edit + digit edit + reset)
│ ├─ Anti-Deadzone Y (DzSlider + % edit + digit edit + reset)
│ ├─ Linear (DzSlider + % edit + reset)
│ ├─ "Sensitivity 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)
│ ├─ Deadzone overlays (shape-dependent):
│ │ ├─ Axial: yellow cross arms + red center rectangle
│ │ ├─ Radial/ScaledRadial: red ellipse
│ │ ├─ Sloped/SlopedScaled: yellow wedges (SlopedWedgeGeometryConverter)
│ │ └─ Hybrid: yellow wedges + red circle center
│ └─ 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
Deadzone Shape Options (ComboBox index):
| Index | Shape | Deadzone Overlay |
|---|---|---|
| 0 | Scaled Radial | Red ellipse (IsRadialShape) |
| 1 | Radial | Red ellipse (IsRadialShape) |
| 2 | Axial | Yellow cross arms + red center rectangle (IsAxialShape) |
| 3 | Hybrid | Yellow wedges + red circle (IsHybridShape) |
| 4 | Sloped Scaled Axial | Yellow wedges (HasSlopedWedges) |
| 5 | Sloped Axial | Yellow wedges (HasSlopedWedges) |
Per-Slider Row Pattern: Each parameter row follows this layout:
[DzLabel 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-Deadzone (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 deadzone floor and max range ceiling:
-
LowerValue="{Binding DeadZone}". Deadzone threshold. -
UpperValue="{Binding MaxRange}". Max range ceiling. - The range between thumbs represents the active trigger zone.
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.
HMaestroProfileBar is shown when OutputType is Microsoft, PlayStation, or Extended (any HM-backed slot). It contains the profile picker only:
| Control | AutomationId | Binding |
|---|---|---|
| Profile ComboBox | HMaestroProfileCombo |
ProfileId, items from AvailableProfiles (HMaestro profile catalog) |
The profile drives identity (VID/PID/product string) and layout (axes/buttons/POVs/touchpad/rumble) for the HM virtual.
ExtendedConfigBar is shown when OutputType == Extended. Stacked rows:
| Row | Controls | Notes |
|---|---|---|
| 1 |
ExtendedProfileCombo + ExtendedImportBtn
|
Profile picker (same AvailableProfiles source) plus Import-From-Device |
| 2 |
ExtendedCustomizeChk + ExtendedResetDefaultsBtn
|
Master toggle for the rows below; reset reverts to catalog defaults |
| 3 |
ExtendedProductStringBox, ExtendedOemOverrideChk, ExtendedVidBox, ExtendedPidBox
|
Identity overrides |
| 4 |
ExtendedStickCountBox, ExtendedTriggerCountBox, ExtendedPovCountBox, ExtendedButtonCountBox, ExtendedTouchpadChk, ExtendedRumbleChk
|
Layout overrides |
Override rows 3 and 4 are gated by IsChecked={ElementName=ExtendedCustomizeChk}, so toggling Customize off restores the catalog profile as-is. _syncingExtendedConfig guard prevents recursive updates inside SyncExtendedConfigBar().
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) | WPF UI Flyout with warning icon, message, Proceed/Cancel buttons. Reverts checkbox immediately, re-checks only on Proceed. |
SubmitMapping_Click |
Button.Click | Opens browser to GitHub issue template with device info pre-filled |
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.
Pre-cached static readonly dark and light brush variants for key backgrounds, borders, and text. The full set is rebuilt on theme change, avoiding per-frame DynamicResource lookups.
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.
Pre-cached static readonly dark and light brush variants for CC bar fills, piano key surfaces, and label text. Rebuilt on theme change to avoid per-frame DynamicResource overhead.
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.
Pre-cached static readonly dark and light brush variants for mouse body, button fills, and indicator colors. Rebuilt on theme change, consistent with KBMPreviewView and MidiPreviewView.
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
├─ HIDMaestro Driver card
│ ├─ Icon E7FC + title + description
│ └─ Status: green dot + "Installed" + HIDMaestroVersion (no Install/Uninstall buttons; HIDMaestro is embedded in the binary)
├─ 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 |
IsMidiServicesInstalled / IsMidiOsSupported
|
bool | MIDI Services status; controls Install button visibility and disabled-tooltip |
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)
│ ├─ PlayStation badge: PS SVG + PlayStationCount (collapsed when 0)
│ ├─ Extended badge: Joystick SVG + ExtendedCount (collapsed when 0)
│ ├─ MIDI badge: E8D6 glyph + MidiCount (collapsed when 0)
│ ├─ KB+M badge: E961 glyph + KbmCount (collapsed when 0)
│ └─ "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 / PlayStationCount / ExtendedCount / MidiCount / KbmCount
|
Per-type counts (badge collapsed when 0) |
HasNoSlots |
Shows "No slots" badge when all type counts are zero |
Added below the profile management card. Provides per-shortcut combo recording and mode selection.
CardBorder (Margin="0,20,0,0")
└─ StackPanel
├─ Icon E71B + "Shortcuts" title + description
├─ ItemsControl (ItemsSource="{Binding ProfileShortcuts}")
│ └─ ItemTemplate (Grid, 5 columns):
│ ├─ Col 0: Mode ComboBox (Next/Previous/Specific/ToggleWindow, Width=155)
│ ├─ Col 1: Profile ComboBox (Specific only, Width=140, collapsed otherwise)
│ ├─ Col 2: Device ComboBox (Width=220)
│ ├─ Col 3: ButtonComboDisplay TextBlock (fills remaining, marquee-enabled)
│ └─ Col 4: Action buttons (Learn/Clear/Delete)
│ ├─ Learn: Click="ShortcutLearn_Click", icon toggles Record/Stop
│ ├─ Clear: Command="{Binding ClearCommand}", icon E75C
│ └─ Delete: Command="{Binding DeleteCommand}", icon E74D
└─ "Add Shortcut" Button (Click="AddShortcut_Click")
| Handler | Trigger | Action |
|---|---|---|
ProfileList_MouseDoubleClick |
ListBox.MouseDoubleClick | Executes LoadProfileCommand
|
ShortcutLearn_Click |
Learn button Click | Starts 5-second combo recording for the row's ProfileShortcutViewModel
|
AddShortcut_Click |
"Add Shortcut" button Click | Creates a new GlobalMacroData, wraps in ProfileShortcutViewModel, adds to list |
| Field | Type | Description |
|---|---|---|
_recordingShortcut |
ProfileShortcutViewModel |
Currently recording shortcut, or null
|
_recordTimer |
DispatcherTimer (33 ms) |
Fires RecordTimer_Tick during recording |
_lastRecordedEntries |
TriggerButtonEntry[] |
Entries captured so far (saved on stop) |
_recordAxisBaselines |
Dictionary<Guid, int[]> |
Per-device axis snapshots at record start. Deflections exceeding AxisRecordDeltaThreshold (0.25) register as axis triggers |
RecordTimeoutSeconds |
const double (5) |
Auto-stop timeout |
Files: ProfileSwitchOverlay.xaml, ProfileSwitchOverlay.xaml.cs
Win11 volume OSD–style flyout that appears above the taskbar during profile switches. Shows the profile name, then transitions through initializing/active/offline states.
Non-activating, topmost, transparent-background overlay:
-
WindowStyle="None",AllowsTransparency="True",Topmost="True" -
ShowInTaskbar="False",ShowActivated="False",Focusable="False" -
WS_EX_NOACTIVATE | WS_EX_TOOLWINDOWapplied viaSetWindowLonginOnLoaded -
WM_MOUSEACTIVATEintercepted to returnMA_NOACTIVATE. Clicks pass through
Grid (ClipToBounds="True") ← clips slide animation at taskbar edge
└─ Grid x:Name="FlyoutPanel" (Margin="10,10,10,14")
├─ ShadowBorder (CornerRadius=8, DropShadowEffect BlurRadius=15)
└─ ContentBorder (CornerRadius=8, Background=#2D2E2E, Border=#141516)
└─ Grid (Margin="16,13,16,13")
└─ StackPanel (Horizontal)
├─ StatusIcon (Segoe Fluent Icons, 16px)
└─ StatusText (Segoe UI Variable Text, 14px)
All color values pixel-measured from the native Win11 volume OSD at 2560x1600 / 150% DPI. Dark and light themes applied dynamically via ApplyTheme() using ApplicationThemeManager.GetAppTheme().
FlyoutPanel.RenderTransform is a TranslateTransform. The outer Grid uses ClipToBounds="True" to hide the panel while it slides:
| Method | Direction | Duration | Easing |
|---|---|---|---|
SlideIn() |
Y: 80 → 0 | 300 ms | CubicEase (EaseOut) |
SlideOut(Action onCompleted) |
Y: 0 → 80 | 250 ms | CubicEase (EaseIn) |
SlideIn() snaps _slideTransform.Y = SlideTravel (80) synchronously, then defers the animation to DispatcherPriority.Loaded so WPF does not coalesce start and end values into a single frame.
The overlay progresses through up to four phases after ShowProfileName(name) is called:
Profile Name (2s) → Initializing (polling) → Active (2s) → Offline (2s) → Hide
└─→ Hide (if no offline)
| Phase | Icon | Text | Timer |
|---|---|---|---|
| Profile |
\uE8F1 (people) |
Profile name |
_dismissTimer 2 s → start init monitor |
| Initializing |
\uE895 (sync) |
"Initializing" |
_initMonitorTimer 33 ms polling CheckInitState
|
| Active |
\uE73E (checkmark, accent color) |
"Active" |
_dismissTimer 2 s → check offline |
| Offline |
\uE7BA (warning, #FFB900 amber) |
"Controllers Offline" |
_dismissTimer 2 s → slide out + hide |
During the Initializing phase, StatusIcon plays a DoubleAnimation opacity flash (1.0 → 0.3, 600 ms, AutoReverse, RepeatBehavior.Forever).
| Method / Property | Description |
|---|---|
CheckInitState |
Func<(bool anyInitializing, bool allReady)>. Set by InputService
|
CheckAnyOffline |
Func<bool>. Set by InputService
|
ShowProfileName(string name) |
Resets state, shows profile name, starts the state machine |
StopTimers() |
Stops both _dismissTimer and _initMonitorTimer. Called during shutdown |
ShowFlyout() centers the window horizontally within SystemParameters.WorkArea and positions the bottom edge at WorkArea.Bottom. The 14 px bottom margin on FlyoutPanel provides the gap above the taskbar.
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, 10 rows):
│ ├─ .NET 10
│ ├─ SDL3
│ ├─ Raw Input
│ ├─ HIDMaestro
│ ├─ OpenXInput
│ ├─ HidHide
│ ├─ MIDI Services
│ ├─ HelixToolkit
│ ├─ WPF UI
│ └─ 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 deadzone 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 deadzone overlay. |
SlopedWedgeGeometryConverter |
SlopedWedgeGeometryConverter |
MultiBinding(DeadZoneX, DeadZoneY) |
Geometry |
4 triangular wedge paths for sloped deadzone overlay. |
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark"/> <!-- WPF UI theme dictionary -->
<ui:ControlsDictionary/> <!-- WPF UI control styles -->
<ResourceDictionary Source="/Resources/ControllerIcons.xaml"/>
<ResourceDictionary>
<SolidColorBrush x:Key="RangeSliderThumbFill" Color="#F0F0F0"/>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
<!-- ComboBox + DynamicScrollBar retemplates, then 14+ global converter registrations -->
</ResourceDictionary>
</Application.Resources>Theme is applied at runtime via Wpf.Ui.Appearance.ApplicationThemeManager.Apply(...) (Light / Dark) or ApplySystemTheme(). The Theme="Dark" attribute on ThemesDictionary is the design-time default.
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 |
ExtendedControllerIcon |
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 Wpf.Ui.Appearance.ApplicationThemeManager.Apply(ApplicationTheme.Light|Dark) or ApplySystemTheme() in OnThemeChanged. Code subscribes to ApplicationThemeManager.Changed to rebuild theme-aware brush caches in the visualization views (KBM, MIDI, Mouse, schematic).
Defined inline in App.xaml:
| Brush Key | Color | Usage |
|---|---|---|
RangeSliderThumbFill |
#F0F0F0 |
RangeSlider custom thumb fill |
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 interval
}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 _syncingExtendedConfig;
private void SyncExtendedConfigBar()
{
if (DataContext is not PadViewModel vm) return;
bool isExtended = vm.OutputType == Engine.VirtualControllerType.Extended;
HMaestroProfileBar.Visibility = (vm.HasHMaestroProfileBar && !isExtended)
? Visibility.Visible
: Visibility.Collapsed;
ExtendedConfigBar.Visibility = isExtended ? Visibility.Visible : Visibility.Collapsed;
if (isExtended)
{
_syncingExtendedConfig = true;
SyncExtendedFields(vm); // populates profile combo + Customize-gated overrides
_syncingExtendedConfig = false;
}
}
private void CustomizeToggle_Changed(object sender, RoutedEventArgs e)
{
if (_syncingExtendedConfig) return; // Skip when syncing programmatically
// ... handle user-initiated change ...
}- Architecture Overview: Application shell, page hosting, WPF UI theme
-
ViewModels:
PadViewModel,DashboardViewModel,DevicesViewModel,SettingsViewModel -
Services Layer:
InputService,SettingsService,DeviceServicewired inMainWindow.xaml.cs -
2D Overlay System:
ControllerModel2DView,ControllerSchematicView,KBMPreviewView,MidiPreviewView -
3D Model System:
ControllerModelView(HelixToolkit 3D viewport) -
Settings and Serialization:
PadSettingdescriptors driving mapping grid UI -
Virtual Controllers: Output type selection UI for Xbox, PlayStation, Extended, MIDI, KB+M (all HM-backed types are produced by
HMaestroVirtualController) -
Driver Installation Internals: HidHide and Windows MIDI Services install/uninstall triggered from
SettingsPage(HIDMaestro is embedded; OpenXInput shim is unpacked next toPadForge.exe)