diff --git a/src/bridgesourcesink.py b/src/bridgesourcesink.py index cabbe10..c61e5ca 100755 --- a/src/bridgesourcesink.py +++ b/src/bridgesourcesink.py @@ -21,7 +21,7 @@ from collections import namedtuple -from PyQt5.QtCore import Qt, QRegExp +from PyQt5.QtCore import Qt, QRegExp, pyqtSignal from PyQt5.QtGui import QRegExpValidator from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QLineEdit, QSpinBox, QPushButton, QCheckBox, QHBoxLayout, QWidget from shared import * @@ -61,6 +61,8 @@ class BridgeSourceSink(QTableWidget): channels="2", connected="True") + customChanged = pyqtSignal() + def __init__(self, parent): QTableWidget.__init__(self, parent) self.bridgeData = [] @@ -79,7 +81,7 @@ class BridgeSourceSink(QTableWidget): # Name name_col = QLineEdit() name_col.setText(data.name) - name_col.returnPressed.connect(self.enable_buttons) + name_col.textChanged.connect(self.customChanged.emit) rx = QRegExp("[^|]+") validator = QRegExpValidator(rx, self) name_col.setValidator(validator) @@ -100,21 +102,22 @@ class BridgeSourceSink(QTableWidget): combo_box.addItem(loudspeaker_icon, "sink") combo_box.setCurrentIndex(0 if data.s_type == "source" else 1) - combo_box.currentTextChanged.connect(self.enable_buttons) + combo_box.currentTextChanged.connect(self.customChanged.emit) self.setCellWidget(row, 1, combo_box) # Channels chan_col = QSpinBox() chan_col.setValue(int(data.channels)) chan_col.setMinimum(1) - chan_col.valueChanged.connect(self.enable_buttons) + chan_col.setAlignment(Qt.AlignCenter) + chan_col.valueChanged.connect(self.customChanged.emit) self.setCellWidget(row, 2, chan_col) # Auto connect? auto_cb = QCheckBox() auto_cb.setObjectName("auto_cb") auto_cb.setCheckState(Qt.Checked if data.connected in ['true', 'True', 'TRUE'] else Qt.Unchecked) - auto_cb.stateChanged.connect(self.enable_buttons) + auto_cb.stateChanged.connect(self.customChanged.emit) widget = QWidget() h_layout = QHBoxLayout(widget) h_layout.addWidget(auto_cb) @@ -124,19 +127,15 @@ class BridgeSourceSink(QTableWidget): self.setCellWidget(row, 3, widget) self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) - def enable_buttons(self): - # Can't work out how to tell the table that data has changed (to cause the buttons to become enabled), - # so instead manually make the buttons enabled. - for btn_name in ["b_bridge_save", "b_bridge_undo"]: - self.parent().findChild(QPushButton, btn_name).setProperty("enabled", True) - def defaults(self): self.bridgeData = [self.defaultPASourceData, self.defaultPASinkData] self.load_data_into_cells() + self.customChanged.emit() def undo(self): self.load_from_settings() self.load_data_into_cells() + self.customChanged.emit() def initialise_settings(self): GlobalSettings.setValue( @@ -147,14 +146,91 @@ class BridgeSourceSink(QTableWidget): bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges") self.bridgeData = self.decode_bridge_data(bridgeDataText) + def hasChanges(self)->bool: + bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges") + saved_data = self.decode_bridge_data(bridgeDataText) + + if self.rowCount() != len(saved_data): + return True + + for row in range(self.rowCount()): + orig_data = saved_data[row] + + name = self.cellWidget(row, 0).text() + if name != orig_data[0]: + return True + + type = self.cellWidget(row, 1).currentText() + if type != orig_data[1]: + return True + + channels = self.cellWidget(row, 2).value() + if channels != int(orig_data[2]): + return True + + auto_cb = self.cellWidget(row, 3).findChild(QCheckBox, "auto_cb") + connected = auto_cb.isChecked() + if connected != bool(orig_data[3]): + return True + + return False + + def hasValidValues(self)->bool: + used_names = [] + + row_count = self.rowCount() + # Prevent save without any bridge + if not row_count: + return False + + for row in range(row_count): + line_edit = self.cellWidget(row, 0) + name = line_edit.text() + + if not name or name in used_names: + # prevent double name entries + return False + + used_names.append(name) + + return True + def add_row(self): - self.bridgeData.append(SSData(name="", s_type="source", channels="2", connected="False")) + # first, search in table which bridge exists + # to add the most pertinent new bridge + has_source = False + has_sink = False + + for row in range(self.rowCount()): + cell_widget = self.cellWidget(row, 1) + + group_type = "" + if cell_widget: + group_type = cell_widget.currentText() + + if group_type == "source": + has_source = True + elif group_type == "sink": + has_sink = True + + if has_source and has_sink: + break + + ss_data = SSData(name="", s_type="source", channels="2", connected="False") + if not has_sink: + ss_data = self.defaultPASinkData + elif not has_source: + ss_data = self.defaultPASourceData + + self.bridgeData.append(ss_data) self.load_data_into_cells() self.editItem(self.item(self.rowCount() - 1, 0)) + self.customChanged.emit() def remove_row(self): del self.bridgeData[self.currentRow()] self.load_data_into_cells() + self.customChanged.emit() def save_bridges(self): self.bridgeData = [] @@ -177,6 +253,7 @@ class BridgeSourceSink(QTableWidget): # Need an extra line at the end conn_file.write("\n") conn_file.close() + self.customChanged.emit() # encode and decode from tuple so it isn't stored in the settings file as a type, and thus the # configuration is backwards compatible with versions that don't understand SSData types. diff --git a/src/cadence.py b/src/cadence.py index 80cd7e0..369c746 100755 --- a/src/cadence.py +++ b/src/cadence.py @@ -1139,7 +1139,7 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW): self.b_bridge_undo.clicked.connect(self.slot_PulseAudioBridgeUndo) self.b_bridge_save.clicked.connect(self.slot_PulseAudioBridgeSave) self.b_bridge_defaults.clicked.connect(self.slot_PulseAudioBridgeDefaults) - self.tableSinkSourceData.itemChanged.connect(self.slot_PulseAudioBridgeTableChanged) + self.tableSinkSourceData.customChanged.connect(self.slot_PulseAudioBridgeTableChanged) self.tableSinkSourceData.doubleClicked.connect(self.slot_PulseAudioBridgeTableChanged) self.pic_catia.clicked.connect(self.func_start_catia) @@ -1236,11 +1236,13 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW): except: version, groups, conns = (list(), list(), list()) + pa_first_group_name = self.getFirstPulseAudioGroupName() + for group_id, group_name, ports in groups: if group_name == "alsa2jack": global jackClientIdALSA jackClientIdALSA = group_id - elif group_name == "PulseAudio JACK Sink": + elif group_name in (pa_first_group_name, "PulseAudio JACK Sink"): global jackClientIdPulse jackClientIdPulse = group_id @@ -1415,6 +1417,14 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW): self.systray.setActionEnabled("a2j_stop", False) self.label_bridge_a2j.setText(self.tr("ALSA MIDI Bridge is stopped")) + def getFirstPulseAudioGroupName(self)->str: + # search PulseAudio JACK first group in settings + pa_bridges_settings = GlobalSettings.value("Pulse2JACK/PABridges", type=list) + if pa_bridges_settings: + return pa_bridges_settings[0].partition('|')[0] + + return "PulseAudio JACK Sink" + def checkAlsaAudio(self): asoundrcFile = os.path.join(HOME, ".asoundrc") @@ -1626,11 +1636,13 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW): @pyqtSlot(int, str) def slot_DBusJackClientAppearedCallback(self, group_id, group_name): + pa_first_group_name = self.getFirstPulseAudioGroupName() + if group_name == "alsa2jack": global jackClientIdALSA jackClientIdALSA = group_id self.checkAlsaAudio() - elif group_name == "PulseAudio JACK Sink": + elif group_name in ("PulseAudio JACK Sink", pa_first_group_name): global jackClientIdPulse jackClientIdPulse = group_id self.checkPulseAudio() @@ -1809,38 +1821,30 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW): @pyqtSlot() def slot_PulseAudioBridgeTableChanged(self): - self.b_bridge_save.setEnabled(True) - self.b_bridge_undo.setEnabled(True) + has_changes = self.tableSinkSourceData.hasChanges() + has_valid_values = self.tableSinkSourceData.hasValidValues() + self.b_bridge_save.setEnabled(has_changes and has_valid_values) + self.b_bridge_undo.setEnabled(has_changes) @pyqtSlot() def slot_PulseAudioBridgeAdd(self): self.tableSinkSourceData.add_row() - self.b_bridge_save.setEnabled(True) - self.b_bridge_undo.setEnabled(True) @pyqtSlot() def slot_PulseAudioBridgeRemove(self): self.tableSinkSourceData.remove_row() - self.b_bridge_save.setEnabled(True) - self.b_bridge_undo.setEnabled(True) @pyqtSlot() def slot_PulseAudioBridgeUndo(self): self.tableSinkSourceData.undo() - self.b_bridge_save.setEnabled(False) - self.b_bridge_undo.setEnabled(False) @pyqtSlot() def slot_PulseAudioBridgeSave(self): self.tableSinkSourceData.save_bridges() - self.b_bridge_save.setEnabled(False) - self.b_bridge_undo.setEnabled(False) @pyqtSlot() def slot_PulseAudioBridgeDefaults(self): self.tableSinkSourceData.defaults() - self.b_bridge_save.setEnabled(True) - self.b_bridge_undo.setEnabled(True) @pyqtSlot() def slot_PulseAudioBridgeOptions(self):