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

863 lines
32 KiB
Python

"""Kontakte-Widget - Tabelle mit lokalen + CardDAV-Kontakten und Detail-Dialog."""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
QPushButton, QDialog, QFormLayout, QLineEdit, QLabel, QHeaderView,
QAbstractItemView, QGroupBox, QGridLayout, QScrollArea,
QPlainTextEdit, QMessageBox,
)
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QColor, QFont
class KontaktDetailDialog(QDialog):
"""Detail-Dialog - zeigt alle Kontaktdaten, Nummern anklickbar."""
# Signal: Nummer zum Anrufen
anrufen = Signal(str)
# Signal: Favorit hinzufügen/entfernen (name, nummer, hinzufuegen)
favorit_toggle = Signal(str, str, bool)
def __init__(self, kontakt, parent=None):
super().__init__(parent)
self._kontakt = kontakt
self._bearbeiten_gewuenscht = False
self.setWindowTitle(kontakt.get("name", "Kontakt"))
self.setMinimumWidth(420)
self.setMinimumHeight(350)
self.setModal(True)
self._ui_aufbauen()
def _ui_aufbauen(self):
dialog_layout = QVBoxLayout(self)
# Scrollbereich für viele Daten
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.Shape.NoFrame)
inhalt = QWidget()
layout = QVBoxLayout(inhalt)
# === Name ===
name_label = QLabel(self._kontakt.get("name", ""))
name_font = QFont()
name_font.setPointSize(16)
name_font.setBold(True)
name_label.setFont(name_font)
name_label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse)
layout.addWidget(name_label)
# Firma + Titel
firma = self._kontakt.get("firma", "")
titel = self._kontakt.get("titel", "")
if firma and titel:
layout.addWidget(self._info_label(f"{titel} bei {firma}"))
elif firma:
layout.addWidget(self._info_label(firma))
elif titel:
layout.addWidget(self._info_label(titel))
# Account-Info
quelle = self._kontakt.get("quelle", "lokal")
account = self._kontakt.get("account", "")
if quelle == "carddav" and account:
layout.addWidget(self._info_label(
f"CardDAV: {account}", "#999", 11))
layout.addSpacing(8)
# === Telefonnummern (klickbar) ===
nummern = self._kontakt.get("nummern", {})
if not nummern and self._kontakt.get("nummer"):
nummern = {"telefon": self._kontakt["nummer"]}
if nummern:
nummern_box = QGroupBox("Telefonnummern")
nummern_grid = QGridLayout(nummern_box)
typ_labels = {
"telefon": "Telefon:",
"handy": "Handy:",
"geschaeftlich": "Geschäftlich:",
"sonstige": "Sonstige:",
}
zeile = 0
for typ, label_text in typ_labels.items():
nummer = nummern.get(typ, "")
if not nummer:
continue
label = QLabel(label_text)
label.setStyleSheet("font-weight: bold;")
nummern_grid.addWidget(label, zeile, 0)
nummer_btn = QPushButton(f" {nummer} ")
nummer_btn.setStyleSheet(
"text-align: left; font-size: 14px; "
"padding: 6px 12px; "
"color: #4CAF50; border: 1px solid #4CAF50; "
"border-radius: 4px; background: transparent;"
)
nummer_btn.setCursor(Qt.CursorShape.PointingHandCursor)
nummer_btn.setToolTip(f"Anrufen: {nummer}")
nummer_btn.clicked.connect(
lambda checked, n=nummer: self._nummer_anrufen(n)
)
nummern_grid.addWidget(nummer_btn, zeile, 1)
zeile += 1
layout.addWidget(nummern_box)
# === Details (E-Mail, Adresse, Geburtstag, etc.) ===
details_form = QFormLayout()
details_form.setSpacing(6)
details_vorhanden = False
# E-Mails
emails = self._kontakt.get("emails", [])
if not emails and self._kontakt.get("email"):
emails = [self._kontakt["email"]]
for em in emails:
details_form.addRow(
self._bold_label("E-Mail:"),
self._selectable_label(em, "#5599DD"))
details_vorhanden = True
# Adresse
adresse = self._kontakt.get("adresse", "")
if adresse:
adr_label = self._selectable_label(adresse)
adr_label.setWordWrap(True)
details_form.addRow(self._bold_label("Adresse:"), adr_label)
details_vorhanden = True
# Geburtstag
geburtstag = self._kontakt.get("geburtstag", "")
if geburtstag:
details_form.addRow(
self._bold_label("Geburtstag:"),
self._selectable_label(geburtstag))
details_vorhanden = True
# Website
url = self._kontakt.get("url", "")
if url:
details_form.addRow(
self._bold_label("Website:"),
self._selectable_label(url, "#5599DD"))
details_vorhanden = True
# Notiz
notiz = self._kontakt.get("notiz", "")
if notiz:
notiz_label = self._selectable_label(notiz)
notiz_label.setWordWrap(True)
details_form.addRow(self._bold_label("Notiz:"), notiz_label)
details_vorhanden = True
if details_vorhanden:
details_box = QGroupBox("Details")
details_box.setLayout(details_form)
layout.addWidget(details_box)
layout.addStretch()
scroll.setWidget(inhalt)
dialog_layout.addWidget(scroll)
# Buttons: Bearbeiten + Favorit + Schließen
btn_layout = QHBoxLayout()
bearbeiten_btn = QPushButton("Bearbeiten")
bearbeiten_btn.clicked.connect(self._bearbeiten_klick)
btn_layout.addWidget(bearbeiten_btn)
# Favorit-Toggle (nur wenn Nummer vorhanden)
self._haupt_nummer = self._haupt_nummer_bestimmen()
if self._haupt_nummer:
self.favorit_btn = QPushButton()
self.favorit_btn.setCheckable(True)
self._favorit_btn_aktualisieren(False)
self.favorit_btn.clicked.connect(self._favorit_toggle_klick)
btn_layout.addWidget(self.favorit_btn)
btn_layout.addStretch()
schliessen_btn = QPushButton("Schließen")
schliessen_btn.clicked.connect(self.accept)
btn_layout.addWidget(schliessen_btn)
dialog_layout.addLayout(btn_layout)
def _info_label(self, text, farbe="#888", groesse=12):
"""Info-Label mit Farbe und Größe."""
label = QLabel(text)
label.setStyleSheet(f"color: {farbe}; font-size: {groesse}px;")
label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse)
return label
def _bold_label(self, text):
"""Fettgedrucktes Label."""
label = QLabel(text)
label.setStyleSheet("font-weight: bold;")
return label
def _selectable_label(self, text, farbe=None):
"""Selektierbares/kopierbares Label."""
label = QLabel(text)
label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse)
if farbe:
label.setStyleSheet(f"color: {farbe};")
return label
def _haupt_nummer_bestimmen(self):
"""Erste verfügbare Nummer des Kontakts bestimmen."""
nummern = self._kontakt.get("nummern", {})
if not nummern and self._kontakt.get("nummer"):
return self._kontakt["nummer"]
for typ in ("handy", "telefon", "geschaeftlich", "sonstige"):
if typ in nummern:
return nummern[typ]
return ""
def favorit_status_setzen(self, ist_favorit):
"""Favorit-Button von außen aktualisieren."""
if hasattr(self, "favorit_btn"):
self.favorit_btn.setChecked(ist_favorit)
self._favorit_btn_aktualisieren(ist_favorit)
def _favorit_btn_aktualisieren(self, aktiv):
"""Favorit-Button Aussehen je nach Status."""
if aktiv:
self.favorit_btn.setText("Favorit entfernen")
self.favorit_btn.setStyleSheet(
"color: #FFD700; font-weight: bold;")
else:
self.favorit_btn.setText("Als Favorit")
self.favorit_btn.setStyleSheet("")
def _favorit_toggle_klick(self):
"""Favorit-Button geklickt."""
ist_aktiv = self.favorit_btn.isChecked()
self._favorit_btn_aktualisieren(ist_aktiv)
name = self._kontakt.get("name", "")
self.favorit_toggle.emit(name, self._haupt_nummer, ist_aktiv)
def _bearbeiten_klick(self):
"""Bearbeiten-Button geklickt → Flag setzen und schließen."""
self._bearbeiten_gewuenscht = True
self.accept()
@property
def bearbeiten_gewuenscht(self):
"""True wenn der User 'Bearbeiten' geklickt hat."""
return self._bearbeiten_gewuenscht
def _nummer_anrufen(self, nummer):
"""Nummer-Button geklickt → anrufen und Dialog schließen."""
self.anrufen.emit(nummer)
self.accept()
class KontaktBearbeitenDialog(QDialog):
"""Dialog zum Hinzufügen/Bearbeiten eines Kontakts (lokal + CardDAV)."""
def __init__(self, kontakt=None, parent=None):
super().__init__(parent)
self._kontakt = kontakt or {}
self.setWindowTitle(
"Kontakt bearbeiten" if kontakt else "Neuer Kontakt"
)
self.setMinimumWidth(420)
self.setMinimumHeight(500)
self.setModal(True)
self._ui_aufbauen()
def _ui_aufbauen(self):
dialog_layout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.Shape.NoFrame)
inhalt = QWidget()
layout = QVBoxLayout(inhalt)
# === Grunddaten ===
grund_form = QFormLayout()
grund_form.setSpacing(6)
self.name_eingabe = QLineEdit(self._kontakt.get("name", ""))
self.name_eingabe.setPlaceholderText("Vor- und Nachname")
grund_form.addRow("Name:", self.name_eingabe)
self.firma_eingabe = QLineEdit(self._kontakt.get("firma", ""))
self.firma_eingabe.setPlaceholderText("Firmenname")
grund_form.addRow("Firma:", self.firma_eingabe)
self.titel_eingabe = QLineEdit(self._kontakt.get("titel", ""))
self.titel_eingabe.setPlaceholderText("Berufsbezeichnung")
grund_form.addRow("Titel:", self.titel_eingabe)
grund_box = QGroupBox("Grunddaten")
grund_box.setLayout(grund_form)
layout.addWidget(grund_box)
# === Telefonnummern ===
nummern = self._kontakt.get("nummern", {})
if not nummern and self._kontakt.get("nummer"):
nummern = {"telefon": self._kontakt["nummer"]}
tel_form = QFormLayout()
tel_form.setSpacing(6)
self.telefon_eingabe = QLineEdit(nummern.get("telefon", ""))
self.telefon_eingabe.setPlaceholderText("Festnetz")
tel_form.addRow("Telefon:", self.telefon_eingabe)
self.handy_eingabe = QLineEdit(nummern.get("handy", ""))
self.handy_eingabe.setPlaceholderText("Mobilnummer")
tel_form.addRow("Handy:", self.handy_eingabe)
self.geschaeftlich_eingabe = QLineEdit(
nummern.get("geschaeftlich", ""))
self.geschaeftlich_eingabe.setPlaceholderText("Geschäftlich")
tel_form.addRow("Geschäftlich:", self.geschaeftlich_eingabe)
self.sonstige_eingabe = QLineEdit(nummern.get("sonstige", ""))
self.sonstige_eingabe.setPlaceholderText("Weitere Nummer")
tel_form.addRow("Sonstige:", self.sonstige_eingabe)
tel_box = QGroupBox("Telefonnummern")
tel_box.setLayout(tel_form)
layout.addWidget(tel_box)
# === Kontaktdaten ===
kontakt_form = QFormLayout()
kontakt_form.setSpacing(6)
# E-Mail (erste aus der Liste oder Einzelwert)
emails = self._kontakt.get("emails", [])
email_wert = emails[0] if emails else self._kontakt.get("email", "")
self.email_eingabe = QLineEdit(email_wert)
self.email_eingabe.setPlaceholderText("name@beispiel.de")
kontakt_form.addRow("E-Mail:", self.email_eingabe)
self.url_eingabe = QLineEdit(self._kontakt.get("url", ""))
self.url_eingabe.setPlaceholderText("https://...")
kontakt_form.addRow("Website:", self.url_eingabe)
self.geburtstag_eingabe = QLineEdit(
self._kontakt.get("geburtstag", ""))
self.geburtstag_eingabe.setPlaceholderText("TT.MM.JJJJ")
kontakt_form.addRow("Geburtstag:", self.geburtstag_eingabe)
kontakt_box = QGroupBox("Kontaktdaten")
kontakt_box.setLayout(kontakt_form)
layout.addWidget(kontakt_box)
# === Adresse + Notiz (mehrzeilig) ===
extra_form = QFormLayout()
extra_form.setSpacing(6)
self.adresse_eingabe = QPlainTextEdit(
self._kontakt.get("adresse", ""))
self.adresse_eingabe.setPlaceholderText(
"Straße, PLZ Ort, Land")
self.adresse_eingabe.setMaximumHeight(80)
extra_form.addRow("Adresse:", self.adresse_eingabe)
self.notiz_eingabe = QPlainTextEdit(
self._kontakt.get("notiz", ""))
self.notiz_eingabe.setPlaceholderText("Freitext-Notizen...")
self.notiz_eingabe.setMaximumHeight(80)
extra_form.addRow("Notiz:", self.notiz_eingabe)
extra_box = QGroupBox("Sonstiges")
extra_box.setLayout(extra_form)
layout.addWidget(extra_box)
layout.addStretch()
scroll.setWidget(inhalt)
dialog_layout.addWidget(scroll)
# Buttons
btn_layout = QHBoxLayout()
ok_btn = QPushButton("Speichern")
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)
dialog_layout.addLayout(btn_layout)
@property
def kontakt_daten(self):
"""Kontakt-Dict aus den Eingabefeldern erstellen."""
name = self.name_eingabe.text().strip()
if not name:
return None
nummern = {}
if self.telefon_eingabe.text().strip():
nummern["telefon"] = self.telefon_eingabe.text().strip()
if self.handy_eingabe.text().strip():
nummern["handy"] = self.handy_eingabe.text().strip()
if self.geschaeftlich_eingabe.text().strip():
nummern["geschaeftlich"] = (
self.geschaeftlich_eingabe.text().strip())
if self.sonstige_eingabe.text().strip():
nummern["sonstige"] = self.sonstige_eingabe.text().strip()
# Mindestens Name muss vorhanden sein
# (Nummer nicht zwingend, z.B. bei CardDAV nur Email)
ergebnis = {
"name": name,
"nummern": nummern,
"firma": self.firma_eingabe.text().strip(),
"titel": self.titel_eingabe.text().strip(),
"email": self.email_eingabe.text().strip(),
"emails": ([self.email_eingabe.text().strip()]
if self.email_eingabe.text().strip() else []),
"adresse": self.adresse_eingabe.toPlainText().strip(),
"geburtstag": self.geburtstag_eingabe.text().strip(),
"url": self.url_eingabe.text().strip(),
"notiz": self.notiz_eingabe.toPlainText().strip(),
}
# Metadaten vom Original übernehmen (CardDAV-Referenz)
for key in ("_href", "_etag", "quelle", "account"):
if key in self._kontakt:
ergebnis[key] = self._kontakt[key]
return ergebnis
class KontakteWidget(QWidget):
"""Kontaktliste als Tabelle mit Spalten (Name, Telefon, Handy, Geschäftlich)."""
# Signal: Nummer zum Anrufen
anrufen = Signal(str)
# Signal: Kontaktliste hat sich geändert (für Wählfeld-Suche)
kontakte_geaendert = Signal()
# Signal: Favorit hinzufügen/entfernen (name, nummer, hinzufuegen)
favorit_toggle = Signal(str, str, bool)
# Spalten-Index
SPALTE_NAME = 0
SPALTE_TELEFON = 1
SPALTE_HANDY = 2
SPALTE_GESCHAEFTLICH = 3
def __init__(self, config_manager, parent=None):
super().__init__(parent)
self._config = config_manager
self._kontakte = [] # Lokale Kontakte
self._carddav_kontakte = [] # CardDAV-Kontakte (read-only)
self._alle_kontakte = [] # Zusammengeführte, sortierte Liste
self._ui_aufbauen()
self._kontakte_laden()
def _ui_aufbauen(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(6)
# Suchfeld
self._suchfeld = QLineEdit()
self._suchfeld.setPlaceholderText("Kontakte durchsuchen...")
self._suchfeld.setClearButtonEnabled(True)
self._suchfeld.setStyleSheet(
"QLineEdit { "
" border: 1px solid #555; border-radius: 6px; "
" padding: 6px 10px; "
" background: rgba(255,255,255,0.05); "
"}"
"QLineEdit:focus { border-color: #5599DD; }"
)
self._suchfeld.textChanged.connect(self._suche_filtern)
layout.addWidget(self._suchfeld)
# Tabelle
self.tabelle = QTableWidget()
self.tabelle.setColumnCount(4)
self.tabelle.setHorizontalHeaderLabels(
["Name", "Telefon", "Handy", "Geschäftlich"]
)
self.tabelle.setSelectionBehavior(
QAbstractItemView.SelectionBehavior.SelectRows
)
self.tabelle.setSelectionMode(
QAbstractItemView.SelectionMode.SingleSelection
)
self.tabelle.setEditTriggers(
QAbstractItemView.EditTrigger.NoEditTriggers
)
self.tabelle.setAlternatingRowColors(True)
self.tabelle.verticalHeader().setVisible(False)
self.tabelle.setSortingEnabled(True)
self.tabelle.setStyleSheet(
"QTableWidget { "
" border: 1px solid #444; border-radius: 4px; "
" gridline-color: #3a3a3a; "
"}"
"QTableWidget::item { padding: 4px 6px; }"
"QTableWidget::item:selected { "
" background-color: rgba(85,153,221,0.3); "
"}"
"QHeaderView::section { "
" background-color: rgba(255,255,255,0.06); "
" border: none; border-bottom: 2px solid #555; "
" padding: 6px 8px; font-weight: bold; "
"}"
)
# Spaltenbreiten
header = self.tabelle.horizontalHeader()
header.setSectionResizeMode(
self.SPALTE_NAME, QHeaderView.ResizeMode.Stretch
)
header.setSectionResizeMode(
self.SPALTE_TELEFON, QHeaderView.ResizeMode.ResizeToContents
)
header.setSectionResizeMode(
self.SPALTE_HANDY, QHeaderView.ResizeMode.ResizeToContents
)
header.setSectionResizeMode(
self.SPALTE_GESCHAEFTLICH, QHeaderView.ResizeMode.ResizeToContents
)
self.tabelle.doubleClicked.connect(self._doppelklick)
layout.addWidget(self.tabelle)
# Buttons
btn_layout = QHBoxLayout()
btn_layout.setSpacing(4)
btn_style = (
"QPushButton { "
" border: 1px solid #555; border-radius: 6px; "
" padding: 6px 12px; "
" background: rgba(255,255,255,0.06); "
"}"
"QPushButton:hover { "
" background: rgba(85,153,221,0.15); "
" border-color: #5599DD; "
"}"
"QPushButton:pressed { background: rgba(85,153,221,0.25); }"
)
gruen_style = (
"QPushButton { "
" border: 1px solid #4CAF50; border-radius: 6px; "
" padding: 6px 12px; color: #4CAF50; "
" background: rgba(76,175,80,0.08); "
"}"
"QPushButton:hover { background: rgba(76,175,80,0.2); }"
"QPushButton:pressed { background: rgba(76,175,80,0.3); }"
)
anrufen_btn = QPushButton("Anrufen")
anrufen_btn.setStyleSheet(gruen_style)
anrufen_btn.setCursor(Qt.CursorShape.PointingHandCursor)
anrufen_btn.clicked.connect(self._anrufen_klick)
btn_layout.addWidget(anrufen_btn)
details_btn = QPushButton("Details")
details_btn.setStyleSheet(btn_style)
details_btn.setCursor(Qt.CursorShape.PointingHandCursor)
details_btn.clicked.connect(self._details_zeigen)
btn_layout.addWidget(details_btn)
btn_layout.addStretch()
hinzufuegen_btn = QPushButton("Neu")
hinzufuegen_btn.setStyleSheet(btn_style)
hinzufuegen_btn.setCursor(Qt.CursorShape.PointingHandCursor)
hinzufuegen_btn.clicked.connect(self._kontakt_hinzufuegen)
btn_layout.addWidget(hinzufuegen_btn)
bearbeiten_btn = QPushButton("Bearbeiten")
bearbeiten_btn.setStyleSheet(btn_style)
bearbeiten_btn.setCursor(Qt.CursorShape.PointingHandCursor)
bearbeiten_btn.clicked.connect(self._kontakt_bearbeiten)
btn_layout.addWidget(bearbeiten_btn)
loeschen_btn = QPushButton("Löschen")
loeschen_btn.setStyleSheet(btn_style)
loeschen_btn.setCursor(Qt.CursorShape.PointingHandCursor)
loeschen_btn.clicked.connect(self._kontakt_loeschen)
btn_layout.addWidget(loeschen_btn)
sync_btn = QPushButton("Sync")
sync_btn.setToolTip("CardDAV-Kontakte synchronisieren")
sync_btn.setStyleSheet(btn_style)
sync_btn.setCursor(Qt.CursorShape.PointingHandCursor)
sync_btn.clicked.connect(self._carddav_sync)
btn_layout.addWidget(sync_btn)
layout.addLayout(btn_layout)
def _tabelle_fuellen(self):
"""Tabelle mit allen Kontakten füllen."""
self.tabelle.setSortingEnabled(False)
# Alle Kontakte zusammenführen
self._alle_kontakte = []
for k in self._kontakte:
eintrag = {**k, "quelle": "lokal"}
# Abwärtskompatibel: einzelne "nummer" → nummern-Dict
if "nummer" in eintrag and "nummern" not in eintrag:
eintrag["nummern"] = {"telefon": eintrag["nummer"]}
self._alle_kontakte.append(eintrag)
for k in self._carddav_kontakte:
self._alle_kontakte.append({**k, "quelle": "carddav"})
self._alle_kontakte.sort(key=lambda k: k.get("name", "").lower())
self.tabelle.setRowCount(len(self._alle_kontakte))
carddav_farbe = QColor("#5599DD")
for zeile, kontakt in enumerate(self._alle_kontakte):
quelle = kontakt.get("quelle", "lokal")
nummern = kontakt.get("nummern", {})
account = kontakt.get("account", "")
# Name (mit Account-Info für CardDAV)
name = kontakt.get("name", "")
if quelle == "carddav" and account:
name_text = f"{name} [{account}]"
else:
name_text = name
name_item = QTableWidgetItem(name_text)
name_item.setData(Qt.ItemDataRole.UserRole, zeile) # Index
# Nummern-Spalten
telefon_item = QTableWidgetItem(nummern.get("telefon", ""))
handy_item = QTableWidgetItem(nummern.get("handy", ""))
geschaeft_item = QTableWidgetItem(
nummern.get("geschaeftlich", ""))
if quelle == "carddav":
for item in (name_item, telefon_item,
handy_item, geschaeft_item):
item.setForeground(carddav_farbe)
self.tabelle.setItem(zeile, self.SPALTE_NAME, name_item)
self.tabelle.setItem(zeile, self.SPALTE_TELEFON, telefon_item)
self.tabelle.setItem(zeile, self.SPALTE_HANDY, handy_item)
self.tabelle.setItem(
zeile, self.SPALTE_GESCHAEFTLICH, geschaeft_item)
self.tabelle.setSortingEnabled(True)
def _kontakt_am_index(self, zeile):
"""Kontakt-Dict für eine Tabellenzeile holen."""
name_item = self.tabelle.item(zeile, self.SPALTE_NAME)
if not name_item:
return None
idx = name_item.data(Qt.ItemDataRole.UserRole)
if idx is not None and 0 <= idx < len(self._alle_kontakte):
return self._alle_kontakte[idx]
return None
def _aktuelle_zeile_kontakt(self):
"""Kontakt der aktuell ausgewählten Zeile."""
zeile = self.tabelle.currentRow()
if zeile < 0:
return None
return self._kontakt_am_index(zeile)
def _doppelklick(self, index):
"""Doppelklick → Detail-Dialog öffnen."""
kontakt = self._kontakt_am_index(index.row())
if kontakt:
self._detail_dialog_oeffnen(kontakt)
def _details_zeigen(self):
"""Details-Button → Detail-Dialog."""
kontakt = self._aktuelle_zeile_kontakt()
if kontakt:
self._detail_dialog_oeffnen(kontakt)
def _detail_dialog_oeffnen(self, kontakt):
"""Detail-Dialog mit allen Kontaktdaten und klickbaren Nummern."""
dialog = KontaktDetailDialog(kontakt, parent=self)
dialog.anrufen.connect(self.anrufen)
dialog.favorit_toggle.connect(
lambda n, nr, h: self.favorit_toggle.emit(n, nr, h))
dialog.exec()
if dialog.bearbeiten_gewuenscht:
self._kontakt_bearbeiten_dialog(kontakt)
def _anrufen_klick(self):
"""Anrufen-Button → erste verfügbare Nummer anrufen."""
kontakt = self._aktuelle_zeile_kontakt()
if not kontakt:
return
nummern = kontakt.get("nummern", {})
if not nummern and kontakt.get("nummer"):
nummern = {"telefon": kontakt["nummer"]}
# Erste verfügbare Nummer (Priorität: handy > telefon > geschäftlich > sonstige)
for typ in ("handy", "telefon", "geschaeftlich", "sonstige"):
if typ in nummern:
self.anrufen.emit(nummern[typ])
return
def _kontakt_hinzufuegen(self):
"""Neuen lokalen Kontakt hinzufügen."""
dialog = KontaktBearbeitenDialog(parent=self)
if dialog.exec() == QDialog.DialogCode.Accepted:
daten = dialog.kontakt_daten
if daten:
self._kontakte.append(daten)
self._kontakte.sort(
key=lambda k: k.get("name", "").lower())
self._tabelle_fuellen()
self._kontakte_speichern()
def _kontakt_bearbeiten(self):
"""Ausgewählten Kontakt bearbeiten (über Tabellen-Button)."""
kontakt = self._aktuelle_zeile_kontakt()
if not kontakt:
return
self._kontakt_bearbeiten_dialog(kontakt)
def _kontakt_bearbeiten_dialog(self, kontakt):
"""Edit-Dialog öffnen und Änderungen speichern (lokal + CardDAV)."""
dialog = KontaktBearbeitenDialog(kontakt=kontakt, parent=self)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
daten = dialog.kontakt_daten
if not daten:
return
quelle = kontakt.get("quelle", "lokal")
if quelle == "carddav":
self._carddav_kontakt_speichern(kontakt, daten)
else:
self._lokalen_kontakt_speichern(kontakt, daten)
def _lokalen_kontakt_speichern(self, kontakt_alt, kontakt_neu):
"""Lokalen Kontakt aktualisieren."""
alter_name = kontakt_alt.get("name", "")
for i, k in enumerate(self._kontakte):
if k.get("name") == alter_name:
self._kontakte[i] = kontakt_neu
break
else:
# Nicht gefunden → als neuen Kontakt anlegen
self._kontakte.append(kontakt_neu)
self._kontakte.sort(key=lambda k: k.get("name", "").lower())
self._tabelle_fuellen()
self._kontakte_speichern()
self.kontakte_geaendert.emit()
def _carddav_kontakt_speichern(self, kontakt_alt, kontakt_neu):
"""CardDAV-Kontakt auf Server schreiben (PUT)."""
account_name = kontakt_alt.get("account", "")
accounts = self._config.get("carddav", "accounts") or []
# Account-Credentials suchen
account = None
for acc in accounts:
if acc.get("name") == account_name:
account = acc
break
if not account:
QMessageBox.warning(
self, "Fehler",
f"CardDAV-Account '{account_name}' nicht gefunden.\n"
"Kontakt kann nicht gespeichert werden.")
return
from utils.carddav import CardDavSync
sync = CardDavSync()
erfolg, fehler = sync.kontakt_aktualisieren(
account["url"], account["benutzername"],
account["passwort"], kontakt_neu,
)
if erfolg:
# Cache aktualisieren
for i, k in enumerate(self._carddav_kontakte):
if k.get("_href") == kontakt_alt.get("_href"):
self._carddav_kontakte[i] = kontakt_neu
break
self._config.set("carddav", "kontakte_cache",
self._carddav_kontakte)
self._config.speichern()
self._tabelle_fuellen()
self.kontakte_geaendert.emit()
QMessageBox.information(
self, "Gespeichert",
f"Kontakt '{kontakt_neu.get('name')}' wurde auf dem "
f"Server aktualisiert.")
else:
QMessageBox.warning(
self, "Fehler beim Speichern",
f"Kontakt konnte nicht gespeichert werden:\n{fehler}")
def _kontakt_loeschen(self):
"""Ausgewählten Kontakt löschen (nur lokale)."""
kontakt = self._aktuelle_zeile_kontakt()
if not kontakt or kontakt.get("quelle") == "carddav":
return
name = kontakt.get("name")
self._kontakte = [k for k in self._kontakte if k.get("name") != name]
self._tabelle_fuellen()
self._kontakte_speichern()
def _kontakte_laden(self):
"""Kontakte aus Config laden (lokal + CardDAV-Cache)."""
self._kontakte = self._config.kontakte_laden()
self._carddav_kontakte = (
self._config.get("carddav", "kontakte_cache") or [])
self._tabelle_fuellen()
self.kontakte_geaendert.emit()
def _kontakte_speichern(self):
"""Lokale Kontakte in Config speichern."""
self._config.kontakte_speichern(self._kontakte)
def _carddav_sync(self):
"""CardDAV-Kontakte synchronisieren (alle Accounts)."""
accounts = self._config.get("carddav", "accounts") or []
if not accounts:
return
from utils.carddav import CardDavSync
sync = CardDavSync()
kontakte, fehler = sync.alle_accounts_abrufen(accounts)
if not fehler or kontakte:
self._carddav_kontakte = kontakte
self._config.set("carddav", "kontakte_cache", kontakte)
self._config.speichern()
self._tabelle_fuellen()
self.kontakte_geaendert.emit()
def _suche_filtern(self, text):
"""Tabellenzeilen nach Suchbegriff filtern."""
text = text.lower().strip()
for zeile in range(self.tabelle.rowCount()):
sichtbar = True
if text:
# In allen Spalten suchen
sichtbar = False
for spalte in range(self.tabelle.columnCount()):
item = self.tabelle.item(zeile, spalte)
if item and text in item.text().lower():
sichtbar = True
break
self.tabelle.setRowHidden(zeile, not sichtbar)
def aktualisieren(self):
"""Kontakte neu laden (extern aufrufbar)."""
self._kontakte_laden()
def alle_kontakte_fuer_suche(self):
"""Alle Kontakte für die Suche im Wählfeld bereitstellen."""
return self._alle_kontakte