linux.sipsoftphone/ui/waehlfeld.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

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)