Full source code
This page shows the complete TrueAxis source for transparency. it was made with python. Use the Copy button to copy it all at once.
import sys
import time
import threading
import pygame
import json
import os
import vgamepad as vg
from PySide6.QtCore import QObject, Signal, Slot, Property, QTimer
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
# --- CONFIG & PROFILES ---
CONFIG_FILE = "trueaxis_config.json"
BUTTON_MAPPING_FILE = "trueaxis_buttons.json"
PROFILES = {
"Logitech G29 (Standard)": {"axes": [0, 2, 3], "desc": "Recommended first choice for G29"},
"Logitech G29 (Alt Mode)": {"axes": [0, 1, 2], "desc": "Use if Gas/Brake inputs are swapped"},
"Logitech G920 / G923": {"axes": [0, 1, 2], "desc": "Standard Xbox-layout wheels"},
"Driving Force GT": {"axes": [0, 1, 2], "desc": "Legacy Logitech wheels"},
"Logitech G27": {"axes": [0, 2, 3], "desc": "Classic G27 axis mapping"},
"PlayStation 4/5 (Standard)": {"axes": [0, 5, 4], "desc": "Steer: L-Stick, Gas: R2, Brake: L2"},
"PlayStation 4/5 (Alt Mode)": {"axes": [0, 4, 3], "desc": "Use if triggers are reversed"},
"PlayStation (DirectInput)": {"axes": [0, 2, 5], "desc": "Raw DirectInput mode"},
"Fanatec (Standard)": {"axes": [0, 1, 2], "desc": "Yellow compatibility mode"},
"Fanatec (Alt 1)": {"axes": [0, 2, 3], "desc": "Alternative axis configuration"},
"Fanatec (Alt 2)": {"axes": [0, 4, 5], "desc": "Common on CSL DD bases"},
"Fanatec (Alt 3)": {"axes": [0, 5, 6], "desc": "Higher axis pedal mapping"},
"Thrustmaster T300/T150": {"axes": [0, 1, 2], "desc": "Standard Thrustmaster mapping"},
"Thrustmaster T-GT / T248": {"axes": [0, 2, 1], "desc": "Newer generation wheels"},
"Thrustmaster (Combined)": {"axes": [0, 1, 1], "desc": "Combined pedal axis mode"},
"Generic Gamepad (Xbox/XInput)": {"axes": [0, 5, 4], "desc": "Standard Xbox controller layout"},
"Generic Wheel (DirectInput)": {"axes": [0, 1, 2], "desc": "Universal DirectInput wheels"},
"Combined Pedals Mode": {"axes": [0, 1, 1], "desc": "Single axis for Gas/Brake"},
}
XBOX_BUTTON_NAMES = [
"DISABLED",
"A (Cross)",
"B (Circle)",
"X (Square)",
"Y (Triangle)",
"LB (L1)",
"RB (R1)",
"Back (Share)",
"Start (Options)",
"LThumb (L3)",
"RThumb (R3)",
"Guide (PS)",
"DPad Up",
"DPad Down",
"DPad Left",
"DPad Right",
]
XBOX_BUTTON_MAP = {
"DISABLED": None,
"A (Cross)": vg.XUSB_BUTTON.XUSB_GAMEPAD_A,
"B (Circle)": vg.XUSB_BUTTON.XUSB_GAMEPAD_B,
"X (Square)": vg.XUSB_BUTTON.XUSB_GAMEPAD_X,
"Y (Triangle)": vg.XUSB_BUTTON.XUSB_GAMEPAD_Y,
"LB (L1)": vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_SHOULDER,
"RB (R1)": vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_SHOULDER,
"Back (Share)": vg.XUSB_BUTTON.XUSB_GAMEPAD_BACK,
"Start (Options)": vg.XUSB_BUTTON.XUSB_GAMEPAD_START,
"LThumb (L3)": vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_THUMB,
"RThumb (R3)": vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_THUMB,
"Guide (PS)": vg.XUSB_BUTTON.XUSB_GAMEPAD_GUIDE,
"DPad Up": vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_UP,
"DPad Down": vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_DOWN,
"DPad Left": vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_LEFT,
"DPad Right": vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_RIGHT,
}
QML_CODE = """
import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts
import QtQuick.Window
ApplicationWindow {
id: mainWindow
width: 520
height: 950
visible: true
title: "TrueAxis"
color: "#09090b"
// Color Palette
readonly property color colorPrimary: "#10b981"
readonly property color colorPrimaryHover: "#059669"
readonly property color colorAccent: "#3b82f6"
readonly property color colorBg: "#09090b"
readonly property color colorSurface: "#0c0c0f"
readonly property color colorSurfaceHover: "#131316"
readonly property color colorCard: "#111114"
readonly property color colorTextPrimary: "#fafafa"
readonly property color colorTextSecondary: "#a1a1aa"
readonly property color colorTextMuted: "#71717a"
readonly property color colorBorder: "#27272a"
readonly property color colorBorderLight: "#3f3f46"
readonly property color colorSuccess: "#22c55e"
readonly property color colorWarning: "#f59e0b"
readonly property color colorError: "#ef4444"
readonly property color colorInactive: "#3f3f46"
Flickable {
anchors.fill: parent
contentHeight: mainColumn.height
clip: true
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
width: 8
}
Column {
id: mainColumn
width: parent.width
spacing: 0
// HEADER
Item {
width: parent.width
height: 120
Column {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.top: parent.top
anchors.topMargin: 28
spacing: 4
Row {
spacing: 12
Text {
text: "TrueAxis"
font.pixelSize: 32
font.bold: true
color: colorTextPrimary
font.family: "Segoe UI"
}
Rectangle {
width: 45
height: 22
radius: 6
color: colorSurface
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 4
Text {
anchors.centerIn: parent
text: "v2.4"
font.pixelSize: 10
font.bold: true
color: colorTextMuted
font.family: "Segoe UI"
}
}
}
Text {
text: "by nieqtv"
font.pixelSize: 13
color: "#cc1f53"
font.family: "Segoe UI"
}
}
}
Rectangle {
width: parent.width - 48
height: 1
color: colorBorder
anchors.horizontalCenter: parent.horizontalCenter
}
Item { width: 1; height: 20 }
// DEVICE SELECTION
SectionCard {
title: "Input Device"
subtitle: "Select your wheel or controller"
cardHeight: 120
Column {
spacing: 10
width: parent.width
Row {
spacing: 10
width: parent.width
StyledComboBox {
id: deviceCombo
width: parent.width - 110
model: backend.devices
onCurrentIndexChanged: backend.select_device(currentIndex)
}
StyledButton {
text: "↻ Refresh"
width: 100
secondary: true
onClicked: backend.refresh_devices()
}
}
StyledButton {
text: "Open Input Inspector"
width: parent.width
height: 32
secondary: true
fontSize: 11
onClicked: inspectorDialog.open()
}
}
}
// PROFILE SELECTION
SectionCard {
title: "Mapping Profile"
subtitle: "Choose a preset for your device"
cardHeight: 95
Column {
spacing: 8
width: parent.width
StyledComboBox {
id: profileCombo
width: parent.width
model: backend.profileNames
onCurrentTextChanged: backend.select_profile(currentText)
}
Text {
id: profileDesc
text: "Select a profile to see description"
font.pixelSize: 11
color: colorTextMuted
wrapMode: Text.WordWrap
width: parent.width
font.family: "Segoe UI"
}
}
}
// LIVE INPUT PREVIEW
SectionCard {
title: "Live Input Preview"
subtitle: "Real-time axis visualization"
cardHeight: 165
Column {
spacing: 0
width: parent.width
AxisBar {
id: steerBar
label: "Steering"
value: 0
direction: 0
onDirectionChanged: {
// Dynamic gradient based on steering direction
if (direction < -0.3) {
// Hard left - vibrant blue/purple
steerGradient = [
{position: 0.0, color: "#3b82f6"},
{position: 0.3, color: "#6366f1"},
{position: 0.7, color: "#8b5cf6"},
{position: 1.0, color: "#a855f7"}
]
} else if (direction > 0.3) {
// Hard right - vibrant cyan/blue
steerGradient = [
{position: 0.0, color: "#06b6d4"},
{position: 0.3, color: "#0ea5e9"},
{position: 0.7, color: "#3b82f6"},
{position: 1.0, color: "#6366f1"}
]
} else {
// Centered - premium teal gradient
steerGradient = [
{position: 0.0, color: "#2dd4bf"},
{position: 0.4, color: "#0d9488"},
{position: 0.8, color: "#0f766e"},
{position: 1.0, color: "#115e59"}
]
}
}
property var steerGradient: [
{position: 0.0, color: "#2dd4bf"},
{position: 0.4, color: "#0d9488"},
{position: 0.8, color: "#0f766e"},
{position: 1.0, color: "#115e59"}
]
}
Item { width: 1; height: 4 }
AxisBar {
id: gasBar
label: "Gas"
value: 0
onValueChanged: {
// Dynamic gradient based on gas pressure
if (value > 0.8) {
// High pressure - vibrant green/cyan
gasGradient = [
{position: 0.0, color: "#00ff9d"},
{position: 0.2, color: "#00e6b8"},
{position: 0.5, color: "#00d4ff"},
{position: 0.8, color: "#0099ff"},
{position: 1.0, color: "#0066cc"}
]
} else if (value > 0.3) {
// Medium pressure - teal gradient
gasGradient = [
{position: 0.0, color: "#2fa8c2"},
{position: 0.3, color: "#1f8ba3"},
{position: 0.6, color: "#15687d"},
{position: 0.9, color: "#0d4553"},
{position: 1.0, color: "#082c37"}
]
} else {
// Low pressure - subtle gradient
gasGradient = [
{position: 0.0, color: "#34d399"},
{position: 1.0, color: "#059669"}
]
}
}
property var gasGradient: [
{position: 0.0, color: "#34d399"},
{position: 1.0, color: "#059669"}
]
}
Item { width: 1; height: 4 }
AxisBar {
id: brakeBar
label: "Brake"
value: 0
onValueChanged: {
// Dynamic gradient based on brake pressure
if (value > 0.8) {
// Hard braking - intense red gradient
brakeGradient = [
{position: 0.0, color: "#ff6b6b"},
{position: 0.2, color: "#ff5252"},
{position: 0.5, color: "#ff3838"},
{position: 0.8, color: "#ff1e1e"},
{position: 1.0, color: "#dc2626"}
]
} else if (value > 0.3) {
// Medium braking - orange/red gradient
brakeGradient = [
{position: 0.0, color: "#ff9d6b"},
{position: 0.3, color: "#ff7b52"},
{position: 0.6, color: "#ff5a38"},
{position: 0.9, color: "#ff381e"},
{position: 1.0, color: "#e62e00"}
]
} else {
// Light braking - warm amber gradient
brakeGradient = [
{position: 0.0, color: "#ffb74d"},
{position: 1.0, color: "#ff9800"}
]
}
}
property var brakeGradient: [
{position: 0.0, color: "#ffb74d"},
{position: 1.0, color: "#ff9800"}
]
}
Item { width: 1; height: 10 }
Rectangle {
width: parent.width
height: 32
radius: 8
color: colorSurface
Row {
anchors.centerIn: parent
spacing: 20
StyledCheckBox {
text: "Invert Gas"
onToggled: backend.set_invert_gas(checked)
}
StyledCheckBox {
text: "Invert Brake"
onToggled: backend.set_invert_brake(checked)
}
}
}
}
}
// STEERING TUNING
SectionCard {
title: "Steering Tuning"
subtitle: "Adjust steering response curve"
cardHeight: 135
Column {
spacing: 10
width: parent.width
Row {
spacing: 10
width: parent.width
Text {
text: "Square Steering"
font.pixelSize: 11
font.bold: true
color: colorTextSecondary
width: 120
anchors.verticalCenter: parent.verticalCenter
font.family: "Segoe UI"
}
StyledCheckBox {
id: squareCheckbox
text: ""
checked: false
anchors.verticalCenter: parent.verticalCenter
onToggled: backend.set_square_input(checked)
}
Item { width: 1; Layout.fillWidth: true }
}
Row {
spacing: 10
width: parent.width
visible: squareCheckbox.checked
Text {
text: "Square Area:"
font.pixelSize: 11
color: colorTextMuted
width: 120
anchors.verticalCenter: parent.verticalCenter
font.family: "Segoe UI"
}
Rectangle {
id: slider
width: parent.width - 120 - 50
height: 24
radius: 12
color: colorSurface
property real from: 0.0
property real to: 0.99
property real value: 0.5
property real stepSize: 0.01
Rectangle {
width: parent.width * ((parent.value - parent.from) / (parent.to - parent.from))
height: parent.height
radius: 12
color: colorPrimary
}
Rectangle {
x: (parent.width * ((parent.value - parent.from) / (parent.to - parent.from))) - 8
y: (parent.height - 16) / 2
width: 16
height: 16
radius: 8
color: colorTextPrimary
border.color: colorPrimary
border.width: 2
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function updateValue(x) {
var newValue = slider.from + (x / width) * (slider.to - slider.from)
newValue = Math.max(slider.from, Math.min(slider.to, newValue))
if (slider.stepSize > 0) {
newValue = Math.round(newValue / slider.stepSize) * slider.stepSize
}
if (newValue !== slider.value) {
slider.value = newValue
backend.set_square_area(newValue)
}
}
onPressed: updateValue(mouse.x)
onPositionChanged: if (pressed) updateValue(mouse.x)
}
}
Text {
text: Math.round(slider.value * 100) + "%"
font.pixelSize: 11
color: colorTextMuted
width: 40
anchors.verticalCenter: parent.verticalCenter
font.family: "Segoe UI"
}
}
Text {
text: squareCheckbox.checked ?
"Steering will reach 100% at " + Math.round(slider.value * 100) + "% input" :
"Standard circular steering response"
font.pixelSize: 10
color: colorTextMuted
width: parent.width
wrapMode: Text.WordWrap
font.family: "Segoe UI"
}
}
}
// BUTTON MAPPING
SectionCard {
title: "Button Mapping"
subtitle: "Map wheel buttons to controller"
cardHeight: 110
Column {
spacing: 10
width: parent.width
StyledButton {
text: "Configure Buttons"
width: parent.width
secondary: true
onClicked: buttonDialog.open()
}
Text {
id: buttonStatus
text: "No buttons mapped"
font.pixelSize: 11
color: colorTextMuted
font.family: "Segoe UI"
}
Item { width: 1; height: 8 }
}
}
// ACTIVATE BUTTON
Item {
width: parent.width
height: 100
Column {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.right: parent.right
anchors.rightMargin: 24
spacing: 12
Row {
spacing: 6
Text {
id: statusDot
text: "●"
font.pixelSize: 12
color: colorInactive
}
Text {
id: statusText
text: "Ready to activate"
font.pixelSize: 11
color: colorTextMuted
font.family: "Segoe UI"
}
}
StyledButton {
id: activateBtn
text: backend.running ? "DEACTIVATE" : "ACTIVATE"
width: parent.width
height: 52
primary: !backend.running
danger: backend.running
fontSize: 15
onClicked: backend.toggle_mapping()
}
}
}
Item { width: 1; height: 28 }
}
}
// INPUT INSPECTOR DIALOG
Dialog {
id: inspectorDialog
width: 420
height: 550
anchors.centerIn: parent
modal: true
title: "Input Inspector"
background: Rectangle {
color: colorBg
radius: 12
border.color: colorBorder
border.width: 1
}
header: Item {
height: 80
Column {
anchors.left: parent.left
anchors.leftMargin: 20
anchors.top: parent.top
anchors.topMargin: 20
spacing: 4
Text {
text: "Input Inspector"
font.pixelSize: 18
font.bold: true
color: colorTextPrimary
font.family: "Segoe UI"
}
Text {
text: "Move controls to identify axis IDs"
font.pixelSize: 11
color: colorWarning
font.family: "Segoe UI"
}
}
}
contentItem: ScrollView {
clip: true
Column {
spacing: 4
width: parent.width
Repeater {
model: backend.numAxes
Rectangle {
width: 380
height: 50
radius: 8
color: colorCard
border.color: colorBorder
border.width: 1
property int axisIndex: index
Row {
anchors.fill: parent
anchors.margins: 12
spacing: 0
Text {
text: "Axis " + parent.parent.axisIndex
font.pixelSize: 11
font.bold: true
color: colorTextSecondary
width: 50
anchors.verticalCenter: parent.verticalCenter
font.family: "Segoe UI"
}
Item {
width: parent.width - 50 - 55
height: parent.height
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: axisBarBg
anchors.fill: parent
anchors.leftMargin: 5
anchors.rightMargin: 5
height: 8
radius: 4
color: colorSurface
anchors.verticalCenter: parent.verticalCenter
clip: true
Rectangle {
id: axisBarFill
width: 0
height: parent.height
radius: 4
color: colorAccent
Connections {
target: backend
function onAxisValuesChanged() {
var value = backend.getAxisValue(axisIndex)
var clampedValue = Math.max(0, Math.min(1, value))
axisBarFill.width = axisBarBg.width * clampedValue
}
}
Behavior on width {
NumberAnimation { duration: 50; easing.type: Easing.OutQuad }
}
}
}
}
Text {
id: axisValueText
text: "0%"
font.pixelSize: 10
color: colorTextMuted
width: 45
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
font.family: "Segoe UI"
Connections {
target: backend
function onAxisValuesChanged() {
var value = backend.getAxisValue(axisIndex)
var clampedValue = Math.max(0, Math.min(1, value))
axisValueText.text = Math.round(clampedValue * 100) + "%"
}
}
}
}
}
}
}
}
}
// BUTTON MAPPING DIALOG
Dialog {
id: buttonDialog
width: 480
height: 550
anchors.centerIn: parent
modal: true
title: "Button Mapping"
background: Rectangle {
color: colorBg
radius: 12
border.color: colorBorder
border.width: 1
}
header: Item {
height: 80
Column {
anchors.left: parent.left
anchors.leftMargin: 20
anchors.top: parent.top
anchors.topMargin: 20
spacing: 4
Text {
text: "Button Mapping"
font.pixelSize: 18
font.bold: true
color: colorTextPrimary
font.family: "Segoe UI"
}
Text {
text: "Assign wheel buttons to Xbox controller buttons"
font.pixelSize: 11
color: colorTextMuted
font.family: "Segoe UI"
}
}
}
contentItem: Column {
spacing: 10
ScrollView {
width: parent.width
height: 380
clip: true
Column {
spacing: 3
width: parent.width
Repeater {
model: backend.numButtons
Rectangle {
width: 440
height: 48
radius: 8
color: colorCard
border.color: colorBorder
border.width: 1
property int buttonIndex: index
Row {
anchors.centerIn: parent
spacing: 8
width: parent.width - 24
Text {
id: btnIndicator
text: "●"
font.pixelSize: 16
color: colorInactive
Connections {
target: backend
function onButtonStatesChanged() {
btnIndicator.color = backend.getButtonState(buttonIndex) ? colorAccent : colorInactive
}
}
}
Text {
text: "Button " + parent.parent.buttonIndex
font.pixelSize: 11
font.bold: true
color: colorTextSecondary
width: 70
font.family: "Segoe UI"
}
Item { width: 1; Layout.fillWidth: true }
StyledComboBox {
width: 160
height: 32
model: backend.xboxButtonNames
currentIndex: {
var mapping = backend.get_button_mapping(buttonIndex)
return model.indexOf(mapping)
}
onCurrentTextChanged: {
backend.set_button_mapping(buttonIndex, currentText)
}
}
}
}
}
}
}
StyledButton {
text: "Save Configuration"
width: parent.width
height: 42
primary: true
onClicked: {
backend.save_button_mapping()
buttonDialog.close()
}
}
}
}
// CONNECTIONS
Connections {
target: backend
function onSteeringChanged(value) {
steerBar.value = value
// Convert 0-1 value to -1 to 1 for direction
steerBar.direction = (value * 2) - 1
}
function onGasChanged(value) {
gasBar.value = value
}
function onBrakeChanged(value) {
brakeBar.value = value
}
function onStatusChanged(text, color) {
statusText.text = text
statusText.color = color
statusDot.color = color
}
function onProfileDescChanged(desc) {
profileDesc.text = desc
}
function onButtonStatusChanged(status) {
buttonStatus.text = status
}
}
// CUSTOM COMPONENTS
component SectionCard: Item {
property string title: ""
property string subtitle: ""
property int cardHeight: 100
default property alias content: contentArea.children
width: parent.width
height: headerRow.height + card.height + 20
Row {
id: headerRow
anchors.left: parent.left
anchors.leftMargin: 24
anchors.top: parent.top
spacing: 10
Text {
text: title
font.pixelSize: 14
font.bold: true
color: colorTextPrimary
font.family: "Segoe UI"
}
Text {
text: " • " + subtitle
font.pixelSize: 11
color: colorTextMuted
font.family: "Segoe UI"
}
}
Rectangle {
id: card
anchors.top: headerRow.bottom
anchors.topMargin: 8
anchors.left: parent.left
anchors.leftMargin: 24
anchors.right: parent.right
anchors.rightMargin: 24
height: cardHeight
radius: 12
color: colorCard
border.color: colorBorder
border.width: 1
Item {
id: contentArea
anchors.fill: parent
anchors.margins: 16
}
}
}
component AxisBar: Item {
property string label: ""
property color barColor: colorPrimary
property real value: 0.0
property real direction: 0.0 // -1 to 1 for steering direction
property var barGradient: [
{position: 0.0, color: "#2dd4bf"},
{position: 0.4, color: "#0d9488"},
{position: 0.8, color: "#0f766e"},
{position: 1.0, color: "#115e59"}
]
width: parent.width
height: 24
Row {
anchors.fill: parent
spacing: 0
Text {
text: label
font.pixelSize: 11
font.bold: true
color: colorTextSecondary
width: 70
anchors.verticalCenter: parent.verticalCenter
font.family: "Segoe UI"
}
Item {
width: parent.width - 70 - 40
height: parent.height
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: axisBarBg
anchors.fill: parent
anchors.leftMargin: 5
anchors.rightMargin: 5
height: 8
radius: 4
color: colorSurface
anchors.verticalCenter: parent.verticalCenter
clip: true
Rectangle {
width: parent.width * Math.max(0, Math.min(1, value))
height: parent.height
radius: 4
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: barGradient[0].position; color: barGradient[0].color }
GradientStop { position: barGradient[1] ? barGradient[1].position : 0.4; color: barGradient[1] ? barGradient[1].color : barGradient[0].color }
GradientStop { position: barGradient[2] ? barGradient[2].position : 0.8; color: barGradient[2] ? barGradient[2].color : barGradient[1] ? barGradient[1].color : barGradient[0].color }
GradientStop { position: barGradient[3] ? barGradient[3].position : 1.0; color: barGradient[3] ? barGradient[3].color : barGradient[2] ? barGradient[2].color : barGradient[1] ? barGradient[1].color : barGradient[0].color }
}
Behavior on width {
NumberAnimation { duration: 50; easing.type: Easing.OutQuad }
}
}
}
}
Text {
text: Math.round(Math.max(0, Math.min(1, value)) * 100) + "%"
font.pixelSize: 10
color: colorTextMuted
width: 30
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
font.family: "Segoe UI"
}
}
}
component StyledButton: Rectangle {
id: btn
property string text: ""
property bool primary: false
property bool danger: false
property bool secondary: false
property int fontSize: 12
signal clicked()
width: 100
height: 38
radius: secondary ? 8 : 10
color: {
if (danger) return btnMouse.containsMouse ? "#dc2626" : colorError
if (primary) return btnMouse.containsMouse ? colorPrimaryHover : colorPrimary
return btnMouse.containsMouse ? colorSurfaceHover : colorSurface
}
border.color: secondary ? colorBorder : "transparent"
border.width: secondary ? 1 : 0
Text {
anchors.centerIn: parent
text: btn.text
font.pixelSize: fontSize
font.bold: true
color: primary || danger ? colorBg : (secondary ? colorTextSecondary : colorTextPrimary)
font.family: "Segoe UI"
}
MouseArea {
id: btnMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: btn.clicked()
}
Behavior on color {
ColorAnimation { duration: 150 }
}
}
component StyledComboBox: Rectangle {
id: combo
property var model: []
property int currentIndex: 0
property string currentText: model[currentIndex] || ""
width: 200
height: 38
radius: 8
color: comboMouse.containsMouse ? colorSurfaceHover : colorSurface
border.color: colorBorder
border.width: 1
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: combo.currentText
font.pixelSize: 12
color: colorTextPrimary
font.family: "Segoe UI"
elide: Text.ElideRight
width: parent.width - 40
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "▼"
font.pixelSize: 8
color: colorTextMuted
}
MouseArea {
id: comboMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: popup.open()
}
Popup {
id: popup
y: parent.height + 4
width: parent.width
height: Math.min(listView.contentHeight + 8, 300)
padding: 4
background: Rectangle {
color: colorSurface
radius: 8
border.color: colorBorder
border.width: 1
}
contentItem: ListView {
id: listView
clip: true
model: combo.model
currentIndex: combo.currentIndex
delegate: Rectangle {
width: ListView.view.width
height: 32
color: delegateMouse.containsMouse ? colorSurfaceHover : "transparent"
radius: 6
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: 12
color: colorTextPrimary
font.family: "Segoe UI"
}
MouseArea {
id: delegateMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
combo.currentIndex = index
popup.close()
}
}
}
}
}
Behavior on color {
ColorAnimation { duration: 150 }
}
}
component StyledCheckBox: Item {
property string text: ""
property bool checked: false
signal toggled()
width: checkRow.width
height: 20
Row {
id: checkRow
spacing: 8
Rectangle {
width: 16
height: 16
radius: 4
color: checked ? colorPrimary : "transparent"
border.color: checked ? colorPrimary : colorBorderLight
border.width: 1
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "✓"
font.pixelSize: 10
font.bold: true
color: colorBg
visible: checked
}
Behavior on color {
ColorAnimation { duration: 150 }
}
Behavior on border.color {
ColorAnimation { duration: 150 }
}
}
Text {
text: parent.parent.text
font.pixelSize: 11
color: colorTextSecondary
anchors.verticalCenter: parent.verticalCenter
font.family: "Segoe UI"
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
checked = !checked
toggled()
}
}
}
}
"""
class TrueAxisBackend(QObject):
devicesChanged = Signal(list)
profileDescChanged = Signal(str)
steeringChanged = Signal(float)
gasChanged = Signal(float)
brakeChanged = Signal(float)
statusChanged = Signal(str, str)
isRunningChanged = Signal(bool)
buttonStatusChanged = Signal(str)
axisValuesChanged = Signal()
buttonStatesChanged = Signal()
numAxesChanged = Signal()
numButtonsChanged = Signal()
def __init__(self):
super().__init__()
self._running = False
self._devices = []
self._current_device_index = 0
self._current_profile = "Logitech G29 (Standard)"
self._invert_gas = False
self._invert_brake = False
self._square_input = False
self._square_area = 0.5 # Default: 50% square area
self.mapper_thread = None
self.vg_gamepad = None
self.joystick = None
self.button_mapping = {}
self._axis_values = []
self._button_states = []
pygame.init()
pygame.joystick.init()
self.update_timer = QTimer()
self.update_timer.timeout.connect(self.update_inputs)
self.update_timer.start(50)
self.load_button_mapping()
self.refresh_devices()
@Property(bool, notify=isRunningChanged)
def running(self):
return self._running
@Property(list, notify=devicesChanged)
def devices(self):
return self._devices
@Property(list, constant=True)
def profileNames(self):
return list(PROFILES.keys())
@Property(list, constant=True)
def xboxButtonNames(self):
return XBOX_BUTTON_NAMES
@Property(int, notify=numAxesChanged)
def numAxes(self):
if self.joystick:
try:
return self.joystick.get_numaxes()
except:
pass
return 0
@Property(int, notify=numButtonsChanged)
def numButtons(self):
if self.joystick:
try:
return self.joystick.get_numbuttons()
except:
pass
return 0
@Slot(int, result=float)
def getAxisValue(self, index):
if index < len(self._axis_values):
return max(0.0, min(1.0, self._axis_values[index]))
return 0.5
@Slot(int, result=str)
def getAxisValueText(self, index):
if index < len(self._axis_values):
raw = (self._axis_values[index] - 0.5) * 2
return f"{raw:+.2f}"
return "0.00"
@Slot(int, result=bool)
def getButtonState(self, index):
if index < len(self._button_states):
return self._button_states[index]
return False
@Slot(int, result=str)
def get_button_mapping(self, button_index):
return self.button_mapping.get(str(button_index), "DISABLED")
@Slot(int, str)
def set_button_mapping(self, button_index, xbox_button):
if xbox_button == "DISABLED":
if str(button_index) in self.button_mapping:
del self.button_mapping[str(button_index)]
else:
self.button_mapping[str(button_index)] = xbox_button
@Slot()
def save_button_mapping(self):
try:
with open(BUTTON_MAPPING_FILE, 'w') as f:
json.dump(self.button_mapping, f, indent=2)
self.update_button_status()
except:
pass
def load_button_mapping(self):
if os.path.exists(BUTTON_MAPPING_FILE):
try:
with open(BUTTON_MAPPING_FILE, 'r') as f:
self.button_mapping = json.load(f)
self.update_button_status()
except:
pass
def update_button_status(self):
count = len(self.button_mapping)
if count == 0:
status = "No buttons mapped"
elif count == 1:
status = "1 button mapped"
else:
status = f"{count} buttons mapped"
self.buttonStatusChanged.emit(status)
@Slot()
def refresh_devices(self):
try:
pygame.joystick.quit()
pygame.joystick.init()
count = pygame.joystick.get_count()
self._devices = [pygame.joystick.Joystick(i).get_name() for i in range(count)]
if not self._devices:
self._devices = ["No devices found"]
self.devicesChanged.emit(self._devices)
if count > 0:
self.init_joystick(0)
except Exception as e:
print(f"Error refreshing devices: {e}")
self._devices = ["Error scanning devices"]
self.devicesChanged.emit(self._devices)
@Slot(int)
def select_device(self, index):
if not self._running and 0 <= index < len(self._devices):
self._current_device_index = index
self.init_joystick(index)
@Slot(str)
def select_profile(self, profile_name):
if profile_name in PROFILES:
self._current_profile = profile_name
desc = PROFILES[profile_name]["desc"]
self.profileDescChanged.emit(desc)
@Slot(bool)
def set_invert_gas(self, value):
self._invert_gas = value
@Slot(bool)
def set_invert_brake(self, value):
self._invert_brake = value
@Slot(bool)
def set_square_input(self, value):
self._square_input = value
@Slot(float)
def set_square_area(self, value):
self._square_area = value
@Slot()
def toggle_mapping(self):
if self._running:
self.stop_mapping()
else:
self.start_mapping()
def init_joystick(self, index):
try:
if self.joystick:
self.joystick.quit()
self.joystick = pygame.joystick.Joystick(index)
self.joystick.init()
num_axes = self.joystick.get_numaxes()
num_buttons = self.joystick.get_numbuttons()
self._axis_values = [0.5] * num_axes
self._button_states = [False] * num_buttons
self.numAxesChanged.emit()
self.numButtonsChanged.emit()
except Exception as e:
print(f"Error initializing joystick: {e}")
self.joystick = None
def update_inputs(self):
if not self.joystick:
return
try:
if not self._running:
pygame.event.pump()
for i in range(self.joystick.get_numaxes()):
raw = self.joystick.get_axis(i)
self._axis_values[i] = (raw + 1) / 2
self.axisValuesChanged.emit()
for i in range(self.joystick.get_numbuttons()):
self._button_states[i] = self.joystick.get_button(i)
self.buttonStatesChanged.emit()
if self._current_profile in PROFILES:
mapping = PROFILES[self._current_profile]["axes"]
def get_axis(idx):
if idx < self.joystick.get_numaxes():
return self.joystick.get_axis(idx)
return 0.0
# Apply square steering if enabled
if mapping[0] < self.joystick.get_numaxes():
raw_s = self.joystick.get_axis(mapping[0])
if self._square_input:
# Square steering logic
normalized = abs(raw_s)
square_threshold = self._square_area
if normalized <= square_threshold:
# Inside square area - linear response
scaled = normalized / square_threshold
else:
# Outside square area - jump to 100%
scaled = 1.0
# Apply direction
final_s = scaled if raw_s >= 0 else -scaled
else:
# Normal circular steering
final_s = raw_s
steer = (final_s + 1) / 2
self.steeringChanged.emit(steer)
gas = (get_axis(mapping[1]) + 1) / 2
if self._invert_gas:
gas = 1.0 - gas
self.gasChanged.emit(gas)
brake = (get_axis(mapping[2]) + 1) / 2
if self._invert_brake:
brake = 1.0 - brake
self.brakeChanged.emit(brake)
except:
pass
def start_mapping(self):
if not self.joystick:
return
self._running = True
self.isRunningChanged.emit(True)
self.statusChanged.emit("Active: Emulating controller", "#22c55e")
self.mapper_thread = threading.Thread(target=self.mapping_loop, daemon=True)
self.mapper_thread.start()
def stop_mapping(self):
self._running = False
self.isRunningChanged.emit(False)
self.statusChanged.emit("Ready to activate", "#71717a")
def apply_square_steering(self, raw_input):
"""Apply square steering transformation based on configurable area"""
if not self._square_input:
return raw_input
normalized = abs(raw_input)
square_threshold = self._square_area
if normalized <= square_threshold:
# Inside square area - linear response
scaled = normalized / square_threshold
else:
# Outside square area - jump to 100%
scaled = 1.0
# Apply direction
return scaled if raw_input >= 0 else -scaled
def mapping_loop(self):
if not self.vg_gamepad:
self.vg_gamepad = vg.VX360Gamepad()
mapping = PROFILES[self._current_profile]["axes"]
while self._running:
try:
pygame.event.pump()
# STEERING with configurable square area
if mapping[0] < self.joystick.get_numaxes():
raw_s = self.joystick.get_axis(mapping[0])
# Apply square steering transformation
final_s = self.apply_square_steering(raw_s)
# Clamp to valid range
final_s = max(-1.0, min(1.0, final_s))
self.vg_gamepad.left_joystick(x_value=int(final_s * 32767), y_value=0)
# GAS with invert
if mapping[1] < self.joystick.get_numaxes():
raw_g = self.joystick.get_axis(mapping[1])
norm_g = (raw_g + 1) / 2
if self._invert_gas:
norm_g = 1.0 - norm_g
self.vg_gamepad.right_trigger(value=int(max(0.0, min(1.0, norm_g)) * 255))
# BRAKE with invert
if mapping[2] < self.joystick.get_numaxes():
raw_b = self.joystick.get_axis(mapping[2])
norm_b = (raw_b + 1) / 2
if self._invert_brake:
norm_b = 1.0 - norm_b
self.vg_gamepad.left_trigger(value=int(max(0.0, min(1.0, norm_b)) * 255))
# BUTTONS
num_btns = self.joystick.get_numbuttons()
for btn_idx_str, xbox_name in self.button_mapping.items():
btn_idx = int(btn_idx_str)
if btn_idx < num_btns:
xbox_obj = XBOX_BUTTON_MAP.get(xbox_name)
if xbox_obj:
if self.joystick.get_button(btn_idx):
self.vg_gamepad.press_button(xbox_obj)
else:
self.vg_gamepad.release_button(xbox_obj)
self.vg_gamepad.update()
time.sleep(0.001)
except:
time.sleep(1)
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
backend = TrueAxisBackend()
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("backend", backend)
engine.loadData(QML_CODE.encode())
if not engine.rootObjects():
print("ERROR: Failed to load QML!")
sys.exit(-1)
sys.exit(app.exec())