#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Custom QTableWidget that handles pulseaudio source and sinks # Copyright (C) 2011-2018 Filipe Coelho # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # For a full copy of the GNU General Public License see the COPYING file # --------------------------------------------------------------------- # Imports (Global) from collections import namedtuple from PyQt5.QtCore import Qt, QRegExp from PyQt5.QtGui import QRegExpValidator from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QLineEdit, QSpinBox, QPushButton, QCheckBox, QHBoxLayout, QWidget from shared import * from shared_cadence import GlobalSettings # Python3/4 function name normalisation try: range = xrange except NameError: pass PULSE_USER_CONFIG_DIR = os.getenv("PULSE_USER_CONFIG_DIR") if not PULSE_USER_CONFIG_DIR: PULSE_USER_CONFIG_DIR = os.path.join(HOME, ".pulse") if not os.path.exists(PULSE_USER_CONFIG_DIR): os.path.mkdir(PULSE_USER_CONFIG_DIR) # a data class to hold the Sink/Source Data. Use strings in tuple for easy map(_make) # but convert to type in table for editor SSData = namedtuple('SSData', 'name s_type channels connected') # --------------------------------------------------------------------- # Extend QTableWidget to hold Sink/Source data class BridgeSourceSink(QTableWidget): defaultPASourceData = SSData( name="PulseAudio JACK Source", s_type="source", channels="2", connected="True") defaultPASinkData = SSData( name="PulseAudio JACK Sink", s_type="sink", channels="2", connected="True") def __init__(self, parent): QTableWidget.__init__(self, parent) self.bridgeData = [] if not GlobalSettings.contains("Pulse2JACK/PABridges"): self.initialise_settings() self.load_from_settings() def load_data_into_cells(self): self.setHorizontalHeaderLabels(['Name', 'Type', 'Channels', 'Conn?']) self.setRowCount(0) for data in self.bridgeData: row = self.rowCount() self.insertRow(row) # Name name_col = QLineEdit() name_col.setText(data.name) name_col.returnPressed.connect(self.enable_buttons) rx = QRegExp("[^|]+") validator = QRegExpValidator(rx, self) name_col.setValidator(validator) self.setCellWidget(row, 0, name_col) # Type combo_box = QComboBox() microphone_icon = QIcon.fromTheme('audio-input-microphone') if microphone_icon.isNull(): microphone_icon = QIcon.fromTheme('microphone') loudspeaker_icon = QIcon.fromTheme('audio-volume-high') if loudspeaker_icon.isNull(): loudspeaker_icon = QIcon.fromTheme('player-volume') combo_box.addItem(microphone_icon, "source") combo_box.addItem(loudspeaker_icon, "sink") combo_box.setCurrentIndex(0 if data.s_type == "source" else 1) combo_box.currentTextChanged.connect(self.enable_buttons) 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) 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) widget = QWidget() h_layout = QHBoxLayout(widget) h_layout.addWidget(auto_cb) h_layout.setAlignment(Qt.AlignCenter) h_layout.setContentsMargins(0, 0, 0, 0) widget.setLayout(h_layout) 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() def undo(self): self.load_from_settings() self.load_data_into_cells() def initialise_settings(self): GlobalSettings.setValue( "Pulse2JACK/PABridges", self.encode_bridge_data([self.defaultPASourceData, self.defaultPASinkData])) def load_from_settings(self): bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges") self.bridgeData = self.decode_bridge_data(bridgeDataText) def add_row(self): self.bridgeData.append(SSData(name="", s_type="source", channels="2", connected="False")) self.load_data_into_cells() self.editItem(self.item(self.rowCount() - 1, 0)) def remove_row(self): del self.bridgeData[self.currentRow()] self.load_data_into_cells() def save_bridges(self): self.bridgeData = [] for row in range(0, self.rowCount()): new_name = self.cellWidget(row, 0).property("text") new_type = self.cellWidget(row, 1).currentText() new_channels = self.cellWidget(row, 2).value() auto_cb = self.cellWidget(row, 3).findChild(QCheckBox, "auto_cb") new_conn = auto_cb.checkState() == Qt.Checked self.bridgeData.append( SSData(name=new_name, s_type=new_type, channels=new_channels, connected=str(new_conn))) GlobalSettings.setValue("Pulse2JACK/PABridges", self.encode_bridge_data(self.bridgeData)) conn_file_path = os.path.join(PULSE_USER_CONFIG_DIR, "jack-connections") conn_file = open(conn_file_path, "w") conn_file.write("\n".join(self.encode_bridge_data(self.bridgeData))) # Need an extra line at the end conn_file.write("\n") conn_file.close() # 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. # Uses PIPE symbol as separator def encode_bridge_data(self, data): return list(map(lambda s: s.name + "|" + s.s_type + "|" + str(s.channels) + "|" + str(s.connected), data)) def decode_bridge_data(self, data): return list(map(lambda d: SSData._make(d.split("|")), data)) def resizeEvent(self, event): self.setColumnWidth(0, int(self.width() * 0.49)) self.setColumnWidth(1, int(self.width() * 0.17)) self.setColumnWidth(2, int(self.width() * 0.17)) self.setColumnWidth(3, int(self.width() * 0.17))