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>
863 lines
32 KiB
Python
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
|