TrueAxis Source
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.
Back to site

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())