Skip to content

XAML Views

hifihedgehog edited this page Apr 18, 2026 · 36 revisions

XAML Views Reference

All views live in PadForge.App/Views/ (PadForge.Views namespace), except MainWindow.xaml in PadForge.App/. Styled with ModernWpfUI for Fluent Design.

Contents


Application Shell (MainWindow)

Files: MainWindow.xaml, MainWindow.xaml.cs

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

App Branding Bar

A custom branding bar replaces the traditional title bar. 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.

XAML Structure

Three-row Grid:

  • Row 0 (auto): App branding bar (hamburger + icon + title).
  • Row 1 (star): NavigationView containing 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>

Navigation Model

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

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

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

Sidebar Construction

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/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.

Sidebar Drag Reordering

Drag controller cards to reorder virtual controller slots:

  1. OnCardDragStart. PreviewMouseLeftButtonDown records start position.
  2. OnNavViewDragMove. PreviewMouseMove checks threshold, then BeginCardDrag() creates a CardDragAdorner (ghost preview) and InsertionLineAdorner (drop indicator).
  3. UpdateDragPosition. Updates adorner positions, computes target index.
  4. EndCardDrag. PreviewMouseLeftButtonUp completes the swap via SettingsManager.SwapSlots() and EnsureTypeGroupOrder().

Cross-Panel Device Drag-Drop

Drag devices from the Devices page to a sidebar controller card:

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

Add Controller Popup

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.

Status Bar

Bottom Border, four columns:

  1. Status text. StatusText binding, trimmed with CharacterEllipsis.
  2. Device count. ConnectedDeviceCount with "device(s)" suffix.
  3. Polling frequency. PollingFrequency formatted as "{0:F0} Hz".
  4. Engine indicator. Colored dot (EngineStatusBrush binding) + EngineStatusText.

Driver Overlay

Semi-transparent overlay during driver install/uninstall:

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

Composition Root (Code-Behind)

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

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

Timer Architecture

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

DashboardPage

Files: DashboardPage.xaml, DashboardPage.xaml.cs

Engine toggle, slot summary cards, DSU/Web settings, and driver status.

Layout Structure

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/PS/vJoy/KBM/MIDI) + Instance label
       │       ├─ Row 1: DeviceName (marquee overflow)
       │       └─ Row 2: StatusText + mapped/connected counts
       ├─ "Add Controller" card (MouseLeftButtonUp → AddControllerRequested)
       ├─ "Motion Server" section (DSU)
       │   └─ CardBorder: Enable toggle, port NumberBox, status dot, footer
       ├─ "Web Controller" section
       │   └─ CardBorder: Enable toggle, port NumberBox, status dot, footer
       └─ "Drivers" section
           └─ CardBorder (Grid, 4 rows × 2 cols)
               ├─ Row 0: HidHide status
               ├─ Row 1: MIDI Services status
               ├─ Row 2: ViGEmBus status
               └─ Row 3: vJoy status

Key Bindings

Binding ViewModel Description
EngineStateKey DashboardViewModel Color-codes engine toggle: "Running"=green, "Idle"=yellow, "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 / ViGEmStatusText / VJoyStatusText / MidiServicesStatusText DashboardViewModel Driver status text

Slot Card DataTemplate Bindings (SlotSummary)

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

Power Toggle State Machine

Power icon (E7E8) uses multi-condition DataTriggers:

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

Type Switch Buttons

5 type buttons per slot card (Xbox, PS, vJoy, KBM, 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 driver) 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.

UI Automation

AutomationId Element Purpose
EnableWebControllerCheckBox CheckBox Web controller enable toggle

Event Handlers (Code-Behind)

Handler Event Action
EngineToggle_Click Button.Click Raises EngineToggleRequested
AddControllerCard_Click Border.MouseLeftButtonUp Raises AddControllerRequested
DeleteSlot_Click Button.Click Raises DeleteSlotRequested(slotIndex)
PowerToggle_Click Button.Click Raises SlotEnabledToggled(slotIndex, !IsEnabled)
XboxType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Xbox360). Guarded by IsViGEmInstalled
DS4Type_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, DualShock4). Guarded by IsViGEmInstalled
VJoyType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, VJoy). Guarded by IsVJoyInstalled
KeyboardMouseType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, KeyboardMouse)
MidiType_Click Button.Click Raises SlotTypeChangeRequested(slotIndex, Midi)
SlotCard_Loaded Border.Loaded Wires PreviewMouseLeftButtonDown for drag start
OnCardMouseDown PreviewMouseLeftButtonDown Records drag start position; skips if inside a Button
OnDragMove PreviewMouseMove Begins/updates card drag with ghost adorner
OnDragEnd PreviewMouseLeftButtonUp Completes swap/insert or fires SlotCardClicked for navigation
OnDragKeyDown PreviewKeyDown Cancels drag on Escape

Dashboard Card Drag Reordering

Drag to reorder (same adorner system as sidebar):

  • CardDragAdorner. Ghost preview from RenderTargetBitmap snapshot. Adds 4 physical pixels to bitmap dimensions to prevent clipping at high DPI.
  • InsertionLineAdorner. Accent-colored vertical line at insertion point.
  • Three zones per card: left 25% = insert before, middle 50% = swap, right 25% = insert after.
  • Type-group validation: cross-type drag blocked; same-type only.
  • Sidebar rebuild suppression: RebuildControllerSection() is suppressed while a card drag is in progress to avoid visual disruption.
  • Events: SlotSwapRequested(PadIndexA, PadIndexB) and SlotMoveRequested(SourcePadIndex, TargetVisualPos).

PadPage

Files: PadPage.xaml, PadPage.xaml.cs

Per-slot configuration: custom tab strip, optional config bars, and 6 tab panels.

Layout Structure

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

Custom Styles (UserControl.Resources)

Style Key TargetType Properties
DzLabel TextBlock Width=200, centered, theme foreground. Used for slider row labels.
DzSlider Slider Min=0, Max=100, Width=200, SnapToTick=True, TickFrequency=0.1.
OffsetSlider Slider Extends DzSlider with Min=-100 (for center offset).
DzValue TextBlock Right-aligned, medium brush, margin 4,0,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.

Custom Tab Strip

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.

Tab Visibility Rules

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 Type Icon

Controller tab header shows a dynamic type icon via nested DataTriggers:

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

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

Multi-Device Selector

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.

UI Automation Properties

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

Event Handlers (Code-Behind)

Handler Trigger Action
PadPage_Loaded UserControl.Loaded Calls ApplyViewMode, SyncTabStripSelection, SyncVJoyConfigBar, SyncMidiConfigBar
OnDataContextChanged DataContextChanged Unsubscribes old VM, subscribes new VM PropertyChanged, resyncs all
ViewModeToggle_Click Button.Click Toggles SettingsViewModel.Use2DControllerView, calls ApplyViewMode
TabBtn_Click RadioButton.Click Sets vm.SelectedConfigTab from Tag
Motor_MouseEnter/Leave StackPanel.Mouse Hover opacity effect (0.7/1.0)
LeftMotor_Click StackPanel.MouseLeftButtonDown padVm.FireTestLeftMotor()
RightMotor_Click StackPanel.MouseLeftButtonDown padVm.FireTestRightMotor()
MapAllStop_Click Button.Click padVm.StopMapAll()
CalibrateCenter_Click Button.Click StickConfigItem.StartCalibration()
VJoyPresetCombo_SelectionChanged ComboBox.SelectionChanged Re-automaps slot, sets VJoyConfig.Preset, syncs custom fields
VJoyCustomValue_Changed TextBox.LostFocus Applies clamped vJoy custom values
VJoyCustomValue_KeyDown TextBox.KeyDown(Enter) Same as LostFocus apply
MidiConfig_Changed TextBox.LostFocus Applies clamped MIDI config, rebuilds mappings if counts change
MidiConfig_KeyDown TextBox.KeyDown(Enter) Same as LostFocus apply
StickPresetX_SelectionChanged ComboBox.SelectionChanged Sets StickConfigItem.SensitivityCurveX from preset
StickPresetY_SelectionChanged ComboBox.SelectionChanged Sets StickConfigItem.SensitivityCurveY from preset
TriggerPreset_SelectionChanged ComboBox.SelectionChanged Sets TriggerConfigItem.SensitivityCurve from preset
AppVolumeProcessDropDown_Opened ComboBox.DropDownOpened action.RefreshAudioProcessesCommand.Execute()
DeviceAxisPicker_DropDownOpened ComboBox.DropDownOpened Populates ComboBox with devices assigned to current slot
DeviceAxisIndexPicker_DropDownOpened ComboBox.DropDownOpened Populates ComboBox with axis-type DeviceObjects from selected device
OnPadVmPropertyChanged PadViewModel.PropertyChanged Syncs tab strip on SelectedConfigTab, resyncs config bars on OutputType

Controller Tab (Tab 0). Detailed

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

View Switching Logic (ApplyViewMode):

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

BindActiveModelView(): 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.

Macros Tab (Tab 1). Detailed

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

Macro Trigger Section:

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

Action Type Editor Panels:

Action Type Visible Panel Key Controls
ButtonPress / ButtonRelease IsButtonType WrapPanel of CheckBox items from ButtonOptions
KeyPress / KeyRelease IsKeyType KeyString TextBox (Consolas font), VirtualKey ComboBox picker, Clear button
ButtonPress / KeyPress / Delay IsDurationType DurationMs TextBox + "ms" label
AxisSet IsAxisType Axis target ComboBox (LStickX/Y, RStickX/Y, LT, RT) + AxisValue TextBox
SystemVolume IsSystemVolumeType Axis source (Output/Input), axis selector, device picker, volume limit slider, invert toggle, OSD toggle
AppVolume IsAppVolumeType Process ComboBox (editable, refreshes on dropdown), axis source, device picker, volume limit, invert toggle
MouseMove / MouseScroll IsMouseMoveType Axis source (Output/Input), axis selector, device picker, sensitivity slider
MouseButtonPress / MouseButtonRelease IsMouseButtonType Mouse button ComboBox (Left/Right/Middle/X1/X2)

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

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

Mappings Tab (Tab 2). Detailed

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

DataGrid Properties:

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

Source Column ComboBox:

  • ItemsSource="{Binding AvailableInputs}". Per-row physical inputs.
  • SelectedItem="{Binding SelectedInput, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}".
  • DisplayMemberPath="DisplayName".

Options Column:

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

Sticks Tab (Tab 3). Detailed

ScrollViewer
  └─ ItemsControl (ItemsSource=StickConfigs, DataType=StickConfigItem)
      └─ per-stick StackPanel:
          ├─ Title + "Reset All" button
          └─ Grid (2 columns)
              ├─ Col 0 (*): Slider controls
              │   ├─ "Calibrate Center" button (click → StartCalibration)
              │   ├─ Center Offset X (OffsetSlider, -100 to 100, + digit edit + reset)
              │   ├─ Center Offset Y (OffsetSlider + digit edit + reset)
              │   ├─ 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%).

Triggers Tab (Tab 4). Detailed

ScrollViewer
  └─ ItemsControl (ItemsSource=TriggerConfigs, DataType=TriggerConfigItem)
      └─ per-trigger StackPanel:
          ├─ Title + "Reset All" button
          └─ Grid (2 columns)
              ├─ Col 0 (*): Controls
              │   ├─ Range: RangeSlider (dual-thumb, DeadZone/MaxRange 0-100%)
              │   │   + two TextBox edits (dz.max) + two digit edits + reset
              │   ├─ Anti-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.

Force Feedback Tab (Tab 5). Detailed

ScrollViewer
  └─ StackPanel
      ├─ "Force Feedback / Rumble" header + "Reset All" button
      ├─ Overall Gain slider (0-100%, ForceOverallGain)
      ├─ Left Motor Strength slider (0-100%, LeftMotorStrength)
      ├─ Right Motor Strength slider (0-100%, RightMotorStrength)
      ├─ Swap Motors CheckBox (SwapMotors)
      ├─ "Test Rumble" Button (TestRumbleCommand)
      ├─ "Motor Activity" header
      ├─ Left Motor live bar (ProgressBar 0-1, LeftMotorDisplay)
      ├─ Right Motor live bar (ProgressBar 0-1, RightMotorDisplay)
      ├─ Separator
      └─ Audio Bass Rumble section
          ├─ "Audio Rumble" header + "Reset All" button + description
          ├─ Enable CheckBox (AudioRumbleEnabled)
          ├─ Sensitivity slider (1-20, AudioRumbleSensitivity, format F1)
          ├─ Bass Cutoff slider (20-200 Hz, AudioRumbleCutoffHz, format F0)
          ├─ Left Motor slider (0-100%, AudioRumbleLeftMotor)
          ├─ Right Motor slider (0-100%, AudioRumbleRightMotor)
          └─ Level meter (ProgressBar 0-1, AudioRumbleLevelMeter)

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

vJoy Config Bar

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.

MIDI Config Bar

Visible when OutputType == Midi. Centered horizontal StackPanel:

Control Binding Range
Channel TextBox MidiConfig.Channel 1-16
CC Count TextBox MidiConfig.CcCount 0-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.

Copy From Dialog

Opens CopyFromDialog to copy mappings from another slot's device.


DevicesPage

Files: DevicesPage.xaml, DevicesPage.xaml.cs

All detected input devices with raw input state visualization.

Layout Structure

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

Key Bindings

Binding ViewModel Description
Devices DevicesViewModel Device list collection
SelectedDevice DevicesViewModel Currently selected device row
HasSelectedDevice DevicesViewModel Controls detail panel visibility
OnlineCount / TotalCount DevicesViewModel Header device counts
RefreshCommand DevicesViewModel Refresh button
ActiveSlotItems DevicesViewModel Slot toggle button items
ToggleSlotCommand DevicesViewModel Toggle device-to-slot assignment
RemoveDeviceCommand DevicesViewModel Remove device from list

Device Card Bindings (DeviceRowViewModel)

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

Detail Panel Bindings (SelectedDevice)

Binding Description
DeviceName, ProductName, DeviceType Device identity
CapabilitiesSummary Capabilities line
InstanceGuid GUID (marquee)
HidHideInstancePath Instance path (conditional visibility via StringToVisibility)
ShowSubmitMapping Submit mapping button visibility (joysticks only)
IsGamepad Controls Input Mode section visibility
ForceRawJoystickMode Force raw toggle
IsHidHideAvailable Enables/disables HidHide checkbox
HidHideEnabled HidHide toggle
ShowConsumeToggle Consume toggle visibility (mouse/keyboard devices)
ConsumeInputEnabled Consume toggle
RawAxes Axis ProgressBar items
RawButtons Button circle items
IsKeyboardDevice / IsMouseDevice Switches between button circles, keyboard canvas, or mouse graphic
KeyboardKeys QWERTY keyboard layout items
RawPovs POV compass items
HasGyroData / HasAccelData Gyro/accel section visibility
GyroX/Y/Z / AccelX/Y/Z Motion sensor values

Selection Highlighting

Custom ListBoxItem ControlTemplate:

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

Event Handlers (Code-Behind)

Handler Trigger Action
RemoveDevice_Click Button.Click Selects device, executes RemoveDeviceCommand
HidingToggle_Changed CheckBox.Checked/Unchecked Shows warning flyout for mouse/keyboard enable; clears LastRawStateDeviceGuid for rebuild; calls NotifyDeviceHidingChanged
ShowHidingWarningFlyout (internal) ModernWpf Flyout with warning icon, message, Proceed/Cancel buttons. Reverts checkbox immediately, re-checks only on Proceed.
SubmitMapping_Click Button.Click Opens browser to GitHub issue template with device info pre-filled
DeviceCard_MouseDown PreviewMouseLeftButtonDown Records drag start position; skips if inside a Button
DeviceCard_MouseMove PreviewMouseMove Initiates DragDrop.DoDragDrop with DeviceInstanceGuid data when threshold exceeded
DeviceCard_MouseUp PreviewMouseLeftButtonUp Resets drag state

Device Drag to Sidebar

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


KBMPreviewView

Files: KBMPreviewView.xaml, KBMPreviewView.xaml.cs

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

Layout

Two horizontal Canvas areas:

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

Interaction

All elements are clickable for click-to-record. Fires ControllerElementRecordRequested with the target name (e.g., KbmKey41, KbmMBtn0, KbmMouseX). Hover highlights use blue; recording targets flash at 400ms with orange.

Rendering

Uses CompositionTarget.Rendering with a _dirty flag. Per frame:

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

Theme-Aware Brushes

Pre-cached 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.

Tooltip Helper

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


MidiPreviewView

Files: MidiPreviewView.xaml, MidiPreviewView.xaml.cs

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

Layout

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

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

Interaction

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

Rendering

Uses CompositionTarget.Rendering with a _dirty flag. Per frame:

  • CC sliders: reads MidiOutputSnapshot.CcValues[], scales fill height to 0-100%.
  • Piano keys: reads MidiOutputSnapshot.Notes[] boolean array, applies PressedBrush (blue tint for white keys, darker blue for black keys) on active notes.

Theme-Aware Brushes

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.

Layout Rebuild

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


MousePreviewControl

Files: MousePreviewControl.xaml, MousePreviewControl.xaml.cs

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

Layout

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

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

Theme-Aware Brushes

Pre-cached static readonly dark and light brush variants for mouse body, button fills, and indicator colors. Rebuilt on theme change, consistent with KBMPreviewView and MidiPreviewView.

Rendering

Uses CompositionTarget.Rendering (no dirty flag. Every frame). Reads from DevicesViewModel:

  • Buttons: RawButtons[0..4].IsPressed mapped to LMB, MMB, RMB, X1, X2.
  • Movement: MouseMotionX/MouseMotionY (normalized) to dot deflection.
  • Scroll: MouseScrollIntensity drives arrow fill, opacity, and scale. Arrows grow and brighten with scroll magnitude.

SettingsPage

Files: SettingsPage.xaml, SettingsPage.xaml.cs

Application settings in vertical CardBorder sections.

Layout Structure

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

Key Bindings

Binding Target Description
AvailableLanguages ComboBox ItemsSource Language options with NativeName display
SelectedLanguage ComboBox SelectedItem Current language
SelectedThemeIndex ComboBox SelectedIndex 0=System, 1=Light, 2=Dark
AutoStartEngine CheckBox Auto-start engine on launch
EnablePollingOnFocusLoss CheckBox Continue polling when app loses focus
PollingRateMs NumberBox (1-16) Polling interval in ms
EnableInputHiding CheckBox Master input hiding toggle
MinimizeToTray CheckBox Minimize to system tray
StartMinimized CheckBox Start app minimized
StartAtLogin CheckBox Start at Windows login
IsHidHideInstalled bool Controls status dot, button visibility, whitelist section
InstallHidHideCommand / UninstallHidHideCommand ICommand Driver install/uninstall
HidHideWhitelistPaths Collection Whitelist ListBox items
SelectedWhitelistPath object Selected whitelist item
AddWhitelistPathCommand / RemoveWhitelistPathCommand ICommand Whitelist management
IsViGEmInstalled / IsVJoyInstalled / IsMidiServicesInstalled bool Per-driver status
SaveCommand / ReloadCommand / ResetCommand / OpenSettingsFolderCommand ICommand Settings file operations
HasUnsavedChanges bool Orange warning visibility

Code-Behind

Constructor only; all logic in ViewModel.


ProfilesPage

Files: ProfilesPage.xaml, ProfilesPage.xaml.cs

Per-app profile management.

Layout Structure

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

Key Bindings

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

Profile Item Bindings

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

Controller Shortcuts Card

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

CardBorder (Margin="0,20,0,0")
  └─ StackPanel
      ├─ Icon E71B + "Shortcuts" title + description
      ├─ ItemsControl (ItemsSource="{Binding ProfileShortcuts}")
      │   └─ ItemTemplate (Grid, 5 columns):
      │       ├─ Col 0: 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")

Event Handlers (Code-Behind)

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

Shortcut Recording State (Code-Behind)

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

ProfileSwitchOverlay

Files: ProfileSwitchOverlay.xaml, ProfileSwitchOverlay.xaml.cs

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

Window Properties

Non-activating, topmost, transparent-background overlay:

  • WindowStyle="None", AllowsTransparency="True", Topmost="True"
  • ShowInTaskbar="False", ShowActivated="False", Focusable="False"
  • WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW applied via SetWindowLong in OnLoaded
  • WM_MOUSEACTIVATE intercepted to return MA_NOACTIVATE. Clicks pass through

Layout Structure

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

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

Slide Animation

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

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

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

State Machine

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

Profile Name (2s) → Initializing (polling) → Active (2s) → Offline (2s) → Hide
                                                         └─→ Hide (if no offline)
Phase Icon Text Timer
Profile \uE8F1 (people) Profile name _dismissTimer 2 s → start init monitor
Initializing \uE895 (sync) "Initializing" _initMonitorTimer 33 ms polling CheckInitState
Active \uE73E (checkmark, accent color) "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).

Public API

Method / Property Description
CheckInitState Func<(bool anyInitializing, bool allReady)>. Set by InputService
CheckAnyOffline Func<bool>. Set by InputService
ShowProfileName(string name) Resets state, shows profile name, starts the state machine
StopTimers() Stops both _dismissTimer and _initMonitorTimer. Called during shutdown

Positioning

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


AboutPage

Files: AboutPage.xaml, AboutPage.xaml.cs

Application identity, description, technologies, and license.

Layout Structure

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

Code-Behind

Constructor only; all text from localized string bindings.


Dialog Windows

CopyFromDialog

Files: CopyFromDialog.xaml, CopyFromDialog.xaml.cs

Modal dialog to copy mappings from another slot's device. Lists all slots with mapped devices.

ProfileDialog

Files: ProfileDialog.xaml, ProfileDialog.xaml.cs

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


Value Converters

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.

Resource Dictionaries and Theming

App.xaml Resources

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

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

ControllerIcons.xaml

File: PadForge.App/Resources/ControllerIcons.xaml

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

Theme Switching

SettingsViewModel.SelectedThemeIndex maps to:

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

Applied via ThemeManager.Current.ApplicationTheme in OnThemeChanged.

Custom Per-Theme Brushes

Defined in ThemeDictionaries within App.xaml:

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

Common XAML Patterns

Card-Based Layout

Settings and driver pages use this card pattern:

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

Status Indicator Dots

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 Asset Icons

Segoe MDL2 Assets font glyphs instead of image resources:

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

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

ModernWPF NumberBox

Numeric input with inline spin buttons:

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

Localized String Bindings

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

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

Enables runtime language switching without restart.

MarqueeBehavior for Overflow Text

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


Code-Behind Patterns

Bind/Unbind Pattern

All 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 reference

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

CompositionTarget.Rendering with Dirty Flag

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

private bool _dirty;

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

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

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

DispatcherTimer Flash Animation

"Map All" recording flow across all views:

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

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

Event Relay Pattern

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

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

// MainWindow.xaml.cs (wiring)
DashboardPageView.DeleteSlotRequested += (s, idx) => DeleteSlot(idx);

Syncing Guard Pattern

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 ...
}

See Also

Clone this wiki locally