-
Notifications
You must be signed in to change notification settings - Fork 6
XAML Views
All views live in PadForge.App/Views/ under the PadForge.Views namespace. The app uses ModernWpfUI for Windows 10/11 Fluent Design styling.
Files: MainWindow.xaml, MainWindow.xaml.cs
The application shell. Contains the NavigationView sidebar, page content area, status bar, and driver overlay.
Two-row Grid:
-
Row 0 (star):
NavigationViewcontaining all page containers. -
Row 1 (auto): Status bar
Border. - Overlay (ZIndex=1000): Driver operation overlay (blocks UI during install/uninstall).
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/> <!-- NavigationView -->
<RowDefinition Height="Auto"/> <!-- Status bar -->
</Grid.RowDefinitions>
<ui:NavigationView x:Name="NavView" PaneDisplayMode="Left" PaneTitle="PadForge" OpenPaneLength="200"
IsBackButtonVisible="Collapsed" IsSettingsVisible="True">
<Grid>
<views:DashboardPage x:Name="DashboardPageView" Visibility="Visible"/>
<views:PadPage x:Name="PadPageView" Visibility="Collapsed"/>
<views:DevicesPage x:Name="DevicesPageView" Visibility="Collapsed"/>
<views:SettingsPage x:Name="SettingsPageView" Visibility="Collapsed"/>
<views:ProfilesPage x:Name="ProfilesPageView" Visibility="Collapsed"/>
<views:AboutPage x:Name="AboutPageView" Visibility="Collapsed"/>
</Grid>
</ui:NavigationView>
<Border Grid.Row="1"> <!-- Status bar --> </Border>
<Grid x:Name="DriverOverlay" Grid.RowSpan="2" Panel.ZIndex="1000"> <!-- ... --> </Grid>
</Grid>PadForge does not use WPF Frame-based navigation. All pages are instantiated once in the XAML Grid and visibility-swapped:
-
NavView_SelectionChangedreads the selected item'sTagstring. - All page containers set to
Visibility.Collapsed. - The matching page set to
Visibility.Visible. - For controller slots (tag
"Pad:{index}"), the PadPage'sDataContextis set to the correspondingPadViewModel.
This approach preserves control state (scroll position, selected tabs, text field contents) across navigation since pages are never destroyed.
The sidebar is built programmatically in BuildNavigationItems(). Fixed items:
| Tag | Content | Icon |
|---|---|---|
Dashboard |
Dashboard |
Home SymbolIcon |
Devices |
Devices |
Gamepad SymbolIcon |
Profiles |
Profiles |
People SymbolIcon |
About |
About |
Help SymbolIcon (footer) |
| Settings | (built-in) | Built-in gear icon |
Dynamic controller cards are inserted between "Devices" and "Profiles" via RebuildControllerSection(). Each card is a NavigationViewItem with custom Content containing:
- Power/type glyph (Xbox/DS4/vJoy icon)
- Slot label ("Controller 1", etc.)
- Device name subtitle
- Delete button (visible on hover)
RebuildControllerSection() is called when slots are created, deleted, or reordered. It uses a _rebuildingControllerSection guard to prevent re-entrancy during NavigationView selection changes.
Users can drag controller cards to reorder virtual controller slots:
-
OnCardDragStart—PreviewMouseLeftButtonDownrecords start position. -
OnNavViewDragMove—PreviewMouseMovechecks distance threshold, thenBeginCardDrag()creates aCardDragAdorner(ghost preview) andInsertionLineAdorner(drop indicator line). -
UpdateDragPosition— Updates adorner positions, computes target insertion index. -
EndCardDrag—PreviewMouseLeftButtonUpcompletes the swap. CallsSettingsManager.SwapSlots()andEnsureTypeGroupOrder().
Devices can be dragged from the Devices page card list to a sidebar controller card:
-
DevicesPageinitiatesDragDrop.DoDragDrop()withDeviceRowViewModelas the data payload. - Sidebar
NavigationViewItemhandlers (DragOver,Drop) accept the drop and assign the device to that slot.
A Popup with buttons for Xbox 360, DualShock 4, and vJoy. Per-type buttons disabled at capacity:
- Opacity 0.35 and "(max N)" tooltip when limit reached.
-
HasAnyControllerTypeCapacity()counts per-type fromPads[].OutputType. - Per-type limits:
MaxXbox360Slots = 4,MaxDS4Slots = 4,MaxVJoySlots = 16.
Bottom Border with four columns:
-
Status text —
StatusTextbinding, trimmed withCharacterEllipsis. -
Device count —
ConnectedDeviceCountwith "device(s)" suffix. -
Polling frequency —
PollingFrequencyformatted as"{0:F0} Hz". -
Engine indicator — Green/gray dot (
BoolToColorConverter) +EngineStatusText.
Semi-transparent overlay shown during driver install/uninstall operations:
-
ProgressRingspinner + text message. - Blocks all UI interaction (ZIndex=1000,
Grid.RowSpan="2"). - Shown/hidden by
RunDriverOperationAsync().
MainWindow.xaml.cs is the service wiring hub (~1400 lines). Constructor:
- Creates
MainViewModelas root ViewModel; setsDataContext. - Sets child
DataContext: Dashboard, Devices, Settings, Profiles pages. - Creates services:
SettingsService,InputService,RecorderService,DeviceService. - Wires all ViewModel events to services:
-
StartEngineRequested/StopEngineRequestedtoInputService.Start()/Stop(). -
SaveRequested/ReloadRequested/ResetRequestedtoSettingsService. - Driver install/uninstall commands to
DriverInstallermethods wrapped inRunDriverOperationAsync. -
TestRumbleRequested/TestLeftMotorRequested/TestRightMotorRequestedper pad. - Recording flow events per pad/mapping row.
- Profile management events (New, SaveAs, Edit, Load, Delete, RevertToDefault).
- Device assignment events via
DeviceService.
-
| Timer | Interval | Purpose |
|---|---|---|
DispatcherTimer |
33ms (~30Hz) | Calls InputService.UpdateUI() to push engine state into ViewModels |
_driverStatusTimer |
5s | Polls ViGEm/vJoy driver status for hot-plug detection |
CompositionTarget.Rendering |
~60fps | Used by 3D/2D/Schematic views for per-frame visual updates |
Files: DashboardPage.xaml, DashboardPage.xaml.cs
Overview page with engine toggle, slot summary cards, DSU settings, and driver status.
- Engine toggle: Start/Stop button with status indicator dot.
-
Slot cards:
WrapPanelinsideItemsControlbound toDashboardViewModel.SlotSummaries. Each card shows slot label, device name, status dot (green=active, gray=idle), and output type icon with instance number. -
"Add Controller" card: Shown when
ShowAddControlleris true. Opens the type selection popup. - DSU settings: Enable toggle, port number field, server status text.
- Driver status: Three rows showing ViGEmBus, HidHide, and vJoy installation status with green/gray indicator dots.
- Card drag reordering: Slot cards support drag to reorder (same adorner system as sidebar).
Files: PadPage.xaml, PadPage.xaml.cs
Configuration page for a single virtual controller slot. Contains a custom tab strip and 6 tab panels.
Horizontal bar of RadioButton controls styled via a custom ControlTemplate (the TabStripButton style). Each RadioButton:
- Uses
GroupName="PadTab"for mutual exclusion. - Stores the tab index in its
Tagproperty. -
Clickhandler callsvm.SelectedConfigTab = idx.
A hidden TabControl header via custom ControlTemplate provides the actual content switching. The TabControl's SelectedIndex is bound to SelectedConfigTab.
| Index | Tab Name | Description |
|---|---|---|
| 0 | Controller | 3D/2D/Schematic model visualization |
| 1 | Macros | Macro editor with trigger and action configuration |
| 2 | Mappings | DataGrid of MappingItem rows with Record/Clear buttons |
| 3 | Sticks | Dynamic ItemsControl of StickConfigItem with dead zone sliders and live preview |
| 4 | Triggers | Dynamic ItemsControl of TriggerConfigItem with RangeSlider for dead zone/max range |
| 5 | Force Feedback | Motor strength sliders, swap toggle, gain control, test buttons |
Hosts one of three views depending on output type and user settings:
-
ControllerModelView (3D, HelixToolkit) -- for Xbox 360/DS4 when
Use2DControllerViewis false. -
ControllerModel2DView (2D overlays) -- for Xbox 360/DS4 when
Use2DControllerViewis true. - ControllerSchematicView (procedural) -- for custom vJoy (non-gamepad preset), always.
ApplyViewMode() in PadPage.xaml.cs controls which view is visible:
- Custom vJoy: Schematic always, 2D/3D toggle hidden.
- Gamepad: 2D/3D toggle visible, user preference from
SettingsViewModel.Use2DControllerView.
BindActiveModelView() wires the active view:
- Unbinds all three views.
- Subscribes the active view's
ControllerElementRecordRequestedevent. - Calls
Bind(vm)on the active view.
All three views fire ControllerElementRecordRequested with a PadSetting target name string for click-to-record.
Button with icon that switches between 2D and 3D:
-
E8B9(flat/photo icon) shown in 3D mode, click switches to 2D. -
F158(3D/cube icon) shown in 2D mode, click switches to 3D. - Hidden when displaying custom vJoy schematic.
Visible when OutputType == VJoy. Contains:
- Preset ComboBox (Xbox 360, DualShock 4, Custom).
- Four spinners visible only in Custom: ThumbstickCount, TriggerCount, PovCount, ButtonCount.
-
VJoyPresetCombo_SelectionChangedupdatesVJoyConfig.Presetand callsSyncVJoyCustomFields(). -
_syncingVJoyConfigguard prevents recursive updates during programmatic sync.
ComboBox bound to MappedDevices with SelectedMappedDevice. Visible when multiple physical devices are assigned to one slot.
"Copy From" button opens CopyFromDialog to copy mappings from another slot's device.
Files: DevicesPage.xaml, DevicesPage.xaml.cs
Lists all detected input devices with raw input state visualization.
ListBox with card ItemTemplate. Each card shows:
- Status dot (green=online, gray=offline via
BoolToColorConverter) - Device name (bold)
- Slot badges (
SlotBadgescollection with sequential numbering) - Device type and capabilities summary
- VID/PID
Selection highlighting: Custom ControlTemplate for ListBoxItem with 4px accent-colored left bar on the selected card.
Shown when a device is selected (HasSelectedDevice). Contains:
-
Axes:
ItemsControlofProgressBarelements (0-100% range viaAxisToPercentConverter). Each bar shows name, normalized bar, and raw value. -
Buttons:
WrapPanelof circles. Accent fill when pressed (BoolToOpacityConverter). For keyboard devices, displaysKeyboardKeyson a positionedCanvasinside aViewbox(QWERTY layout). -
POV hats:
ItemsControlbound toRawPovs. Each POV displays as a compass with aRotateTransformarrow (PovToAngleConverterconverts centidegrees to degrees). -
Gyroscope/Accelerometer: Grid showing X/Y/Z values when
HasGyroData/HasAccelDataare true.
ItemsControl of toggle buttons bound to ActiveSlotItems. Accent fill when IsAssigned. Click fires ToggleSlotCommand to assign/unassign the device to a controller slot.
Device cards support drag start via PreviewMouseLeftButtonDown + PreviewMouseMove. Dropping on a sidebar controller card assigns the device to that slot.
Files: SettingsPage.xaml, SettingsPage.xaml.cs
Application settings organized in vertical card sections using CardBorder style.
| Section | Icon | Contents |
|---|---|---|
| Appearance | E790 |
Theme ComboBox (System Default / Light / Dark) |
| Input Engine | E9F5 |
Auto-start toggle, polling on focus loss, polling interval NumberBox (1-16ms) |
| Window | E737 |
Minimize to tray, start minimized, start at login checkboxes |
| HidHide Driver | ED1A |
Status dot, version text, Install/Uninstall buttons |
| ViGEmBus Driver | E7FC |
Status dot, version text, Install/Uninstall buttons |
| vJoy Driver | joystick SVG | Status dot, version text, Install/Uninstall buttons |
| Settings File | E8A5 |
File path, Save/Reload/Reset/Open Folder buttons, unsaved indicator |
| Diagnostics | E9D9 |
App version, .NET runtime, SDL version |
Driver sections use BoolToColorConverter for status dots and BoolToVisibilityConverter (with Invert parameter) to toggle Install/Uninstall button visibility.
Files: ProfilesPage.xaml, ProfilesPage.xaml.cs
Per-app profile management.
-
Auto-switch toggle:
CheckBoxbound toEnableAutoProfileSwitching. -
Active profile info:
TextBlockshowing current profile name. -
Profile ListBox: Each item shows profile name, executable list (trimmed), and topology badges (Xbox/DS4/vJoy counts with mini SVG icons). Badges use
DataTriggeron count = 0 to collapse empty types. "No slots" fallback badge shown when all counts are zero. - Action buttons: New, Save As, Edit, Load, Delete.
Each profile item contains:
- Profile name (
FontWeight="SemiBold"). - Executable list (
TextTrimming, collapsed when empty viaDataTrigger). - Type count badges in a
StackPanel:- Xbox badge: Xbox SVG icon + count, collapsed when
XboxCount == 0. - DS4 badge: PlayStation SVG icon + count, collapsed when
DS4Count == 0. - vJoy badge: Joystick SVG icon + count, collapsed when
VJoyCount == 0. - "No slots" fallback when
TopologyLabel == "No slots".
- Xbox badge: Xbox SVG icon + count, collapsed when
Files: AboutPage.xaml, AboutPage.xaml.cs
Static information page showing application name, version, copyright, license information, and links.
Files: CopyFromDialog.xaml, CopyFromDialog.xaml.cs
Modal dialog for copying mappings from another slot's device. Lists all slots with their mapped devices for selection.
Files: ProfileDialog.xaml, ProfileDialog.xaml.cs
Modal dialog for creating/editing profiles. Fields: profile name, executable list (comma-separated).
All converters live in PadForge.App/Converter/ under the PadForge.Converters namespace and are registered as StaticResource in App.xaml.
| Converter | Key | Input | Output | Description |
|---|---|---|---|---|
AxisToPercentConverter |
AxisToPercentConverter |
ushort (0-65535) |
double (0-100) or string ("50.0%") |
Axis value to percentage. Returns formatted string when target type is string. |
BoolToColorConverter |
BoolToColorConverter |
bool |
SolidColorBrush |
true = Green #4CAF50, false = Gray #9E9E9E. Brushes are frozen. |
BoolToInstallTextConverter |
BoolToInstallTextConverter |
bool |
string |
Default: "Installed"/"Not Installed". With parameter "Action": "Uninstall"/"Install". |
BoolToOpacityConverter |
BoolToOpacityConverter |
bool |
double |
true = 1.0, false = 0.2. Parameter: "falseVal" or "falseVal,trueVal". |
BoolToVisibilityConverter |
BoolToVisibilityConverter |
bool |
Visibility |
true = Visible, false = Collapsed. Parameter "Invert" reverses. Supports ConvertBack. |
NormToCanvasConverter |
NormToCanvasConverter |
double (0-1) |
double |
Canvas position. Parameter: "canvasDim" or "canvasDim,dotSize". Dot default 14px. |
NormToTriggerHeightConverter |
NormToTriggerHeightConverter |
double (0-1) |
double |
Pixel height. Parameter = max size (default 40). Used for trigger bars and motor indicators. |
NormToTriggerSlideConverter |
NormToTriggerSlideConverter |
double (0-1) |
double |
Canvas.Top for sliding bar. Parameter: "containerH,barH". 0 = bottom, 1 = top. |
NullToCollapsedConverter |
NullToCollapsedConverter |
object |
Visibility |
Non-null = Visible, null = Collapsed. |
PercentToSizeConverter |
PercentToSizeConverter |
int/double (0-100) |
double |
Pixel size from percentage. Parameter = max size. Used for dead zone ring visualization. |
PovToAngleConverter |
PovToAngleConverter |
int (centidegrees) |
double (degrees) |
0-35999 centidegrees to 0-359.99 degrees. Returns NaN for -1 (centered). |
StatusToColorConverter |
StatusToColorConverter |
string |
SolidColorBrush |
"Running"/"Online"/"Connected" = Green, "Stopped"/"Offline"/"Error" = Red, "Warning" = Orange, other = Gray. |
StringToGeometryConverter |
StringToGeometry |
string |
Geometry |
Parses SVG path data string to Geometry via Geometry.Parse(). |
StringToVisibilityConverter |
StringToVisibility |
string |
Visibility |
Non-null and non-empty = Visible, else Collapsed. |
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources> <!-- ModernWPF theme (Light/Dark/HighContrast) -->
<ui:ThemeResources.ThemeDictionaries>
<!-- Per-theme brush overrides: RangeSliderThumbFill, PopupBackgroundBrush -->
</ui:ThemeResources.ThemeDictionaries>
</ui:ThemeResources>
<ui:XamlControlsResources /> <!-- ModernWPF control styles -->
<ResourceDictionary Source="/Resources/ControllerIcons.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 14 global converter registrations -->
</ResourceDictionary>
</Application.Resources>File: PadForge.App/Resources/ControllerIcons.xaml
Contains:
DrawingImage icons (used in sidebar, dashboard, profiles):
| Key | Source | Description |
|---|---|---|
XboxControllerIcon |
svgrepo.com (32x32) | Xbox logo icon |
DS4ControllerIcon |
svgrepo.com (32x32) | PlayStation logo icon |
VJoyControllerIcon |
svgrepo.com (24x24) | Joystick icon |
GenericControllerIcon |
svgrepo.com (512x512, scaled) | Generic gamepad with D-pad and face buttons |
All icons use DynamicResource SystemControlForegroundBaseHighBrush as fill for automatic theme adaptation.
Shared card styles:
| Key | TargetType | Properties |
|---|---|---|
CardBorder |
Border |
ChromeMediumLow background, 8px corner radius, 16px padding, 12px bottom margin |
CardTitle |
TextBlock |
14px font, SemiBold weight |
CardDescription |
TextBlock |
12px font, 0.6 opacity, wrap enabled |
SettingsViewModel.SelectedThemeIndex maps to:
- 0 = System Default (follows Windows setting)
- 1 = Light
- 2 = Dark
Applied via ThemeManager.Current.ApplicationTheme in OnThemeChanged handler.
Defined in ThemeDictionaries within App.xaml:
| Brush Key | Light | Dark | Usage |
|---|---|---|---|
RangeSliderThumbFill |
#F0F0F0 |
#F0F0F0 |
RangeSlider custom thumb fill |
PopupBackgroundBrush |
#FFE0E0E0 |
#FF3A3A3A |
Add controller popup background |
Settings and driver pages use a consistent card pattern:
<Border Style="{StaticResource CardBorder}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBlock Text="Card Title" Style="{StaticResource CardTitle}"/>
</StackPanel>
<TextBlock Text="Description text." Style="{StaticResource CardDescription}"/>
<!-- Card content -->
</StackPanel>
</Border>Used throughout for online/offline and installed/not-installed states:
<Ellipse Width="8" Height="8"
Fill="{Binding SomeBool, Converter={StaticResource BoolToColorConverter}}"
VerticalAlignment="Center" Margin="0,0,8,0"/>The app uses Segoe MDL2 Assets font for icon glyphs rather than image resources:
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="20"/>Common codes used: E713 (settings gear), E790 (personalization), E9F5 (processing), E737 (star), ED1A (shield), E7FC (gamepad), E8A5 (save), E9D9 (bug), E8F1 (group), E8B9 (photo), F158 (3D).
Used for numeric input with inline spin buttons:
<ui:NumberBox Value="{Binding PollingRateMs, Mode=TwoWay}"
Minimum="1" Maximum="16"
SpinButtonPlacementMode="Inline" Width="120"/>All three visualization views (3D, 2D, Schematic) follow the same interface:
public void Bind(PadViewModel vm) // Subscribe to PropertyChanged, hook rendering, load model
public void Unbind() // Stop flash, unhook rendering, clear VM referenceThis pattern allows PadPage to switch between views cleanly when the output type or user preference changes.
Views use CompositionTarget.Rendering for per-frame visual updates, gated by a _dirty flag:
private bool _dirty;
private void OnRendering(object sender, EventArgs e)
{
if (!_dirty || _vm == null) return;
_dirty = false;
// Update visuals...
}
private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
_dirty = true; // Coalesce multiple property changes into one render frame
}This batches multiple ViewModel property changes (button presses, axis movements) into a single visual update per render frame, avoiding redundant work.
Used for "Map All" recording flow across all three views:
private DispatcherTimer _flashTimer;
private string _flashTarget;
private bool _flashOn;
private void UpdateFlashTarget(string target)
{
// Start or stop flash timer based on target
// Timer callback toggles highlight/default materials at 400ms (2D/3D) or 170ms (Schematic)
}