"""Hauptfenster - Zentrales Fenster der SIP-Softphone-Anwendung.""" import os import re from datetime import datetime from pathlib import Path from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QSlider, QStatusBar, QSystemTrayIcon, QMenu, QMessageBox, QInputDialog, QSplitter, QSizePolicy, ) from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QIcon, QAction, QShortcut, QKeySequence # Icon-Pfad relativ zum Projektverzeichnis _ICON_PFAD = Path(__file__).parent.parent / "resources" / "icons" / "phone.svg" from sip.engine import SipEngine from utils.config_manager import ConfigManager from utils.audio_manager import AudioManager from utils.klingelton import KlingeltonPlayer from utils.benachrichtigung import AnrufBenachrichtigung from ui.waehlfeld import Waehlfeld from ui.anrufliste import AnruflisteWidget from ui.kontakte import KontakteWidget from ui.einstellungen import EinstellungenDialog from ui.blf_panel import BlfPanel from ui.favoriten_panel import FavoritenPanel class HauptFenster(QMainWindow): """Hauptfenster der SIP-Softphone-Anwendung.""" def __init__(self): super().__init__() self.setWindowTitle("SIP Softphone") self.setMinimumSize(380, 600) # App-Icon setzen if _ICON_PFAD.exists(): self._app_icon = QIcon(str(_ICON_PFAD)) self.setWindowIcon(self._app_icon) else: self._app_icon = self.windowIcon() # Kern-Komponenten self._config = ConfigManager() self._sip = SipEngine() self._audio = AudioManager(self._sip) self._klingelton = KlingeltonPlayer(self) self._benachrichtigung = AnrufBenachrichtigung() self._ist_stumm = False self._ist_gehalten = False self._anruf_start_zeit = None self._anruf_timer = QTimer(self) self._anruf_timer.timeout.connect(self._anruf_dauer_aktualisieren) self._breit_modus = None # Wird in resizeEvent gesetzt # UI aufbauen self._ui_aufbauen() self._menue_aufbauen() self._tray_aufbauen() self._hotkeys_aufbauen() self._signale_verbinden() # Kontaktliste ans Wählfeld + Favoriten self._kontakte_an_waehlfeld() self._favoriten_aktualisieren() def _ui_aufbauen(self): """Hauptlayout mit Splitter: Links Wählfeld, Rechts Tabs.""" zentral = QWidget() self.setCentralWidget(zentral) layout = QVBoxLayout(zentral) # === Status-Bereich (feste Höhe, nicht stretchen) === status_layout = QHBoxLayout() self.status_label = QLabel("Nicht verbunden") self.status_label.setStyleSheet( "font-size: 13px; padding: 4px; " "background-color: #666; color: white; border-radius: 4px;" ) self.status_label.setSizePolicy( QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed) status_layout.addWidget(self.status_label) self.dauer_label = QLabel("") self.dauer_label.setStyleSheet("font-size: 13px; font-weight: bold;") status_layout.addWidget(self.dauer_label) status_layout.addStretch() layout.addLayout(status_layout, 0) # Stretch=0: nicht wachsen # === Anruf-Info === self.anruf_info_label = QLabel("") self.anruf_info_label.setStyleSheet( "font-size: 16px; font-weight: bold; padding: 8px;" ) self.anruf_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.anruf_info_label.hide() self.anruf_info_label.setSizePolicy( QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) layout.addWidget(self.anruf_info_label, 0) # Stretch=0 # === Widgets erstellen === self.waehlfeld = Waehlfeld() self.kontakte = KontakteWidget(self._config) self.anrufliste = AnruflisteWidget(self._config) self.blf_panel = BlfPanel() self.favoriten = FavoritenPanel(self._config) # === Splitter: Links (Wählfeld) | Rechts (Tabs) === self.splitter = QSplitter(Qt.Orientation.Horizontal) self.splitter.setChildrenCollapsible(False) self.splitter.setHandleWidth(1) self.splitter.setStyleSheet( "QSplitter::handle { background-color: #555; }") # Linke Seite: Wählfeld in eigenem Container self._links_widget = QWidget() self._links_widget.setObjectName("telefon_panel") links_layout = QVBoxLayout(self._links_widget) links_layout.setContentsMargins(6, 6, 6, 6) links_layout.addWidget(self.waehlfeld) # Rechte Seite: Tabs (Favoriten, Verlauf, Kontakte, BLF) self.tabs = QTabWidget() self.tabs.addTab(self.favoriten, "Favoriten") self.tabs.addTab(self.anrufliste, "Verlauf") self.tabs.addTab(self.kontakte, "Kontakte") self.tabs.addTab(self.blf_panel, "BLF") self.splitter.addWidget(self._links_widget) self.splitter.addWidget(self.tabs) self.splitter.setStretchFactor(0, 0) # Links: nicht stretchen self.splitter.setStretchFactor(1, 1) # Rechts: stretcht mit layout.addWidget(self.splitter, 1) # Stretch=1: bekommt allen extra Platz # === Anruf-Steuerung === steuerung_layout = QHBoxLayout() btn_style_vorlage = ( "QPushButton {{ " " background-color: {bg}; color: white; " " font-size: 14px; font-weight: bold; border-radius: 8px; " " border: none; " "}}" "QPushButton:hover {{ background-color: {hover}; }}" "QPushButton:pressed {{ background-color: {pressed}; }}" ) self.anrufen_btn = QPushButton("Anrufen") self.anrufen_btn.setMinimumHeight(42) self.anrufen_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.anrufen_btn.setStyleSheet(btn_style_vorlage.format( bg="#4CAF50", hover="#45a049", pressed="#388E3C")) self.anrufen_btn.clicked.connect(self._anrufen) self.annehmen_btn = QPushButton("Annehmen") self.annehmen_btn.setMinimumHeight(42) self.annehmen_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.annehmen_btn.setStyleSheet(btn_style_vorlage.format( bg="#2196F3", hover="#1e88e5", pressed="#1565C0")) self.annehmen_btn.clicked.connect(self._annehmen) self.annehmen_btn.hide() self.auflegen_btn = QPushButton("Auflegen") self.auflegen_btn.setMinimumHeight(42) self.auflegen_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.auflegen_btn.setStyleSheet(btn_style_vorlage.format( bg="#F44336", hover="#e53935", pressed="#C62828")) self.auflegen_btn.clicked.connect(self._auflegen) self.auflegen_btn.hide() steuerung_layout.addWidget(self.anrufen_btn) steuerung_layout.addWidget(self.annehmen_btn) steuerung_layout.addWidget(self.auflegen_btn) layout.addLayout(steuerung_layout, 0) # Stretch=0 # === Gespräch-Aktionen (während Anruf sichtbar) === self.gespraech_widget = QWidget() gespraech_layout = QHBoxLayout(self.gespraech_widget) gespraech_layout.setContentsMargins(0, 0, 0, 0) self.stumm_btn = QPushButton("Stumm") self.stumm_btn.setCheckable(True) self.stumm_btn.clicked.connect(self._stumm_umschalten) gespraech_layout.addWidget(self.stumm_btn) self.halten_btn = QPushButton("Halten") self.halten_btn.setCheckable(True) self.halten_btn.clicked.connect(self._halten_umschalten) gespraech_layout.addWidget(self.halten_btn) self.transfer_btn = QPushButton("Transfer") self.transfer_btn.clicked.connect(self._transfer) gespraech_layout.addWidget(self.transfer_btn) self.konferenz_btn = QPushButton("Konferenz") self.konferenz_btn.clicked.connect(self._konferenz) gespraech_layout.addWidget(self.konferenz_btn) self.gespraech_widget.hide() layout.addWidget(self.gespraech_widget, 0) # Stretch=0 # === Lautstärke === laut_layout = QHBoxLayout() laut_layout.addWidget(QLabel("Mik:")) self.mik_slider = QSlider(Qt.Orientation.Horizontal) self.mik_slider.setRange(0, 200) self.mik_slider.setValue(100) self.mik_slider.setToolTip("Mikrofon-Lautstärke") self.mik_slider.valueChanged.connect(self._mik_lautstaerke_geaendert) laut_layout.addWidget(self.mik_slider) laut_layout.addWidget(QLabel("Lsp:")) self.lsp_slider = QSlider(Qt.Orientation.Horizontal) self.lsp_slider.setRange(0, 200) self.lsp_slider.setValue(100) self.lsp_slider.setToolTip("Lautsprecher-Lautstärke") self.lsp_slider.valueChanged.connect(self._lsp_lautstaerke_geaendert) laut_layout.addWidget(self.lsp_slider) layout.addLayout(laut_layout, 0) # Stretch=0 # Statusleiste self.setStatusBar(QStatusBar()) self.statusBar().showMessage("Bereit") def resizeEvent(self, event): """Fensterbreite → Layout anpassen (kompakt vs. erweitert).""" super().resizeEvent(event) breite = event.size().width() breit_modus = breite >= 650 if breit_modus == self._breit_modus: return # Kein Wechsel nötig self._breit_modus = breit_modus if breit_modus: # Erweiterter Modus: Tabs anzeigen, Wählfeld begrenzt self.tabs.show() self._links_widget.setMaximumWidth(350) self._links_widget.setMinimumWidth(250) self._links_widget.setStyleSheet( "QWidget#telefon_panel { " " background-color: rgba(255,255,255,0.03); " " border-right: 1px solid #444; " "}") self.splitter.setSizes([280, breite - 280]) else: # Kompakter Modus: Nur Wählfeld, volle Breite self.tabs.hide() self._links_widget.setMaximumWidth(16777215) self._links_widget.setMinimumWidth(0) self._links_widget.setStyleSheet( "QWidget#telefon_panel { " " background-color: transparent; " "}") def _menue_aufbauen(self): """Menüleiste erstellen.""" menubar = self.menuBar() # Datei-Menü datei = menubar.addMenu("Datei") einstellungen_action = QAction("Einstellungen...", self) einstellungen_action.setShortcut(QKeySequence("Ctrl+,")) einstellungen_action.triggered.connect(self._einstellungen_oeffnen) datei.addAction(einstellungen_action) datei.addSeparator() beenden_action = QAction("Beenden", self) beenden_action.setShortcut(QKeySequence("Ctrl+Q")) beenden_action.triggered.connect(self._wirklich_beenden) datei.addAction(beenden_action) # Verbindung-Menü verbindung = menubar.addMenu("Verbindung") self.verbinden_action = QAction("Verbinden...", self) self.verbinden_action.triggered.connect(self._login_anzeigen) verbindung.addAction(self.verbinden_action) self.trennen_action = QAction("Trennen", self) self.trennen_action.triggered.connect(self._trennen) self.trennen_action.setEnabled(False) verbindung.addAction(self.trennen_action) def _tray_aufbauen(self): """System-Tray-Icon einrichten.""" self.tray = QSystemTrayIcon(self) self.tray.setIcon(self._app_icon) self.tray.setToolTip("SIP Softphone - Offline") # Tray-Menü tray_menu = QMenu() anzeigen_action = QAction("Anzeigen", self) anzeigen_action.triggered.connect(self._aus_tray_anzeigen) tray_menu.addAction(anzeigen_action) tray_menu.addSeparator() beenden_action = QAction("Beenden", self) beenden_action.triggered.connect(self._wirklich_beenden) tray_menu.addAction(beenden_action) self.tray.setContextMenu(tray_menu) self.tray.activated.connect(self._tray_aktiviert) self.tray.show() def _hotkeys_aufbauen(self): """Tastatur-Shortcuts einrichten.""" # F5: Annehmen QShortcut(QKeySequence("F5"), self, self._annehmen) # F6: Auflegen QShortcut(QKeySequence("F6"), self, self._auflegen) # F7: Stummschalten QShortcut(QKeySequence("F7"), self, self._stumm_umschalten) # F8: Halten/Fortsetzen QShortcut(QKeySequence("F8"), self, self._halten_umschalten) # Escape: Nummernfeld leeren QShortcut(QKeySequence("Escape"), self, self.waehlfeld.nummer_loeschen) def _signale_verbinden(self): """SIP-Engine Signale mit UI verbinden.""" self._sip.registrierung_geaendert.connect(self._on_registrierung) self._sip.eingehender_anruf.connect(self._on_eingehender_anruf) self._sip.anruf_zustand_geaendert.connect(self._on_anruf_zustand) self._sip.anruf_beendet.connect(self._on_anruf_beendet) self._sip.dtmf_empfangen.connect(self._on_dtmf) self._sip.fehler.connect(self._on_fehler) # Wählfeld → Anruf starten self.waehlfeld.nummer_gewaehlt.connect(self._anruf_mit_nummer) self.waehlfeld.dtmf_gedrueckt.connect(self._sip.dtmf_senden) # Kontakte → Anruf starten + Wählfeld-Suche aktualisieren self.kontakte.anrufen.connect(self._anruf_mit_nummer) self.kontakte.kontakte_geaendert.connect(self._kontakte_an_waehlfeld) # Anrufliste → Rückruf self.anrufliste.rueckruf.connect(self._anruf_mit_nummer) # BLF → Anruf zu Extension self.blf_panel.extension_geklickt.connect(self._anruf_mit_nummer) # Favoriten → Anruf + Updates self.favoriten.anrufen.connect(self._anruf_mit_nummer) self.kontakte.favorit_toggle.connect(self._on_favorit_toggle) # === SIP-Aktionen === def _anrufen(self): """Anruf-Button geklickt.""" nummer = self.waehlfeld.nummer_eingabe.text().strip() if nummer: self._anruf_mit_nummer(nummer) @staticmethod def _ist_gueltige_nummer(text): """Prüft ob der Text eine wählbare Nummer ist (keine Textsuche).""" bereinigt = text.strip().replace(" ", "") if not bereinigt: return False # Erlaubte Zeichen: Ziffern, +, *, # return all(c in "0123456789+*#" for c in bereinigt) def _anruf_mit_nummer(self, nummer): """Anruf an eine bestimmte Nummer starten.""" if not self._ist_gueltige_nummer(nummer): self.statusBar().showMessage( f"Keine gültige Nummer: {nummer}") return # Doppelklick-Schutz: Nicht starten wenn bereits ein Anruf läuft if self._sip.account and self._sip.account.aktueller_anruf: return self._sip.anruf_starten(nummer) self.anruf_info_label.setText(f"Rufe an: {nummer}") self.anruf_info_label.show() self._anruf_ui_aktivieren() def _annehmen(self): """Eingehenden Anruf annehmen.""" self._klingelton.stoppen() self._benachrichtigung.schliessen() self._sip.anruf_annehmen() def _auflegen(self): """Anruf beenden.""" self._klingelton.stoppen() self._benachrichtigung.schliessen() self._sip.anruf_beenden() def _stumm_umschalten(self): """Mikrofon stumm schalten / aktivieren.""" self._ist_stumm = not self._ist_stumm self._sip.stummschalten(self._ist_stumm) self.stumm_btn.setChecked(self._ist_stumm) self.stumm_btn.setText("Stumm (AN)" if self._ist_stumm else "Stumm") def _halten_umschalten(self): """Anruf halten / fortsetzen.""" self._ist_gehalten = not self._ist_gehalten if self._ist_gehalten: self._sip.halten() self.halten_btn.setText("Fortsetzen") else: self._sip.fortsetzen() self.halten_btn.setText("Halten") self.halten_btn.setChecked(self._ist_gehalten) def _transfer(self): """Blinde Weiterleitung - Zielnummer abfragen.""" nummer, ok = QInputDialog.getText( self, "Weiterleitung", "Weiterleiten an Nummer:" ) if ok and nummer.strip(): self._sip.blind_transfer(nummer.strip()) def _konferenz(self): """3er-Konferenz - Zweite Nummer abfragen.""" nummer, ok = QInputDialog.getText( self, "Konferenz", "Zweiten Teilnehmer anrufen:" ) if ok and nummer.strip(): self._sip.konferenz_starten(nummer.strip()) # === SIP-Event-Handler === def _on_registrierung(self, daten): """Registrierungsstatus hat sich geändert.""" if daten.get("aktiv"): self.status_label.setText("Registriert") self.status_label.setStyleSheet( "font-size: 13px; padding: 4px; " "background-color: #4CAF50; color: white; border-radius: 4px;" ) self.tray.setToolTip("SIP Softphone - Online") self.trennen_action.setEnabled(True) self.statusBar().showMessage(f"Registriert als {daten.get('uri', '')}") # BLF konfigurieren blf_extensions = self._config.blf.get("extensions", []) if blf_extensions and self._sip.account: self.blf_panel.konfigurieren( self._sip.account, self._config.sip.get("server", ""), blf_extensions, ) # PJSUA2 auf PipeWire routen (über 'pulse'-Device) self._audio.pulse_als_standard_setzen() # Gespeichertes PipeWire-Gerät anwenden audio_cfg = self._config.audio aufnahme = audio_cfg.get("aufnahme_geraet", "") wiedergabe = audio_cfg.get("wiedergabe_geraet", "") if aufnahme: self._audio.aufnahme_geraet_setzen(aufnahme) if wiedergabe: self._audio.wiedergabe_geraet_setzen(wiedergabe) else: code = daten.get("code", 0) grund = daten.get("grund", "") self.status_label.setText(f"Nicht registriert ({code})") self.status_label.setStyleSheet( "font-size: 13px; padding: 4px; " "background-color: #F44336; color: white; border-radius: 4px;" ) self.tray.setToolTip("SIP Softphone - Offline") self.statusBar().showMessage(f"Registrierung fehlgeschlagen: {grund}") def _on_eingehender_anruf(self, daten): """Eingehender Anruf empfangen.""" anrufer = self._uri_zu_nummer(daten.get("remoteUri", "Unbekannt")) # Kontaktname nachschlagen anrufer_anzeige = self._kontakt_name_finden(anrufer) or anrufer self.anruf_info_label.setText(f"Eingehend: {anrufer_anzeige}") self.anruf_info_label.show() self.annehmen_btn.show() self.auflegen_btn.show() # Klingelton über separates Gerät abspielen klingelton_datei = self._config.allgemein.get("klingelton", "") klingelton_geraet = self._config.audio.get("klingelton_geraet", "") self._klingelton.abspielen(klingelton_datei, klingelton_geraet) # KDE-Benachrichtigung mit Annehmen/Ablehnen-Buttons self._benachrichtigung.anruf_anzeigen( anrufer_anzeige, callback_annehmen=self._annehmen, callback_ablehnen=self._auflegen, ) # Fenster in den Vordergrund self.show() self.activateWindow() self.raise_() self.statusBar().showMessage(f"Eingehender Anruf von {anrufer_anzeige}") def _on_anruf_zustand(self, daten): """Anrufstatus hat sich geändert.""" import pjsua2 as pj state = daten.get("state") text = daten.get("stateText", "") remote = self._uri_zu_nummer(daten.get("remoteUri", "")) if state == pj.PJSIP_INV_STATE_CONFIRMED: # Gespräch verbunden self.anruf_info_label.setText(f"Verbunden: {remote}") self.annehmen_btn.hide() self._anruf_ui_aktivieren() self._anruf_start_zeit = datetime.now() self._anruf_timer.start(1000) self.waehlfeld.set_im_gespraech(True) self.tray.setToolTip(f"SIP Softphone - Im Gespräch mit {remote}") elif state == pj.PJSIP_INV_STATE_DISCONNECTED: # Anruf beendet/abgebrochen - UI immer zurücksetzen self._anruf_ui_deaktivieren() grund = daten.get("lastReason", "") self.anruf_info_label.setText(f"Beendet: {remote} ({grund})") self.anruf_info_label.show() QTimer.singleShot(3000, self.anruf_info_label.hide) self.tray.setToolTip("SIP Softphone - Online") elif state == pj.PJSIP_INV_STATE_CALLING: self.anruf_info_label.setText(f"Rufe an: {remote}") elif state == pj.PJSIP_INV_STATE_EARLY: self.anruf_info_label.setText(f"Klingelt: {remote}") self.statusBar().showMessage(f"Anruf: {text}") def _on_anruf_beendet(self, daten): """Anruf wurde beendet.""" remote = self._uri_zu_nummer(daten.get("remoteUri", "")) grund = daten.get("lastReason", "") dauer = daten.get("connectDuration", 0) # In Anrufliste eintragen richtung = "ausgehend" # Wird bei eingehend überschrieben if "eingehend" in self.anruf_info_label.text().lower(): richtung = "eingehend" if dauer > 0 else "verpasst" self.anrufliste.anruf_hinzufuegen(remote, richtung, dauer) self._favoriten_aktualisieren() # UI zurücksetzen self._anruf_ui_deaktivieren() self.anruf_info_label.setText(f"Beendet: {remote} ({grund})") QTimer.singleShot(3000, self.anruf_info_label.hide) self.statusBar().showMessage(f"Anruf beendet: {grund}") self.tray.setToolTip("SIP Softphone - Online") def _on_dtmf(self, digit): """DTMF-Ton empfangen.""" self.statusBar().showMessage(f"DTMF empfangen: {digit}") def _on_fehler(self, text): """Fehlermeldung von der SIP-Engine.""" self.statusBar().showMessage(f"Fehler: {text}") # === UI-Hilfsfunktionen === def _anruf_ui_aktivieren(self): """UI für aktiven Anruf umschalten.""" self.anrufen_btn.hide() self.auflegen_btn.show() self.gespraech_widget.show() def _anruf_ui_deaktivieren(self): """UI nach Anrufende zurücksetzen.""" self._klingelton.stoppen() self._benachrichtigung.schliessen() self.anrufen_btn.show() self.annehmen_btn.hide() self.auflegen_btn.hide() self.gespraech_widget.hide() self.dauer_label.setText("") self._anruf_timer.stop() self._anruf_start_zeit = None self._ist_stumm = False self._ist_gehalten = False self.stumm_btn.setChecked(False) self.stumm_btn.setText("Stumm") self.halten_btn.setChecked(False) self.halten_btn.setText("Halten") self.waehlfeld.set_im_gespraech(False) def _anruf_dauer_aktualisieren(self): """Timer-Callback: Gesprächsdauer anzeigen.""" if self._anruf_start_zeit: delta = datetime.now() - self._anruf_start_zeit sekunden = int(delta.total_seconds()) minuten = sekunden // 60 sek = sekunden % 60 self.dauer_label.setText(f"{minuten:02d}:{sek:02d}") def _uri_zu_nummer(self, uri): """SIP-URI in Nummer umwandeln: 'sip:200@server' → '200'.""" match = re.search(r"sip:([^@]+)@", uri) return match.group(1) if match else uri def _kontakt_name_finden(self, nummer): """Kontaktname für eine Nummer nachschlagen.""" if not nummer: return None # Lokale Kontakte for kontakt in self._config.kontakte_laden(): # Altes Format: einzelne "nummer" if kontakt.get("nummer") == nummer: return kontakt.get("name") # Neues Format: "nummern"-Dict nummern = kontakt.get("nummern", {}) if nummer in nummern.values(): return kontakt.get("name") # CardDAV-Cache for kontakt in self._config.get("carddav", "kontakte_cache") or []: nummern = kontakt.get("nummern", {}) if nummer in nummern.values(): return kontakt.get("name") return None def _mik_lautstaerke_geaendert(self, wert): """Mikrofon-Slider bewegt.""" level = wert / 100.0 self._audio.mikrofon_lautstaerke(level) def _lsp_lautstaerke_geaendert(self, wert): """Lautsprecher-Slider bewegt.""" level = wert / 100.0 self._audio.lautsprecher_lautstaerke(level) # === Verbindung / Login === def _login_anzeigen(self): """Login-Dialog öffnen und verbinden.""" from ui.login_dialog import LoginDialog dialog = LoginDialog(self._config, parent=self) if dialog.exec() == LoginDialog.DialogCode.Accepted: self._verbinden() def _verbinden(self): """Mit FreePBX verbinden.""" sip = self._config.sip self.statusBar().showMessage("Verbinde...") self._sip.registrieren( server=sip.get("server", ""), extension=sip.get("extension", ""), passwort=sip.get("passwort", ""), port=sip.get("port", 5060), ) def _trennen(self): """Verbindung trennen.""" self._sip.abmelden() self.blf_panel.aufraumen() self.status_label.setText("Getrennt") self.status_label.setStyleSheet( "font-size: 13px; padding: 4px; " "background-color: #666; color: white; border-radius: 4px;" ) self.trennen_action.setEnabled(False) def _einstellungen_oeffnen(self): """Einstellungs-Dialog öffnen.""" dialog = EinstellungenDialog( self._config, self._audio, self._klingelton, parent=self) dialog.einstellungen_geaendert.connect(self._on_einstellungen_geaendert) dialog.exec() def _kontakte_an_waehlfeld(self): """Kontaktliste für die Wählfeld-Suche aktualisieren.""" kontakte = self.kontakte.alle_kontakte_fuer_suche() self.waehlfeld.kontakte_setzen(kontakte) self.favoriten.kontakte_setzen(kontakte) self.anrufliste.kontakte_setzen(kontakte) def _favoriten_aktualisieren(self): """Favoriten-Panel neu aufbauen.""" self.favoriten.aktualisieren() def _on_favorit_toggle(self, name, nummer, hinzufuegen): """Favorit hinzufügen/entfernen (aus Kontakt-Detail-Dialog).""" if hinzufuegen: self.favoriten.favorit_hinzufuegen(name, nummer) else: self.favoriten.favorit_entfernen(nummer) self._favoriten_aktualisieren() def _on_einstellungen_geaendert(self): """Einstellungen wurden gespeichert - anwenden.""" # PipeWire-Audiogeräte anwenden audio_cfg = self._config.audio aufnahme = audio_cfg.get("aufnahme_geraet", "") wiedergabe = audio_cfg.get("wiedergabe_geraet", "") if aufnahme: self._audio.aufnahme_geraet_setzen(aufnahme) if wiedergabe: self._audio.wiedergabe_geraet_setzen(wiedergabe) # BLF aktualisieren blf_extensions = self._config.blf.get("extensions", []) if self._sip.account: self.blf_panel.konfigurieren( self._sip.account, self._config.sip.get("server", ""), blf_extensions, ) # Kontakte neu laden und Wählfeld + Favoriten aktualisieren self.kontakte.aktualisieren() self._kontakte_an_waehlfeld() self._favoriten_aktualisieren() # === System-Tray === def _tray_aktiviert(self, grund): """Tray-Icon wurde angeklickt.""" if grund == QSystemTrayIcon.ActivationReason.Trigger: self._aus_tray_anzeigen() def _aus_tray_anzeigen(self): """Fenster aus dem Tray wiederherstellen.""" self.show() self.activateWindow() self.raise_() def closeEvent(self, event): """Fenster schließen → in Tray minimieren oder beenden.""" if self._config.allgemein.get("minimieren_in_tray", True): event.ignore() self.hide() self.tray.showMessage( "SIP Softphone", "Läuft im Hintergrund weiter", QSystemTrayIcon.MessageIcon.Information, 2000, ) else: self._wirklich_beenden() def _wirklich_beenden(self): """Anwendung komplett beenden.""" self.blf_panel.aufraumen() self._sip.beenden() self.tray.hide() from PySide6.QtWidgets import QApplication QApplication.instance().quit() def starten(self): """App starten: Login prüfen und ggf. verbinden.""" if self._config.hat_sip_zugangsdaten(): # Gespeicherte Zugangsdaten vorhanden → direkt verbinden self._verbinden() else: # Keine Zugangsdaten → Login-Dialog zeigen self._login_anzeigen()