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>
240 lines
8.3 KiB
Python
240 lines
8.3 KiB
Python
"""Wählfeld-Widget - Nummerneingabe + DTMF-Tastatur mit Kontaktsuche."""
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QGridLayout, QLineEdit, QPushButton, QSizePolicy,
|
|
QListWidget, QListWidgetItem,
|
|
)
|
|
from PySide6.QtCore import Signal, Qt
|
|
from PySide6.QtGui import QFont, QColor
|
|
|
|
|
|
class Waehlfeld(QWidget):
|
|
"""Wähltastatur mit Nummernfeld, Kontaktsuche und 12 Tasten."""
|
|
|
|
# Signale
|
|
nummer_gewaehlt = Signal(str) # Komplette Nummer zum Anrufen
|
|
dtmf_gedrueckt = Signal(str) # Einzelne DTMF-Ziffer
|
|
|
|
# Tasten-Layout (4 Zeilen x 3 Spalten)
|
|
TASTEN = [
|
|
("1", ""), ("2", "ABC"), ("3", "DEF"),
|
|
("4", "GHI"), ("5", "JKL"), ("6", "MNO"),
|
|
("7", "PQRS"), ("8", "TUV"), ("9", "WXYZ"),
|
|
("*", ""), ("0", "+"), ("#", ""),
|
|
]
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._im_gespraech = False
|
|
self._kontakte_liste = [] # Wird von HauptFenster gesetzt
|
|
self._ui_aufbauen()
|
|
|
|
def _ui_aufbauen(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(8)
|
|
|
|
# Nummern-/Suchfeld
|
|
self.nummer_eingabe = QLineEdit()
|
|
self.nummer_eingabe.setPlaceholderText("Nummer oder Name...")
|
|
self.nummer_eingabe.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
eingabe_font = QFont()
|
|
eingabe_font.setPointSize(18)
|
|
self.nummer_eingabe.setFont(eingabe_font)
|
|
self.nummer_eingabe.setMinimumHeight(48)
|
|
self.nummer_eingabe.setStyleSheet(
|
|
"QLineEdit { "
|
|
" border: 2px solid #555; border-radius: 8px; "
|
|
" padding: 8px 12px; font-size: 18px; "
|
|
" background: rgba(255,255,255,0.05); "
|
|
"}"
|
|
"QLineEdit:focus { border-color: #4CAF50; }"
|
|
)
|
|
self.nummer_eingabe.returnPressed.connect(self._nummer_senden)
|
|
self.nummer_eingabe.textChanged.connect(self._suche_aktualisieren)
|
|
layout.addWidget(self.nummer_eingabe)
|
|
|
|
# Vorschlagsliste (normalerweise versteckt)
|
|
self.vorschlaege = QListWidget()
|
|
self.vorschlaege.setMaximumHeight(180)
|
|
self.vorschlaege.setAlternatingRowColors(True)
|
|
self.vorschlaege.setStyleSheet(
|
|
"QListWidget { border: 1px solid #555; border-radius: 4px; }"
|
|
"QListWidget::item { padding: 4px 8px; }"
|
|
"QListWidget::item:hover { background: rgba(76,175,80,0.2); }"
|
|
)
|
|
self.vorschlaege.itemClicked.connect(self._vorschlag_gewaehlt)
|
|
self.vorschlaege.itemDoubleClicked.connect(
|
|
self._vorschlag_anrufen)
|
|
self.vorschlaege.hide()
|
|
layout.addWidget(self.vorschlaege)
|
|
|
|
# Tastatur-Grid
|
|
grid = QGridLayout()
|
|
grid.setSpacing(6)
|
|
|
|
# DTMF-Button Styling
|
|
btn_style = (
|
|
"QPushButton { "
|
|
" border: 1px solid #555; border-radius: 8px; "
|
|
" background: rgba(255,255,255,0.06); "
|
|
" color: #ddd; font-size: 18px; font-weight: bold; "
|
|
" padding: 4px; "
|
|
"}"
|
|
"QPushButton:hover { "
|
|
" background: rgba(76,175,80,0.15); "
|
|
" border-color: #4CAF50; "
|
|
"}"
|
|
"QPushButton:pressed { "
|
|
" background: rgba(76,175,80,0.3); "
|
|
"}"
|
|
)
|
|
|
|
for index, (ziffer, buchstaben) in enumerate(self.TASTEN):
|
|
zeile = index // 3
|
|
spalte = index % 3
|
|
|
|
if buchstaben:
|
|
text = f"{ziffer}\n{buchstaben}"
|
|
else:
|
|
text = ziffer
|
|
|
|
btn = QPushButton(text)
|
|
btn.setStyleSheet(btn_style)
|
|
btn.setMinimumSize(60, 50)
|
|
btn.setSizePolicy(QSizePolicy.Policy.Expanding,
|
|
QSizePolicy.Policy.Expanding)
|
|
btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
btn.clicked.connect(
|
|
lambda checked, z=ziffer: self._taste_gedrueckt(z))
|
|
grid.addWidget(btn, zeile, spalte)
|
|
|
|
layout.addLayout(grid)
|
|
|
|
def kontakte_setzen(self, kontakte):
|
|
"""Kontaktliste für die Suche setzen (von HauptFenster aufgerufen)."""
|
|
self._kontakte_liste = kontakte or []
|
|
|
|
def _taste_gedrueckt(self, ziffer):
|
|
"""Taste wurde gedrückt - Ziffer anhängen oder DTMF senden."""
|
|
if self._im_gespraech:
|
|
self.dtmf_gedrueckt.emit(ziffer)
|
|
else:
|
|
self.nummer_eingabe.setText(self.nummer_eingabe.text() + ziffer)
|
|
self.nummer_eingabe.setFocus()
|
|
|
|
def _nummer_senden(self):
|
|
"""Eingegebene Nummer zum Anrufen senden."""
|
|
nummer = self.nummer_eingabe.text().strip()
|
|
if nummer:
|
|
self.nummer_gewaehlt.emit(nummer)
|
|
|
|
def _suche_aktualisieren(self, text):
|
|
"""Eingabe geändert → Vorschläge aktualisieren."""
|
|
if self._im_gespraech or not text or len(text) < 2:
|
|
self.vorschlaege.hide()
|
|
return
|
|
|
|
text_lower = text.lower().strip()
|
|
|
|
# Prüfen ob es eine reine Nummer ist (dann keine Suche)
|
|
if all(c in "0123456789+*# " for c in text):
|
|
self.vorschlaege.hide()
|
|
return
|
|
|
|
self.vorschlaege.clear()
|
|
treffer = 0
|
|
|
|
for kontakt in self._kontakte_liste:
|
|
name = kontakt.get("name", "")
|
|
nummern = kontakt.get("nummern", {})
|
|
firma = kontakt.get("firma", "")
|
|
|
|
# Nach Name oder Firma suchen
|
|
if (text_lower not in name.lower() and
|
|
text_lower not in firma.lower()):
|
|
continue
|
|
|
|
# Alle Nummern des Kontakts als Vorschläge
|
|
typ_anzeige = {
|
|
"telefon": "Tel",
|
|
"handy": "Handy",
|
|
"geschaeftlich": "Gesch.",
|
|
"sonstige": "Sonst.",
|
|
}
|
|
|
|
for typ, nummer in nummern.items():
|
|
anzeige_typ = typ_anzeige.get(typ, typ)
|
|
text_anzeige = f"{name} ({anzeige_typ}: {nummer})"
|
|
|
|
item = QListWidgetItem(text_anzeige)
|
|
item.setData(Qt.ItemDataRole.UserRole, nummer)
|
|
item.setData(Qt.ItemDataRole.UserRole + 1, name)
|
|
|
|
# CardDAV-Kontakte blau
|
|
if kontakt.get("quelle") == "carddav":
|
|
item.setForeground(QColor("#5599DD"))
|
|
|
|
self.vorschlaege.addItem(item)
|
|
treffer += 1
|
|
|
|
if treffer >= 15:
|
|
break
|
|
if treffer >= 15:
|
|
break
|
|
|
|
if treffer > 0:
|
|
self.vorschlaege.show()
|
|
else:
|
|
self.vorschlaege.hide()
|
|
|
|
def _vorschlag_gewaehlt(self, item):
|
|
"""Einfacher Klick auf Vorschlag → Nummer ins Feld."""
|
|
nummer = item.data(Qt.ItemDataRole.UserRole)
|
|
if nummer:
|
|
self.nummer_eingabe.setText(nummer)
|
|
self.vorschlaege.hide()
|
|
|
|
def _vorschlag_anrufen(self, item):
|
|
"""Doppelklick auf Vorschlag → direkt anrufen."""
|
|
nummer = item.data(Qt.ItemDataRole.UserRole)
|
|
if nummer:
|
|
self.nummer_eingabe.setText(nummer)
|
|
self.vorschlaege.hide()
|
|
self.nummer_gewaehlt.emit(nummer)
|
|
|
|
def set_im_gespraech(self, aktiv):
|
|
"""Modus wechseln: Nummern-Eingabe ↔ DTMF-Versand."""
|
|
self._im_gespraech = aktiv
|
|
if aktiv:
|
|
self.nummer_eingabe.setPlaceholderText("DTMF-Modus aktiv")
|
|
self.vorschlaege.hide()
|
|
else:
|
|
self.nummer_eingabe.setPlaceholderText(
|
|
"Name oder Nummer eingeben...")
|
|
|
|
def nummer_loeschen(self):
|
|
"""Nummernfeld leeren."""
|
|
self.nummer_eingabe.clear()
|
|
self.vorschlaege.hide()
|
|
|
|
def nummer_setzen(self, nummer):
|
|
"""Nummer ins Eingabefeld setzen."""
|
|
self.nummer_eingabe.setText(nummer)
|
|
self.nummer_eingabe.setFocus()
|
|
|
|
def keyPressEvent(self, event):
|
|
"""Tastatureingaben abfangen (Numpad-Support)."""
|
|
taste = event.text()
|
|
if taste in "0123456789*#":
|
|
self._taste_gedrueckt(taste)
|
|
elif event.key() == Qt.Key.Key_Backspace:
|
|
text = self.nummer_eingabe.text()
|
|
self.nummer_eingabe.setText(text[:-1])
|
|
elif event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
|
self._nummer_senden()
|
|
elif event.key() == Qt.Key.Key_Escape:
|
|
self.vorschlaege.hide()
|
|
else:
|
|
super().keyPressEvent(event)
|