begin adding state management to new document selection

This commit is contained in:
2026-07-30 16:41:50 +02:00
parent 07ae18271e
commit 2b6268e38a
4 changed files with 277 additions and 74 deletions
-412
View File
@@ -1,412 +0,0 @@
from __future__ import annotations
import datetime
import os
import sys
from collections.abc import Iterable
from pathlib import Path
from typing import TYPE_CHECKING
from PySide6.QtCore import (
QFileInfo,
QMimeDatabase,
Qt,
QUrl,
Signal,
)
from PySide6.QtGui import QDesktopServices, QDragEnterEvent, QDropEvent
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QFileDialog,
QFileIconProvider,
QFrame,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
from wce_crm.constants import TIMEZONE_CEST
if TYPE_CHECKING:
from wce_crm.data_models import Module
def get_mime_type(
file_path: Path,
) -> str:
"""gets the MIME type of a file via PySide6 / QMimeDatabase"""
db = QMimeDatabase()
mime_type = db.mimeTypeForFile(str(file_path))
# .name() -> for example "application/pdf" or "image/png"
return mime_type.name()
def _format_file_size(size_in_bytes):
"""converts bytes in readable string, e.g. KB or MB"""
if size_in_bytes < 1024:
return f"{size_in_bytes} B"
elif size_in_bytes < 1024 * 1024:
return f"{size_in_bytes / 1024:.1f} KB"
else:
return f"{size_in_bytes / (1024 * 1024):.1f} MB"
class _DropZoneWidget(QFrame):
files_dropped = Signal(list) # sends list of pathlib.Path objects
STYLE_NORMAL = """
_DropZoneWidget {
border: 2px dashed #B0B0B0;
border-radius: 8px;
background-color: #F9F9F9;
}
_DropZoneWidget:hover {
border-color: #0078D4;
background-color: #F0F6FF;
}
"""
STYLE_DRAG_ACTIVE = """
_DropZoneWidget {
border: 2px dashed #0078D4;
border-radius: 8px;
background-color: #E1EFFA;
}
"""
def __init__(
self,
parent: QWidget | None = None,
):
super().__init__(parent)
self.setAcceptDrops(True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setStyleSheet(self.STYLE_NORMAL)
self.setFixedHeight(60)
layout = QVBoxLayout(self)
self.label = QLabel(
"📁 <b>Dateien hierher ziehen</b> oder <u>klicken</u> zum Auswählen"
)
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.label)
def dragEnterEvent(
self,
event: QDragEnterEvent,
):
if event.mimeData().hasUrls():
# check if MDB/explorer gives at least one real file (no folder)
has_files = False
for url in event.mimeData().urls():
pfad = url.toLocalFile()
if pfad and os.path.isfile(pfad):
has_files = True
break
if has_files:
event.acceptProposedAction()
self.setStyleSheet(self.STYLE_DRAG_ACTIVE)
def dragLeaveEvent(self, event):
self.setStyleSheet(self.STYLE_NORMAL)
def dropEvent(self, event: QDropEvent):
self.dragLeaveEvent(None)
valid_files: list[Path] = []
for url in event.mimeData().urls():
if url.isLocalFile():
path = Path(url.toLocalFile()).resolve()
# only add if it is a file
if path.is_file():
valid_files.append(path)
if valid_files:
self.files_dropped.emit(valid_files)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
files, _ = QFileDialog.getOpenFileNames(self, "Dokumente auswählen")
if files:
files_pathlib = [Path(file).resolve() for file in files]
self.files_dropped.emit(files_pathlib)
class _FileItemWidget(QFrame):
request_remove = Signal(QWidget) # signal to parent to remove entry
def __init__(
self,
file_path: Path,
parent: QWidget | None = None,
):
super().__init__(parent)
self.file_path = file_path
self._created_at_UTC = datetime.datetime.now(tz=datetime.UTC)
self.created_at_string = self._created_at_UTC.astimezone(TIMEZONE_CEST).strftime(
"%d.%m.%Y %H:%M"
)
self.setStyleSheet("""
_FileItemWidget {
background-color: #FFFFFF;
border: 1px solid #E0E0E0;
border-radius: 6px;
padding: 6px;
}
_FileItemWidget:hover {
border-color: #B0B0B0;
}
""")
layout = QHBoxLayout(self)
layout.setContentsMargins(8, 6, 8, 6)
# file icon: use dummy to avoid icons with checkmarks (synchronised folder)
icon_label = QLabel()
provider = QFileIconProvider()
file_icon = provider.icon(QFileInfo(f"dummy{file_path.suffix}"))
icon_label.setPixmap(file_icon.pixmap(28, 28))
layout.addWidget(icon_label)
# file info: name, size, date, mime type
info_vbox = QVBoxLayout()
info_vbox.setSpacing(2)
self.original_name = file_path.name
self.mime_type = get_mime_type(file_path)
self._raw_file_size: int | None = None
self.file_size_display: str
try:
self._raw_file_size = file_path.stat().st_size
self.file_size_display = _format_file_size(self._raw_file_size)
except OSError:
self.file_size_display = "Unbekannt"
name_label = QLabel(f"<b>{self.original_name}</b>")
meta_label = QLabel(
(
f"<span style='color: #666666;'>{self.file_size_display} • Hinzugefügt: "
f"{self.created_at_string}</span>"
)
)
meta_label.setStyleSheet("font-size: 11px;")
# additional note field for more comprehensible file descriptions
self.note_edit = QLineEdit()
self.note_edit.setPlaceholderText("Eigene Anmerkung / Bez. eingeben (optional)...")
self.note_edit.setStyleSheet("""
QLineEdit {
font-size: 11px;
padding: 2px 5px;
min-height: 22px; /* avoid vertical cropping of text */
border: 1px solid #CCCCCC;
border-radius: 3px;
}
QLineEdit:focus {
border-color: #0078D4; /* blue frame on click */
}
""")
info_vbox.addWidget(name_label)
info_vbox.addWidget(meta_label)
info_vbox.addWidget(self.note_edit)
layout.addLayout(info_vbox, stretch=2)
# category dropdown: not needed by WCE, but still included to re-enable later
# TODO maybe add later
self.category_combo = QComboBox()
self.category_combo.addItems(
[
"Allgemein / Unsortiert",
"Identitätsnachweis",
"Vertrag",
"Gewerbeanmeldung",
"Sonstiges",
]
)
self.category_combo.setFixedWidth(160)
layout.addWidget(self.category_combo)
self.category_combo.setVisible(False)
# action buttons: view and delete
self.btn_preview = QPushButton("👁️")
self.btn_preview.setToolTip("Datei im Standardprogramm öffnen")
self.btn_preview.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_preview.setFixedWidth(36)
self.btn_preview.clicked.connect(self._open_preview)
self.btn_delete = QPushButton("🗑️")
self.btn_delete.setToolTip("Datei entfernen")
self.btn_delete.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_delete.setFixedWidth(36)
self.btn_delete.clicked.connect(lambda: self.request_remove.emit(self))
layout.addWidget(self.btn_preview)
layout.addWidget(self.btn_delete)
def _open_preview(self):
QDesktopServices.openUrl(QUrl.fromLocalFile(self.file_path))
# TODO change to protocol/interface with state
def get_data(self):
return {
"file_path": self.file_path,
"created_at": self._created_at_UTC,
"category": None,
"note": self.note_edit.text().strip(),
}
class DocumentSectionWidget(QWidget):
"""main widget for file selection"""
count_changed = Signal(int) # sends current number of files contained by the widget
def __init__(
self,
parent: QWidget | None = None,
):
super().__init__(parent)
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
# Dropzone at the top
self.drop_zone = _DropZoneWidget()
self.drop_zone.files_dropped.connect(self._add_files)
main_layout.addWidget(self.drop_zone)
# list containing the file items
self.file_list_widget = QListWidget()
self.file_list_widget.setStyleSheet("""
QListWidget {
border: none;
background: transparent;
padding: 0px;
margin: 0px;
}
QListWidget::item {
padding: 0px;
margin: 0px 0px 6px 0px; /* 6px Abstand zwischen den einzelnen Datei-Zeilen */
}
""")
main_layout.addWidget(self.file_list_widget)
def _add_files(
self,
filepaths: Iterable[Path],
):
for path in filepaths:
# create custom row widget
item_widget = _FileItemWidget(path)
item_widget.request_remove.connect(self._remove_file_item)
# embed in QListWidget
list_item = QListWidgetItem(self.file_list_widget)
# min size to avoid cropping of text
size_hint = item_widget.sizeHint()
size_hint.setHeight(size_hint.height() + 4)
list_item.setSizeHint(size_hint)
self.file_list_widget.addItem(list_item)
self.file_list_widget.setItemWidget(list_item, item_widget)
self.count_changed.emit(self.file_list_widget.count())
def _remove_file_item(
self,
widget: QWidget,
):
# search and delete row
for i in range(self.file_list_widget.count()):
item = self.file_list_widget.item(i)
if self.file_list_widget.itemWidget(item) == widget:
self.file_list_widget.takeItem(i)
break
self.count_changed.emit(self.file_list_widget.count())
# TODO change to protocol/interface with state
def get_all_documents_data(self):
"""Methode zum Auslesen aller angehängten Dateien inkl. Metadaten für das Speichern."""
documents = []
for i in range(self.file_list_widget.count()):
item = self.file_list_widget.item(i)
widget = self.file_list_widget.itemWidget(item)
if isinstance(widget, _FileItemWidget):
documents.append(widget.get_data())
return documents
class _MainWindow(QMainWindow):
"""main window for internal test only, not the this module's component"""
def __init__(self):
super().__init__()
self.setWindowTitle("Prototyp: Dokumente & Anhänge")
self.resize(650, 500)
central_widget = QWidget()
layout = QVBoxLayout(central_widget)
# simulate custom widget in form
self.header_label = QLabel("<b>📎 Dokumente & Anhänge (0)</b>")
self.header_label.setStyleSheet("font-size: 14px; color: #333333;")
layout.addWidget(self.header_label)
# custom widget
self.doc_section = DocumentSectionWidget()
self.doc_section.count_changed.connect(self.update_header)
layout.addWidget(self.doc_section)
# test button to trigger data gathering (simulation for saving procedure and
# backend interaction)
self.btn_save = QPushButton("Erfassung speichern (Daten auslesen)")
self.btn_save.setStyleSheet(
"background-color: #0078D4; color: white; padding: 8px; font-weight: bold;"
)
self.btn_save.clicked.connect(self.print_backend_data)
layout.addWidget(self.btn_save)
self.setCentralWidget(central_widget)
def update_header(self, count):
"""update counter in corresponding label, must be mapped to section heading in form"""
self.header_label.setText(f"<b>📎 Dokumente & Anhänge ({count})</b>")
def print_backend_data(self):
data = self.doc_section.get_all_documents_data()
print("\n--- DATEN FÜR SPEICHERVORGANG BEREIT ---")
for i, doc in enumerate(data, 1):
print(f"Datei {i}:")
print(f" Pfad: {doc['file_path']}")
print(f" Datum: {doc['created_at']}")
print(f" Kategorie: {doc['category']}")
print(f" Anmerkung: {doc['note']}")
def main() -> None:
app = QApplication(sys.argv)
window = _MainWindow()
window.show()
sys.exit(app.exec())
if TYPE_CHECKING:
# enable static type checking for the protocol
_m1: type[Module] = DocumentSectionWidget
_m2: type[Module] = _FileItemWidget
if __name__ == "__main__":
main()