Files
Steerify-main/controller_gui.py

803 lines
30 KiB
Python

import copy
import sys
import time
import pygame
from PySide6.QtCore import Qt, QTimer
from PySide6.QtWidgets import (
QApplication,
QAbstractItemView,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QHeaderView,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QPushButton,
QDoubleSpinBox,
QSpinBox,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
from abstractify import spotifyAbstract
from controller_profiles import (
ACTION_LABELS,
ACTION_ORDER,
controllerFromJoystick,
findMatchingProfile,
getDefaultProfile,
getProfile,
loadConfig,
profileHasController,
saveConfig,
saveControllerProfile,
)
from spotify_settings import DEFAULT_REDIRECT_URI, saveSpotifySettings, spotifySettingsReady
ACTION_HANDLERS = {
"spotify pause play": spotifyAbstract.togglePlayPause,
"spotify next track": spotifyAbstract.nextTrack,
"spotify previous track": spotifyAbstract.previousTrack,
"spotify mute toggle": spotifyAbstract.muteToggle,
}
VOLUME_ACTIONS = ("spotify vol up", "spotify vol down")
class ControllerWindow(QMainWindow):
def __init__(self):
super().__init__()
pygame.init()
pygame.joystick.init()
self.config = loadConfig()
self.controllers = []
self.joysticks = {}
self.currentBindings = copy.deepcopy(getDefaultProfile(self.config)["bindings"])
self.currentButtonSettings = copy.deepcopy(getDefaultProfile(self.config).get("button_settings", {}))
self.lockedBindings = copy.deepcopy(self.currentBindings)
self.lockedButtonSettings = copy.deepcopy(self.currentButtonSettings)
self.currentProfileId = None
self.saveTargetProfileId = None
self.lockedController = None
self.lockedInstanceId = None
self.lastButtonPress = {}
self.lastLockedPressedButtons = set()
self.remapButtonsAlreadyDown = set()
self.buttonCooldown = self.globalDebounceSeconds()
self.remapAction = None
self.updatingControls = False
self.setWindowTitle("Rae's Steering Mapper")
self.buildGui()
self.refreshControllers()
self.timer = QTimer(self)
self.timer.timeout.connect(self.readControllerEvents)
self.timer.start(16)
def buildGui(self):
self.statusLabel = QLabel("Checking for controllers...")
self.debugLabel = QLabel("Last input: none")
self.debugLabel.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.controllerBox = QComboBox()
self.controllerBox.currentIndexChanged.connect(self.controllerSelectionChanged)
self.refreshButton = QPushButton("Refresh")
self.refreshButton.clicked.connect(self.refreshControllers)
self.lockButton = QPushButton("Lock Controller")
self.lockButton.clicked.connect(self.lockSelectedController)
controllerRow = QHBoxLayout()
controllerRow.addWidget(QLabel("Controller"))
controllerRow.addWidget(self.controllerBox, 1)
controllerRow.addWidget(self.refreshButton)
controllerRow.addWidget(self.lockButton)
self.profileBox = QComboBox()
self.profileBox.currentIndexChanged.connect(self.profileSelectionChanged)
self.profileName = QLineEdit()
self.saveButton = QPushButton("Save Controller Profile")
self.saveButton.clicked.connect(self.saveCurrentProfile)
profileRow = QHBoxLayout()
profileRow.addWidget(QLabel("Mapping"))
profileRow.addWidget(self.profileBox, 1)
profileRow.addWidget(QLabel("Name"))
profileRow.addWidget(self.profileName, 1)
profileRow.addWidget(self.saveButton)
self.buttonPressesLogged = QCheckBox("Log button presses")
self.buttonPressesLogged.setChecked(bool(self.config.get("button_presses_logged", True)))
self.buttonPressesLogged.stateChanged.connect(self.buttonLoggingChanged)
self.globalDebounceInput = QDoubleSpinBox()
self.globalDebounceInput.setRange(0.01, 5.00)
self.globalDebounceInput.setSingleStep(0.05)
self.globalDebounceInput.setDecimals(2)
self.globalDebounceInput.setValue(self.globalDebounceSeconds())
self.globalDebounceInput.valueChanged.connect(self.globalSettingsChanged)
self.globalVolumeInput = QSpinBox()
self.globalVolumeInput.setRange(1, 100)
self.globalVolumeInput.setValue(self.globalVolumeIncrement())
self.globalVolumeInput.valueChanged.connect(self.globalSettingsChanged)
self.globalPollingInput = QSpinBox()
self.globalPollingInput.setRange(25, 5000)
self.globalPollingInput.setSingleStep(25)
self.globalPollingInput.setValue(self.globalPollingIntervalMs())
self.globalPollingInput.valueChanged.connect(self.globalSettingsChanged)
settingsRow = QHBoxLayout()
settingsRow.addWidget(self.buttonPressesLogged)
settingsRow.addStretch(1)
settingsRow.addWidget(QLabel("Global debounce"))
settingsRow.addWidget(self.globalDebounceInput)
settingsRow.addWidget(QLabel("Global volume step"))
settingsRow.addWidget(self.globalVolumeInput)
settingsRow.addWidget(QLabel("Volume repeat ms"))
settingsRow.addWidget(self.globalPollingInput)
self.bindingTable = QTableWidget(0, 5)
self.bindingTable.setHorizontalHeaderLabels(["Button", "Spotify API Call", "Debounce", "Vol Step", "Repeat MS"])
self.bindingTable.setSelectionBehavior(QAbstractItemView.SelectRows)
self.bindingTable.setSelectionMode(QAbstractItemView.SingleSelection)
self.bindingTable.verticalHeader().setVisible(False)
self.bindingTable.setAlternatingRowColors(True)
self.bindingTable.setShowGrid(False)
self.bindingTable.setMinimumHeight(250)
self.bindingTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.bindingTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.bindingTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
self.bindingTable.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
self.bindingTable.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents)
self.mapButton = QPushButton("Capture Button For Selected Action")
self.mapButton.clicked.connect(self.startRemap)
self.clearButton = QPushButton("Clear Selected Mapping")
self.clearButton.clicked.connect(self.clearSelectedMapping)
buttonRow = QHBoxLayout()
buttonRow.addWidget(self.mapButton)
buttonRow.addWidget(self.clearButton)
buttonRow.addStretch(1)
layout = QVBoxLayout()
layout.addWidget(self.statusLabel)
layout.addWidget(self.debugLabel)
layout.addLayout(controllerRow)
layout.addLayout(profileRow)
layout.addLayout(settingsRow)
layout.addWidget(self.bindingTable)
layout.addLayout(buttonRow)
root = QWidget()
root.setLayout(layout)
self.setCentralWidget(root)
self.resize(760, 420)
self.populateProfiles()
self.updateBindingTable()
def populateProfiles(self):
selectedProfileId = self.profileBox.currentData()
self.updatingControls = True
self.profileBox.clear()
for profile in self.config.get("profiles", []):
self.profileBox.addItem(profile.get("name", "Controller"), profile.get("id"))
if selectedProfileId is not None:
self.selectProfile(selectedProfileId)
else:
self.selectProfile(self.config.get("default_profile_id"))
self.updatingControls = False
def selectProfile(self, profileId):
for index in range(self.profileBox.count()):
if self.profileBox.itemData(index) == profileId:
self.profileBox.setCurrentIndex(index)
return
def refreshControllers(self):
pygame.joystick.init()
selectedInstanceId = None
selectedController = self.selectedController()
if selectedController is not None:
selectedInstanceId = selectedController.get("instance_id")
self.controllers = []
self.joysticks = {}
for index in range(pygame.joystick.get_count()):
joystick = pygame.joystick.Joystick(index)
controller = controllerFromJoystick(joystick)
self.controllers.append(controller)
self.joysticks[controller["instance_id"]] = joystick
self.updatingControls = True
self.controllerBox.clear()
for controller in self.controllers:
label = f"{controller['name']} ({controller['numbuttons']} buttons)"
self.controllerBox.addItem(label, controller["instance_id"])
if selectedInstanceId is not None:
for index in range(self.controllerBox.count()):
if self.controllerBox.itemData(index) == selectedInstanceId:
self.controllerBox.setCurrentIndex(index)
break
self.updatingControls = False
self.controllerSelectionChanged()
def selectedController(self):
instanceId = self.controllerBox.currentData()
for controller in self.controllers:
if controller.get("instance_id") == instanceId:
return controller
return None
def controllerSelectionChanged(self):
if self.updatingControls:
return
controller = self.selectedController()
if controller is None:
self.statusLabel.setText("No controller connected.")
self.saveTargetProfileId = None
self.profileName.setText("")
self.currentBindings = copy.deepcopy(getDefaultProfile(self.config)["bindings"])
self.updateBindingTable()
return
matchedProfile = findMatchingProfile(self.config, controller)
if matchedProfile is not None:
self.currentProfileId = matchedProfile["id"]
self.saveTargetProfileId = matchedProfile["id"]
self.currentBindings = copy.deepcopy(matchedProfile["bindings"])
self.currentButtonSettings = copy.deepcopy(matchedProfile.get("button_settings", {}))
self.profileName.setText(matchedProfile["name"])
self.selectProfile(matchedProfile["id"])
self.statusLabel.setText(f"Known controller selected: {controller['name']}")
else:
defaultProfile = getDefaultProfile(self.config)
self.currentProfileId = defaultProfile["id"]
self.saveTargetProfileId = None
self.currentBindings = copy.deepcopy(defaultProfile["bindings"])
self.currentButtonSettings = copy.deepcopy(defaultProfile.get("button_settings", {}))
self.profileName.setText(f"{controller['name']} Profile")
self.selectProfile(defaultProfile["id"])
self.statusLabel.setText(f"Unknown controller selected: {controller['name']}. Using temporary mapping.")
self.updateBindingTable()
def profileSelectionChanged(self):
if self.updatingControls:
return
profile = getProfile(self.config, self.profileBox.currentData())
if profile is None:
return
self.currentProfileId = profile["id"]
self.currentBindings = copy.deepcopy(profile["bindings"])
self.currentButtonSettings = copy.deepcopy(profile.get("button_settings", {}))
if self.saveTargetProfileId is None:
controller = self.selectedController()
if controller is not None:
self.profileName.setText(f"{controller['name']} Profile")
else:
self.profileName.setText(profile["name"])
elif not profileHasController(profile):
self.profileName.setText(profile["name"])
self.statusLabel.setText(f"Mapping selected: {profile['name']}")
self.updateBindingTable()
def buttonLoggingChanged(self):
self.config["button_presses_logged"] = self.buttonPressesLogged.isChecked()
saveConfig(self.config)
def globalSettingsChanged(self):
self.config.setdefault("global_settings", {})
self.config["global_settings"]["debounce_seconds"] = self.globalDebounceInput.value()
self.config["global_settings"]["volume_increment"] = self.globalVolumeInput.value()
self.config["global_settings"]["polling_interval_ms"] = self.globalPollingInput.value()
saveConfig(self.config)
def lockSelectedController(self):
controller = self.selectedController()
if controller is None:
self.statusLabel.setText("No controller selected.")
return
self.lockedInstanceId = controller["instance_id"]
self.lockedController = dict(controller)
self.lockedBindings = copy.deepcopy(self.currentBindings)
self.lockedButtonSettings = copy.deepcopy(self.currentButtonSettings)
self.lastButtonPress = {}
self.lastLockedPressedButtons = self.pressedButtonsForController(controller)
self.statusLabel.setText(f"Locked controller: {controller['name']}")
def saveCurrentProfile(self):
controller = self.selectedController()
if controller is None:
self.statusLabel.setText("No controller selected.")
return
profileName = self.profileName.text().strip()
if not profileName:
profileName = f"{controller['name']} Profile"
savedProfile = saveControllerProfile(
self.config,
controller,
profileName,
self.currentBindings,
self.currentButtonSettings,
self.saveTargetProfileId,
)
self.saveTargetProfileId = savedProfile["id"]
self.currentProfileId = savedProfile["id"]
self.populateProfiles()
self.selectProfile(savedProfile["id"])
self.statusLabel.setText(f"Saved controller profile: {savedProfile['name']}")
def updateBindingTable(self):
self.bindingTable.setRowCount(0)
for row, button in enumerate(self.buttonsForTable()):
binding = f"button:{button}"
self.bindingTable.insertRow(row)
buttonItem = QTableWidgetItem(binding)
buttonItem.setData(Qt.UserRole, button)
buttonItem.setFlags(buttonItem.flags() & ~Qt.ItemIsEditable)
actionBox = QComboBox()
actionBox.addItem(ACTION_LABELS[""], "")
for action in ACTION_ORDER:
actionBox.addItem(ACTION_LABELS[action], action)
actionBox.setCurrentIndex(actionBox.findData(self.currentBindings.get(binding, "")))
actionBox.currentIndexChanged.connect(lambda ignored=None, row=row: self.tableRowChanged(row))
debounceInput = QDoubleSpinBox()
debounceInput.setRange(0.00, 5.00)
debounceInput.setSingleStep(0.05)
debounceInput.setDecimals(2)
debounceInput.setSpecialValueText("Global")
debounceInput.setValue(self.buttonSetting(binding, "debounce_seconds", 0))
debounceInput.valueChanged.connect(lambda ignored=None, row=row: self.tableRowChanged(row))
volumeInput = QSpinBox()
volumeInput.setRange(0, 100)
volumeInput.setSpecialValueText("Global")
volumeInput.setValue(self.buttonSetting(binding, "volume_increment", 0))
volumeInput.valueChanged.connect(lambda ignored=None, row=row: self.tableRowChanged(row))
pollingInput = QSpinBox()
pollingInput.setRange(0, 5000)
pollingInput.setSingleStep(25)
pollingInput.setSpecialValueText("Global")
pollingInput.setValue(self.buttonSetting(binding, "polling_interval_ms", 0))
pollingInput.valueChanged.connect(lambda ignored=None, row=row: self.tableRowChanged(row))
self.bindingTable.setItem(row, 0, buttonItem)
self.bindingTable.setCellWidget(row, 1, actionBox)
self.bindingTable.setCellWidget(row, 2, debounceInput)
self.bindingTable.setCellWidget(row, 3, volumeInput)
self.bindingTable.setCellWidget(row, 4, pollingInput)
self.bindingTable.setRowHeight(row, 34)
self.bindingTable.resizeColumnsToContents()
def buttonsForTable(self):
buttons = set()
controller = self.selectedController()
if controller is not None:
for button in range(controller.get("numbuttons", 0)):
buttons.add(button)
for binding in self.currentBindings.keys():
if binding.startswith("button:"):
buttons.add(int(binding.split(":", 1)[1]))
return sorted(buttons)
def buttonSetting(self, binding, settingName, defaultValue):
return self.currentButtonSettings.get(binding, {}).get(settingName, defaultValue)
def tableRowChanged(self, row):
buttonItem = self.bindingTable.item(row, 0)
if buttonItem is None:
return
binding = buttonItem.text()
actionBox = self.bindingTable.cellWidget(row, 1)
debounceInput = self.bindingTable.cellWidget(row, 2)
volumeInput = self.bindingTable.cellWidget(row, 3)
pollingInput = self.bindingTable.cellWidget(row, 4)
action = actionBox.currentData()
if action:
self.currentBindings[binding] = action
elif binding in self.currentBindings:
del self.currentBindings[binding]
settings = {}
if debounceInput.value() > 0:
settings["debounce_seconds"] = debounceInput.value()
if volumeInput.value() > 0:
settings["volume_increment"] = volumeInput.value()
if pollingInput.value() > 0:
settings["polling_interval_ms"] = pollingInput.value()
if settings:
self.currentButtonSettings[binding] = settings
elif binding in self.currentButtonSettings:
del self.currentButtonSettings[binding]
if self.selectedController() is not None and self.selectedController().get("instance_id") == self.lockedInstanceId:
self.lockedBindings = copy.deepcopy(self.currentBindings)
self.lockedButtonSettings = copy.deepcopy(self.currentButtonSettings)
def selectedAction(self):
selectedRows = self.bindingTable.selectionModel().selectedRows()
if not selectedRows:
return None
row = selectedRows[0].row()
actionBox = self.bindingTable.cellWidget(row, 1)
if actionBox is None:
return None
return actionBox.currentData()
def startRemap(self):
action = self.selectedAction()
if action is None:
self.statusLabel.setText("Select a row with a Spotify action to map.")
return
controller = self.selectedController()
if controller is None:
self.statusLabel.setText("Select a controller before mapping.")
return
self.remapAction = action
self.remapButtonsAlreadyDown = self.pressedButtonsForController(controller)
self.statusLabel.setText(f"Press a controller button for {ACTION_LABELS[action]}.")
def clearSelectedMapping(self):
selectedRows = self.bindingTable.selectionModel().selectedRows()
if not selectedRows:
self.statusLabel.setText("Select a button mapping to clear.")
return
row = selectedRows[0].row()
buttonItem = self.bindingTable.item(row, 0)
actionBox = self.bindingTable.cellWidget(row, 1)
if buttonItem is None or actionBox is None:
return
binding = buttonItem.text()
actionBox.setCurrentIndex(actionBox.findData(""))
if binding in self.currentBindings:
del self.currentBindings[binding]
self.updateBindingTable()
self.statusLabel.setText(f"Cleared mapping for {binding}.")
def readControllerEvents(self):
for event in pygame.event.get():
self.showRawEvent(event)
if event.type == pygame.JOYDEVICEADDED:
self.refreshControllers()
self.statusLabel.setText("New controller connected.")
elif event.type == pygame.JOYDEVICEREMOVED:
if getattr(event, "instance_id", None) == self.lockedInstanceId:
self.lockedController = None
self.lockedInstanceId = None
self.lastLockedPressedButtons = set()
self.statusLabel.setText("Locked controller disconnected.")
self.refreshControllers()
elif event.type in (pygame.JOYBUTTONDOWN, pygame.CONTROLLERBUTTONDOWN):
self.handleButtonPress(event)
self.pollControllerButtons()
def handleButtonPress(self, event):
if self.remapAction is not None:
if not self.eventIsFromSelectedController(event):
return
self.setButtonMapping(event.button, self.remapAction)
self.statusLabel.setText(f"Mapped {ACTION_LABELS[self.remapAction]} to button:{event.button}.")
self.remapAction = None
return
if self.lockedController is None:
return
if not self.eventMatchesController(event, self.lockedController):
return
self.runMappedButton(event.button)
def runMappedButton(self, buttonPressed):
currentTime = time.time()
binding = f"button:{buttonPressed}"
action = self.lockedBindings.get(binding)
if action is None:
return
throttleSeconds = self.debounceSecondsForButton(binding)
if action in VOLUME_ACTIONS:
throttleSeconds = self.pollingSecondsForButton(binding)
if buttonPressed in self.lastButtonPress:
if currentTime - self.lastButtonPress[buttonPressed] < throttleSeconds:
return
self.lastButtonPress[buttonPressed] = currentTime
if self.buttonPressesLogged.isChecked():
print(f"Button pressed: {buttonPressed}, Action: {action}")
if action == "spotify vol up":
spotifyAbstract.volumeUpBy(self.volumeIncrementForButton(binding))
return
if action == "spotify vol down":
spotifyAbstract.volumeDownBy(self.volumeIncrementForButton(binding))
return
handler = ACTION_HANDLERS.get(action)
if handler is not None:
handler()
def pollControllerButtons(self):
if self.remapAction is not None:
controller = self.selectedController()
if controller is None:
return
pressedButtons = self.pressedButtonsForController(controller)
self.showPolledButtons(controller, pressedButtons)
newPressedButtons = pressedButtons - self.remapButtonsAlreadyDown
if newPressedButtons:
buttonPressed = min(newPressedButtons)
self.setButtonMapping(buttonPressed, self.remapAction)
self.statusLabel.setText(f"Mapped {ACTION_LABELS[self.remapAction]} to button:{buttonPressed}.")
self.remapAction = None
self.remapButtonsAlreadyDown = set()
elif not pressedButtons:
self.remapButtonsAlreadyDown = set()
return
if self.lockedController is None:
return
pressedButtons = self.pressedButtonsForController(self.lockedController)
self.showPolledButtons(self.lockedController, pressedButtons)
newPressedButtons = pressedButtons - self.lastLockedPressedButtons
self.lastLockedPressedButtons = pressedButtons
for buttonPressed in sorted(pressedButtons):
binding = f"button:{buttonPressed}"
if self.lockedBindings.get(binding) in VOLUME_ACTIONS:
self.runMappedButton(buttonPressed)
for buttonPressed in sorted(newPressedButtons):
binding = f"button:{buttonPressed}"
if self.lockedBindings.get(binding) not in VOLUME_ACTIONS:
self.runMappedButton(buttonPressed)
def pressedButtonsForController(self, controller):
if controller is None:
return set()
try:
pygame.event.pump()
joystick = self.joysticks.get(controller.get("instance_id"))
if joystick is None:
joystick = pygame.joystick.Joystick(controller["index"])
joystick.init()
pressedButtons = set()
for button in range(joystick.get_numbuttons()):
if joystick.get_button(button):
pressedButtons.add(button)
return pressedButtons
except pygame.error:
return set()
def eventIsFromSelectedController(self, event):
controller = self.selectedController()
if controller is None:
return False
return self.eventMatchesController(event, controller)
def eventMatchesController(self, event, controller):
eventInstanceId = getattr(event, "instance_id", None)
if eventInstanceId is not None and eventInstanceId == controller.get("instance_id"):
return True
eventJoyIndex = getattr(event, "joy", None)
if eventJoyIndex is not None and eventJoyIndex == controller.get("index"):
return True
eventWhich = getattr(event, "which", None)
if eventWhich is not None and eventWhich == controller.get("instance_id"):
return True
return False
def showRawEvent(self, event):
eventName = pygame.event.event_name(event.type)
if event.type in (pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP, pygame.CONTROLLERBUTTONDOWN, pygame.CONTROLLERBUTTONUP):
self.debugLabel.setText(
f"Last input: event={eventName}, button={getattr(event, 'button', None)}, "
f"instance_id={getattr(event, 'instance_id', None)}, joy={getattr(event, 'joy', None)}, "
f"which={getattr(event, 'which', None)}"
)
elif event.type in (pygame.JOYDEVICEADDED, pygame.JOYDEVICEREMOVED):
self.debugLabel.setText(
f"Last input: event={eventName}, instance_id={getattr(event, 'instance_id', None)}, "
f"device_index={getattr(event, 'device_index', None)}, which={getattr(event, 'which', None)}"
)
def showPolledButtons(self, controller, pressedButtons):
self.debugLabel.setText(
f"Last input: polling {controller.get('name')} index={controller.get('index')} "
f"instance_id={controller.get('instance_id')} pressed={sorted(pressedButtons)}"
)
def setButtonMapping(self, button, action):
self.currentBindings[f"button:{button}"] = action
self.updateBindingTable()
if self.selectedController() is not None and self.selectedController().get("instance_id") == self.lockedInstanceId:
self.lockedBindings = copy.deepcopy(self.currentBindings)
self.lockedButtonSettings = copy.deepcopy(self.currentButtonSettings)
def globalDebounceSeconds(self):
return self.config.get("global_settings", {}).get("debounce_seconds", 0.25)
def globalVolumeIncrement(self):
return self.config.get("global_settings", {}).get("volume_increment", 5)
def globalPollingIntervalMs(self):
return self.config.get("global_settings", {}).get("polling_interval_ms", 120)
def debounceSecondsForButton(self, binding):
return self.lockedButtonSettings.get(binding, {}).get("debounce_seconds", self.globalDebounceSeconds())
def volumeIncrementForButton(self, binding):
return self.lockedButtonSettings.get(binding, {}).get("volume_increment", self.globalVolumeIncrement())
def pollingSecondsForButton(self, binding):
intervalMs = self.lockedButtonSettings.get(binding, {}).get("polling_interval_ms", self.globalPollingIntervalMs())
return intervalMs / 1000
class SpotifySetupDialog(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Spotify Setup")
self.clientIdInput = QLineEdit()
self.clientSecretInput = QLineEdit()
self.clientSecretInput.setEchoMode(QLineEdit.Password)
self.redirectUriInput = QLineEdit(DEFAULT_REDIRECT_URI)
self.messageLabel = QLabel("Enter your Spotify app credentials before controller setup.")
form = QFormLayout()
form.addRow("Client ID", self.clientIdInput)
form.addRow("Client Secret", self.clientSecretInput)
form.addRow("Redirect URI", self.redirectUriInput)
self.buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
self.buttons.accepted.connect(self.saveAndAccept)
self.buttons.rejected.connect(self.reject)
layout = QVBoxLayout()
layout.addWidget(self.messageLabel)
layout.addLayout(form)
layout.addWidget(self.buttons)
self.setLayout(layout)
self.resize(480, 180)
def saveAndAccept(self):
clientId = self.clientIdInput.text().strip()
clientSecret = self.clientSecretInput.text().strip()
redirectUri = self.redirectUriInput.text().strip()
if not clientId or not clientSecret:
self.messageLabel.setText("Client ID and Client Secret are required.")
return
saveSpotifySettings(clientId, clientSecret, redirectUri)
self.accept()
def runGui():
app = QApplication(sys.argv)
if not spotifySettingsReady():
setupDialog = SpotifySetupDialog()
if setupDialog.exec() != QDialog.Accepted:
sys.exit(1)
window = ControllerWindow()
window.show()
sys.exit(app.exec())