Skip to content

Commit d4fbc8e

Browse files
committed
Add dialog for the auto mapper
1 parent 1cd398b commit d4fbc8e

5 files changed

Lines changed: 280 additions & 0 deletions

File tree

gremlin/ui/auto_mapper.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# -*- coding: utf-8; -*-
2+
3+
# Copyright (C) 2015 - 2025 Lionel Ott
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
import logging
19+
20+
from PySide6 import QtCore
21+
from PySide6 import QtQml
22+
23+
import dill
24+
from gremlin import auto_mapper
25+
from gremlin.ui import backend
26+
27+
QML_IMPORT_NAME = "Gremlin.UI"
28+
QML_IMPORT_MAJOR_VERSION = 1
29+
30+
31+
@QtQml.QmlElement
32+
class AutoMapper(QtCore.QObject):
33+
def __init__(self, parent=None):
34+
super().__init__(parent)
35+
36+
@QtCore.Slot(dict, dict, bool, bool, result=bool)
37+
def create_mappings(self, physical_devices, vjoy_devices, overwrite, repeat):
38+
"""
39+
Create mappings between physical and vJoy devices.
40+
41+
Args:
42+
physical_devices: Dictionary of {device_guid: is_selected} for physical devices
43+
vjoy_devices: Dictionary of {vjoy_id: is_selected} for vJoy devices
44+
overwrite: Whether to overwrite existing mappings
45+
repeat: Whether to repeat vJoy mappings
46+
"""
47+
logging.getLogger("system").info(
48+
"Creating mappings from physical devices %s to vJoy devices %s, "
49+
"options overwrite: %s, repeat: %s",
50+
physical_devices,
51+
vjoy_devices,
52+
overwrite,
53+
repeat,
54+
)
55+
mapper = auto_mapper.AutoMapper(backend.Backend().profile)
56+
mapper.generate_mappings(
57+
[
58+
dill.GUID.from_str(guid)
59+
for (guid, chosen) in physical_devices.items()
60+
if chosen
61+
],
62+
[int(vjoy_id) for (vjoy_id, chosen) in vjoy_devices.items() if chosen],
63+
auto_mapper.AutoMapperOptions(
64+
repeat_vjoy_inputs=repeat,
65+
overwrite_used_inputs=overwrite,
66+
),
67+
)

gremlin/ui/backend.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from gremlin.logical_device import LogicalDevice
3333
from gremlin.signal import signal
3434

35+
from gremlin.ui.auto_mapper import AutoMapper
3536
from gremlin.ui.device import InputIdentifier, LogicalDeviceManagementModel
3637
from gremlin.ui.profile import InputItemModel, ModeHierarchyModel
3738
from gremlin.ui.script import ScriptListModel
@@ -313,6 +314,10 @@ def getInputItem(
313314
@Slot(result=LogicalDeviceManagementModel)
314315
def getLogicalDeviceManagementModel(self) -> LogicalDeviceManagementModel:
315316
return LogicalDeviceManagementModel(self)
317+
318+
@Slot(result=AutoMapper)
319+
def getAutoMapper(self) -> AutoMapper:
320+
return AutoMapper(self)
316321

317322
@Slot(str, int, result=bool)
318323
def isActionExpanded(self, uuid_str: str, index: int) -> bool:

gremlin/ui/device.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ class DeviceListModel(QtCore.QAbstractListModel):
248248
QtCore.Qt.UserRole + 6: QtCore.QByteArray("vid".encode()),
249249
QtCore.Qt.UserRole + 7: QtCore.QByteArray("guid".encode()),
250250
QtCore.Qt.UserRole + 8: QtCore.QByteArray("joy_id".encode()),
251+
QtCore.Qt.UserRole + 9: QtCore.QByteArray("vjoy_id".encode()),
251252
}
252253

253254
role_query = {
@@ -259,6 +260,7 @@ class DeviceListModel(QtCore.QAbstractListModel):
259260
"vid": lambda dev: "{:04X}".format(dev.vendor_id),
260261
"guid": lambda dev: str(dev.device_guid),
261262
"joy_id": lambda dev: dev.joystick_id,
263+
"vjoy_id": lambda dev: dev.vjoy_id,
262264
}
263265

264266
def __init__(self, parent=None):

qml/DialogAutoMapper.qml

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// -*- coding: utf-8; -*-
2+
//
3+
// Copyright (C) 2015 - 2025 Lionel Ott
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
19+
import QtQuick
20+
import QtQuick.Controls
21+
import QtQuick.Layouts
22+
import QtQuick.Window
23+
24+
import QtQuick.Controls.Universal
25+
26+
import Gremlin.Device
27+
import Gremlin.Profile
28+
import Gremlin.UI
29+
30+
31+
Window {
32+
id: _root
33+
34+
minimumWidth: 900
35+
minimumHeight: 500
36+
37+
title: "Auto Mapper"
38+
39+
DeviceListModel {
40+
id: physicalDevices
41+
deviceType: "physical"
42+
}
43+
44+
DeviceListModel {
45+
id: virtualDevices
46+
deviceType: "virtual"
47+
}
48+
49+
property AutoMapper autoMapper: backend.getAutoMapper()
50+
51+
// Properties to track the selected devices
52+
property var selectedPhysicalDevices: ({})
53+
property var selectedVJoyDevices: ({})
54+
property bool overwriteNonEmpty: false
55+
property bool repeatVJoy: false
56+
57+
// Main layout container
58+
Rectangle {
59+
anchors.fill: parent
60+
anchors.margins: 10
61+
62+
Column {
63+
id: mainColumn
64+
anchors.fill: parent
65+
spacing: 10
66+
67+
// Main content area
68+
Row {
69+
id: contentRow
70+
width: parent.width
71+
height: parent.height - 60 // Leave space for the button
72+
spacing: 20
73+
74+
// Left side - Physical devices
75+
Column {
76+
id: physicalColumn
77+
width: parent.width * 0.45
78+
height: parent.height
79+
spacing: 5
80+
81+
// Overwrite non-empty physical inputs toggle
82+
CheckBox {
83+
id: overwriteCheckbox
84+
width: parent.width
85+
text: qsTr("Overwrite non-empty physical inputs")
86+
checked: overwriteNonEmpty
87+
onCheckedChanged: overwriteNonEmpty = checked
88+
}
89+
90+
// Physical devices list
91+
GroupBox {
92+
width: parent.width
93+
height: parent.height - overwriteCheckbox.height - 5
94+
title: qsTr("Physical Devices")
95+
96+
ScrollView {
97+
id: physicalScroll
98+
anchors.fill: parent
99+
clip: true
100+
101+
Column {
102+
width: physicalScroll.width - 20 // Account for padding
103+
spacing: 5
104+
padding: 5
105+
106+
Repeater {
107+
model: physicalDevices
108+
delegate: CheckBox {
109+
width: parent.width - 10
110+
text: model.name
111+
checked: false
112+
onCheckedChanged: {
113+
selectedPhysicalDevices[model.guid] = checked;
114+
}
115+
}
116+
}
117+
}
118+
}
119+
}
120+
}
121+
122+
// Right side - vJoy devices
123+
Column {
124+
id: vjoyColumn
125+
width: parent.width * 0.45
126+
height: parent.height
127+
spacing: 5
128+
129+
// Repeat vJoy devices toggle
130+
CheckBox {
131+
id: repeatVJoyCheckbox
132+
width: parent.width
133+
text: qsTr("Repeat vJoy devices")
134+
checked: repeatVJoy
135+
onCheckedChanged: repeatVJoy = checked
136+
}
137+
138+
// vJoy devices list
139+
GroupBox {
140+
width: parent.width
141+
height: parent.height - repeatVJoyCheckbox.height - 5
142+
title: qsTr("vJoy Devices")
143+
144+
ScrollView {
145+
id: vjoyScroll
146+
anchors.fill: parent
147+
clip: true
148+
149+
Column {
150+
width: vjoyScroll.width - 20 // Account for padding
151+
spacing: 5
152+
padding: 5
153+
154+
Repeater {
155+
model: virtualDevices
156+
delegate: CheckBox {
157+
width: parent.width - 10
158+
text: model.name
159+
checked: false
160+
onCheckedChanged: {
161+
selectedVJoyDevices[model.vjoy_id] = checked;
162+
}
163+
}
164+
}
165+
}
166+
}
167+
}
168+
}
169+
}
170+
171+
// Create Mappings button
172+
Button {
173+
id: createButton
174+
text: qsTr("Create 1:1 vJoy mappings")
175+
onClicked: {
176+
autoMapper.create_mappings(
177+
selectedPhysicalDevices, selectedVJoyDevices,
178+
overwriteNonEmpty, repeatVJoy);
179+
}
180+
anchors.horizontalCenter: parent.horizontalCenter
181+
width: Math.min(300, parent.width * 0.4)
182+
}
183+
}
184+
}
185+
}

qml/Main.qml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ ApplicationWindow {
150150
text: qsTr("Input Repeater")
151151
//onTriggered: Helpers.createComponent(".qml")
152152
}
153+
MenuItem {
154+
text: qsTr("Auto Mapper")
155+
onTriggered: Helpers.createComponent("DialogAutoMapper.qml")
156+
}
153157
MenuItem {
154158
text: qsTr("Device Information")
155159
onTriggered: Helpers.createComponent("DialogDeviceInformation.qml")
@@ -288,6 +292,23 @@ ApplicationWindow {
288292
}
289293
}
290294

295+
ToolButton {
296+
text: "A"
297+
font.family: "bootstrap-icons"
298+
font.pixelSize: 20
299+
font.weight: 900
300+
301+
ToolTip {
302+
visible: parent.hovered
303+
text: qsTr("Open Auto Mapper")
304+
delay: 500
305+
}
306+
307+
onClicked: {
308+
Helpers.createComponent("DialogAutoMapper.qml")
309+
}
310+
}
311+
291312
Rectangle {
292313
Layout.fillWidth: true
293314
}

0 commit comments

Comments
 (0)