linux.sipsoftphone/ui/einstellungen.py
data 48ddb4b4af Initiales Release: SIP Softphone für FreePBX/Asterisk
PySide6/PJSUA2-basiertes Desktop-Softphone mit:
- SIP-Telefonie (Anrufe, DTMF, Halten, Transfer, Konferenz)
- CardDAV-Kontaktsync mit Write-Back (Multi-Account)
- Kontaktverwaltung mit Suche und Bearbeiten-Dialog
- Anrufliste mit Namensauflösung
- Favoriten-Panel (manuell + häufig angerufen)
- BLF-Panel (SIP Presence Monitoring)
- Responsives Layout (kompakt/erweitert)
- Click-to-Call (tel:/sip: URI via D-Bus)
- KDE-Integration (Tray, Benachrichtigungen)
- PipeWire/PulseAudio Audio-Routing
- AppImage Build-Support (PyInstaller + appimagetool)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:18:54 +01:00

482 lines
18 KiB
Python

"""Einstellungen-Dialog - SIP, Audio, CardDAV und allgemeine Konfiguration."""
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QLineEdit, QPushButton, QComboBox, QCheckBox,
QTabWidget, QWidget, QSpinBox, QLabel, QFileDialog,
QListWidget, QListWidgetItem, QGroupBox, QGridLayout,
)
from PySide6.QtCore import Signal, Qt
class CardDavAccountDialog(QDialog):
"""Dialog zum Hinzufügen/Bearbeiten eines CardDAV-Accounts."""
def __init__(self, account=None, parent=None):
super().__init__(parent)
self._account = account or {}
self.setWindowTitle(
"Account bearbeiten" if account else "Neuer CardDAV-Account"
)
self.setMinimumWidth(450)
self.setModal(True)
self._ui_aufbauen()
def _ui_aufbauen(self):
layout = QVBoxLayout(self)
form = QFormLayout()
self.name_eingabe = QLineEdit(self._account.get("name", ""))
self.name_eingabe.setPlaceholderText("z.B. Privat, Geschäftlich")
form.addRow("Name:", self.name_eingabe)
self.url_eingabe = QLineEdit(self._account.get("url", ""))
self.url_eingabe.setPlaceholderText(
"https://nextcloud.example.com/remote.php/dav/addressbooks/"
"users/name/contacts/"
)
form.addRow("URL:", self.url_eingabe)
self.benutzer_eingabe = QLineEdit(
self._account.get("benutzername", ""))
self.benutzer_eingabe.setPlaceholderText("Benutzername")
form.addRow("Benutzer:", self.benutzer_eingabe)
self.passwort_eingabe = QLineEdit(self._account.get("passwort", ""))
self.passwort_eingabe.setEchoMode(QLineEdit.EchoMode.Password)
self.passwort_eingabe.setPlaceholderText("Passwort oder App-Token")
form.addRow("Passwort:", self.passwort_eingabe)
layout.addLayout(form)
btn_layout = QHBoxLayout()
ok_btn = QPushButton("OK")
ok_btn.setDefault(True)
ok_btn.clicked.connect(self.accept)
abbrechen_btn = QPushButton("Abbrechen")
abbrechen_btn.clicked.connect(self.reject)
btn_layout.addStretch()
btn_layout.addWidget(abbrechen_btn)
btn_layout.addWidget(ok_btn)
layout.addLayout(btn_layout)
@property
def account_daten(self):
"""Account-Dict aus den Eingabefeldern."""
return {
"name": self.name_eingabe.text().strip(),
"url": self.url_eingabe.text().strip(),
"benutzername": self.benutzer_eingabe.text().strip(),
"passwort": self.passwort_eingabe.text(),
}
class EinstellungenDialog(QDialog):
"""Dialog für Anwendungseinstellungen mit Tabs."""
einstellungen_geaendert = Signal()
def __init__(self, config_manager, audio_manager=None,
klingelton_player=None, parent=None):
super().__init__(parent)
self._config = config_manager
self._audio = audio_manager
self._klingelton = klingelton_player
self.setWindowTitle("Einstellungen")
self.setMinimumWidth(500)
self.setModal(True)
self._ui_aufbauen()
self._werte_laden()
def _ui_aufbauen(self):
layout = QVBoxLayout(self)
self.tabs = QTabWidget()
layout.addWidget(self.tabs)
self._sip_tab_erstellen()
self._audio_tab_erstellen()
self._allgemein_tab_erstellen()
self._carddav_tab_erstellen()
self._blf_tab_erstellen()
# Buttons
btn_layout = QHBoxLayout()
self.speichern_btn = QPushButton("Speichern")
self.abbrechen_btn = QPushButton("Abbrechen")
self.speichern_btn.clicked.connect(self._speichern)
self.abbrechen_btn.clicked.connect(self.reject)
btn_layout.addStretch()
btn_layout.addWidget(self.abbrechen_btn)
btn_layout.addWidget(self.speichern_btn)
layout.addLayout(btn_layout)
def _sip_tab_erstellen(self):
"""Tab für SIP-Einstellungen."""
widget = QWidget()
form = QFormLayout(widget)
self.sip_server = QLineEdit()
self.sip_server.setPlaceholderText("192.168.154.242")
form.addRow("Server:", self.sip_server)
self.sip_port = QSpinBox()
self.sip_port.setRange(1, 65535)
self.sip_port.setValue(5060)
form.addRow("Port:", self.sip_port)
self.sip_extension = QLineEdit()
self.sip_extension.setPlaceholderText("200")
form.addRow("Extension:", self.sip_extension)
self.sip_passwort = QLineEdit()
self.sip_passwort.setEchoMode(QLineEdit.EchoMode.Password)
form.addRow("Passwort:", self.sip_passwort)
self.sip_transport = QComboBox()
self.sip_transport.addItems(["UDP", "TCP"])
form.addRow("Transport:", self.sip_transport)
self.tabs.addTab(widget, "SIP")
def _audio_tab_erstellen(self):
"""Tab für Audio-Einstellungen (PipeWire/KDE-Geräte)."""
widget = QWidget()
form = QFormLayout(widget)
hinweis = QLabel(
"Geräte werden wie in KDE-Audioeinstellungen angezeigt.")
hinweis.setStyleSheet(
"color: #888; font-size: 11px; margin-bottom: 4px;")
form.addRow(hinweis)
self.aufnahme_combo = QComboBox()
form.addRow("Mikrofon:", self.aufnahme_combo)
self.wiedergabe_combo = QComboBox()
form.addRow("Lautsprecher:", self.wiedergabe_combo)
self.klingelton_geraet_combo = QComboBox()
form.addRow("Klingelton-Gerät:", self.klingelton_geraet_combo)
aktualisieren_btn = QPushButton("Geräte aktualisieren")
aktualisieren_btn.clicked.connect(self._audiogeraete_aktualisieren)
form.addRow("", aktualisieren_btn)
self.tabs.addTab(widget, "Audio")
def _allgemein_tab_erstellen(self):
"""Tab für allgemeine Einstellungen."""
widget = QWidget()
form = QFormLayout(widget)
self.tray_check = QCheckBox(
"Beim Schließen in System-Tray minimieren")
form.addRow(self.tray_check)
self.autostart_check = QCheckBox(
"Beim Systemstart automatisch starten")
form.addRow(self.autostart_check)
# Klingelton-Datei
klingelton_layout = QHBoxLayout()
self.klingelton_eingabe = QLineEdit()
self.klingelton_eingabe.setPlaceholderText("Standard-Klingelton")
self.klingelton_eingabe.setReadOnly(True)
klingelton_btn = QPushButton("Durchsuchen...")
klingelton_btn.clicked.connect(self._klingelton_waehlen)
klingelton_test_btn = QPushButton("Test")
klingelton_test_btn.clicked.connect(self._klingelton_testen)
klingelton_layout.addWidget(self.klingelton_eingabe)
klingelton_layout.addWidget(klingelton_btn)
klingelton_layout.addWidget(klingelton_test_btn)
form.addRow("Klingelton:", klingelton_layout)
self.tabs.addTab(widget, "Allgemein")
def _carddav_tab_erstellen(self):
"""Tab für CardDAV-Konfiguration (Multi-Account)."""
widget = QWidget()
layout = QVBoxLayout(widget)
hinweis = QLabel(
"Kontakte von Nextcloud/CardDAV-Servern synchronisieren.\n"
"Mehrere Accounts möglich (z.B. Privat + Geschäftlich)."
)
hinweis.setStyleSheet(
"color: #888; font-size: 11px; margin-bottom: 4px;")
layout.addWidget(hinweis)
# Account-Liste
self.carddav_account_liste = QListWidget()
self.carddav_account_liste.setMaximumHeight(120)
layout.addWidget(self.carddav_account_liste)
# Account-Buttons
acc_btn_layout = QHBoxLayout()
acc_hinzu_btn = QPushButton("Account hinzufügen")
acc_hinzu_btn.clicked.connect(self._carddav_account_hinzufuegen)
acc_btn_layout.addWidget(acc_hinzu_btn)
acc_bearbeiten_btn = QPushButton("Bearbeiten")
acc_bearbeiten_btn.clicked.connect(self._carddav_account_bearbeiten)
acc_btn_layout.addWidget(acc_bearbeiten_btn)
acc_loeschen_btn = QPushButton("Entfernen")
acc_loeschen_btn.clicked.connect(self._carddav_account_loeschen)
acc_btn_layout.addWidget(acc_loeschen_btn)
layout.addLayout(acc_btn_layout)
# Sync-Optionen
form = QFormLayout()
self.carddav_auto_sync = QCheckBox("Automatisch synchronisieren")
form.addRow(self.carddav_auto_sync)
self.carddav_intervall = QSpinBox()
self.carddav_intervall.setRange(60, 86400)
self.carddav_intervall.setValue(3600)
self.carddav_intervall.setSuffix(" Sekunden")
form.addRow("Intervall:", self.carddav_intervall)
layout.addLayout(form)
# Sync-Button + Status
sync_layout = QHBoxLayout()
self.carddav_sync_btn = QPushButton("Jetzt synchronisieren")
self.carddav_sync_btn.clicked.connect(self._carddav_sync)
sync_layout.addWidget(self.carddav_sync_btn)
layout.addLayout(sync_layout)
self.carddav_status = QLabel("")
self.carddav_status.setStyleSheet("color: #888; font-size: 11px;")
layout.addWidget(self.carddav_status)
layout.addStretch()
self.tabs.addTab(widget, "CardDAV")
def _blf_tab_erstellen(self):
"""Tab für BLF-Konfiguration."""
widget = QWidget()
layout = QVBoxLayout(widget)
hinweis = QLabel(
"BLF-Extensions (eine pro Zeile):\n"
"Diese Extensions werden auf Besetzt-Status überwacht."
)
layout.addWidget(hinweis)
from PySide6.QtWidgets import QTextEdit
self.blf_eingabe = QTextEdit()
self.blf_eingabe.setPlaceholderText("200\n201\n202")
layout.addWidget(self.blf_eingabe)
self.tabs.addTab(widget, "BLF")
def _werte_laden(self):
"""Gespeicherte Werte in die Felder laden."""
sip = self._config.sip
self.sip_server.setText(sip.get("server", ""))
self.sip_port.setValue(sip.get("port", 5060))
self.sip_extension.setText(sip.get("extension", ""))
self.sip_passwort.setText(sip.get("passwort", ""))
transport = sip.get("transport", "udp").upper()
idx = self.sip_transport.findText(transport)
if idx >= 0:
self.sip_transport.setCurrentIndex(idx)
allg = self._config.allgemein
self.tray_check.setChecked(allg.get("minimieren_in_tray", True))
self.autostart_check.setChecked(allg.get("autostart", False))
self.klingelton_eingabe.setText(allg.get("klingelton", ""))
# CardDAV-Accounts laden
self._carddav_accounts = list(
self._config.get("carddav", "accounts") or [])
self._carddav_account_liste_aktualisieren()
cdav = self._config.carddav
self.carddav_auto_sync.setChecked(cdav.get("auto_sync", True))
self.carddav_intervall.setValue(cdav.get("sync_intervall", 3600))
blf = self._config.blf
extensions = blf.get("extensions", [])
self.blf_eingabe.setPlainText("\n".join(extensions))
self._audiogeraete_aktualisieren()
def _carddav_account_liste_aktualisieren(self):
"""Account-Liste im UI aktualisieren."""
self.carddav_account_liste.clear()
for acc in self._carddav_accounts:
name = acc.get("name", "Unbenannt")
url = acc.get("url", "")
# Kurze URL-Anzeige
url_kurz = url
if len(url_kurz) > 50:
url_kurz = url_kurz[:47] + "..."
text = f"{name} - {url_kurz}"
item = QListWidgetItem(text)
self.carddav_account_liste.addItem(item)
def _carddav_account_hinzufuegen(self):
"""Neuen CardDAV-Account hinzufügen."""
dialog = CardDavAccountDialog(parent=self)
if dialog.exec() == QDialog.DialogCode.Accepted:
acc = dialog.account_daten
if acc.get("url") and acc.get("benutzername"):
self._carddav_accounts.append(acc)
self._carddav_account_liste_aktualisieren()
def _carddav_account_bearbeiten(self):
"""Ausgewählten Account bearbeiten."""
zeile = self.carddav_account_liste.currentRow()
if zeile < 0 or zeile >= len(self._carddav_accounts):
return
dialog = CardDavAccountDialog(
account=self._carddav_accounts[zeile], parent=self)
if dialog.exec() == QDialog.DialogCode.Accepted:
self._carddav_accounts[zeile] = dialog.account_daten
self._carddav_account_liste_aktualisieren()
def _carddav_account_loeschen(self):
"""Ausgewählten Account entfernen."""
zeile = self.carddav_account_liste.currentRow()
if zeile < 0 or zeile >= len(self._carddav_accounts):
return
del self._carddav_accounts[zeile]
self._carddav_account_liste_aktualisieren()
def _audiogeraete_aktualisieren(self):
"""Audiogeräte-Dropdowns mit PipeWire-Geräten aktualisieren."""
self.aufnahme_combo.clear()
self.wiedergabe_combo.clear()
self.klingelton_geraet_combo.clear()
self.aufnahme_combo.addItem("Standard (KDE-Einstellung)", "")
self.wiedergabe_combo.addItem("Standard (KDE-Einstellung)", "")
self.klingelton_geraet_combo.addItem("Standard (KDE-Einstellung)", "")
if not self._audio:
return
for geraet in self._audio.aufnahme_geraete():
self.aufnahme_combo.addItem(
geraet["name"], geraet["pw_name"])
for geraet in self._audio.wiedergabe_geraete():
self.wiedergabe_combo.addItem(
geraet["name"], geraet["pw_name"])
self.klingelton_geraet_combo.addItem(
geraet["name"], geraet["pw_name"])
audio_cfg = self._config.audio
self._combo_auswahl_setzen(
self.aufnahme_combo, audio_cfg.get("aufnahme_geraet", ""))
self._combo_auswahl_setzen(
self.wiedergabe_combo, audio_cfg.get("wiedergabe_geraet", ""))
self._combo_auswahl_setzen(
self.klingelton_geraet_combo,
audio_cfg.get("klingelton_geraet", ""))
def _combo_auswahl_setzen(self, combo, wert):
"""ComboBox auf den Eintrag mit passendem Data-Wert setzen."""
for i in range(combo.count()):
if combo.itemData(i) == wert:
combo.setCurrentIndex(i)
return
def _klingelton_waehlen(self):
"""Dateidialog für Klingelton-Auswahl."""
datei, _ = QFileDialog.getOpenFileName(
self, "Klingelton wählen", "",
"Audio-Dateien (*.wav *.mp3 *.ogg *.oga);;Alle Dateien (*)"
)
if datei:
self.klingelton_eingabe.setText(datei)
def _klingelton_testen(self):
"""Klingelton einmal zum Testen abspielen."""
if not self._klingelton:
return
device = self.klingelton_geraet_combo.currentData() or ""
datei = self.klingelton_eingabe.text()
self._klingelton.test_abspielen(datei, device)
def _carddav_sync(self):
"""CardDAV-Kontakte von allen Accounts synchronisieren."""
if not self._carddav_accounts:
self.carddav_status.setText("Keine Accounts konfiguriert")
self.carddav_status.setStyleSheet(
"color: orange; font-size: 11px;")
return
self.carddav_status.setText("Synchronisiere...")
self.carddav_status.setStyleSheet("color: #888; font-size: 11px;")
self.carddav_sync_btn.setEnabled(False)
from utils.carddav import CardDavSync
sync = CardDavSync()
kontakte, fehler = sync.alle_accounts_abrufen(
self._carddav_accounts)
self.carddav_sync_btn.setEnabled(True)
if fehler and not kontakte:
self.carddav_status.setText(
f"Fehler: {'; '.join(fehler)}")
self.carddav_status.setStyleSheet(
"color: red; font-size: 11px;")
else:
text = f"{len(kontakte)} Kontakte synchronisiert"
if fehler:
text += f" (Warnungen: {'; '.join(fehler)})"
self.carddav_status.setText(text)
self.carddav_status.setStyleSheet(
"color: green; font-size: 11px;")
self._config.set("carddav", "kontakte_cache", kontakte)
def _speichern(self):
"""Einstellungen speichern und Dialog schließen."""
# SIP
self._config.set("sip", "server", self.sip_server.text().strip())
self._config.set("sip", "port", self.sip_port.value())
self._config.set("sip", "extension",
self.sip_extension.text().strip())
self._config.set("sip", "passwort", self.sip_passwort.text())
self._config.set("sip", "transport",
self.sip_transport.currentText().lower())
# Audio
aufnahme = self.aufnahme_combo.currentData() or ""
wiedergabe = self.wiedergabe_combo.currentData() or ""
klingelton_geraet = (
self.klingelton_geraet_combo.currentData() or "")
self._config.set("audio", "aufnahme_geraet", aufnahme)
self._config.set("audio", "wiedergabe_geraet", wiedergabe)
self._config.set("audio", "klingelton_geraet", klingelton_geraet)
# Allgemein
self._config.set("allgemein", "minimieren_in_tray",
self.tray_check.isChecked())
self._config.set("allgemein", "autostart",
self.autostart_check.isChecked())
self._config.set("allgemein", "klingelton",
self.klingelton_eingabe.text())
# CardDAV (Multi-Account)
self._config.set("carddav", "accounts", self._carddav_accounts)
self._config.set("carddav", "auto_sync",
self.carddav_auto_sync.isChecked())
self._config.set("carddav", "sync_intervall",
self.carddav_intervall.value())
# BLF
blf_text = self.blf_eingabe.toPlainText().strip()
extensions = [e.strip() for e in blf_text.split("\n") if e.strip()]
self._config.set("blf", "extensions", extensions)
self._config.speichern()
self.einstellungen_geaendert.emit()
self.accept()