From b3892bb47dd30d7ab0bbf5bc12de2539ee2ffa03 Mon Sep 17 00:00:00 2001 From: matin Date: Tue, 30 Sep 2025 15:38:15 +0200 Subject: =?UTF-8?q?-=20Tabelle=20ist=20editierbar=20-=20Kommentarfeld=20ma?= =?UTF-8?q?cht=20Zeilenumbr=C3=BCche=20-=20im=20Editier-Dialog=20wird=20da?= =?UTF-8?q?s=20Startdatum=20nun=20auch=20automatisch=20verstellt,=20wenn?= =?UTF-8?q?=20das=20enddatum=20derart=20ge=C3=A4ndert=20wird,=20dass=20ein?= =?UTF-8?q?=20ung=C3=BCltiger=20Zeitraum=20entstehen=20w=C3=BCrde?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- calendar_gui.py | 202 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 159 insertions(+), 43 deletions(-) (limited to 'calendar_gui.py') diff --git a/calendar_gui.py b/calendar_gui.py index 8863b8e..5fadd73 100644 --- a/calendar_gui.py +++ b/calendar_gui.py @@ -2,7 +2,8 @@ import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QDateEdit, QTableWidget, QTableWidgetItem, QHeaderView, QDialog, QFormLayout, QComboBox, - QMessageBox, QSpinBox, QAction, QFileDialog, QMenuBar, QTextEdit) + QMessageBox, QSpinBox, QAction, QFileDialog, QMenuBar, QTextEdit, + QAbstractItemView) from PyQt5.QtCore import Qt, QDate, QLocale from PyQt5.QtGui import QFont, QFontDatabase from datetime import datetime, timedelta @@ -112,6 +113,9 @@ class EventDialog(QDialog): self.setLayout(layout) + # Connect end date change to validate start date (keep range consistent both ways) + self.end_date.dateChanged.connect(self.validate_start_date) + # Handle initial keyword selection self.on_keyword_changed(self.keyword.currentText()) @@ -139,6 +143,11 @@ class EventDialog(QDialog): """Ensure end date is not before start date""" if self.end_date.date() < self.start_date.date(): self.end_date.setDate(self.start_date.date()) + + def validate_start_date(self): + """Ensure start date is not after end date when end changes""" + if self.start_date.date() > self.end_date.date(): + self.start_date.setDate(self.end_date.date()) def get_data(self): start_date = DateService.format_date_for_iso(self.start_date.date()) keyword = self.keyword.currentText() @@ -282,6 +291,15 @@ class CalendarManagerGUI(QMainWindow): self.events_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.events_table.horizontalHeader().setFont(self.app_font) self.events_table.setColumnHidden(0, True) # Hide ID column + # Wrap long text and auto-resize rows so comments are readable + self.events_table.setWordWrap(True) + self.events_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) + # Enable inline editing on double click / Enter; keep computed column read-only in population step + self._suppress_item_changed = False + self.events_table.setEditTriggers( + QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked | QAbstractItemView.EditKeyPressed + ) + self.events_table.itemChanged.connect(self.on_events_item_changed) events_layout.addWidget(self.events_table) main_layout.addLayout(events_layout) @@ -363,50 +381,148 @@ class CalendarManagerGUI(QMainWindow): def update_events_table(self): # Clear table - self.events_table.setRowCount(0) + self._suppress_item_changed = True + try: + self.events_table.setRowCount(0) - # Add entries to table - entries = self.calendar_manager.list_entries() - for i, entry in enumerate(entries): - self.events_table.insertRow(i) - - # Set item data - self.events_table.setItem(i, 0, QTableWidgetItem(entry.id)) - - # Format dates using unified display format - start_date_text = DateService.format_date_for_display(entry.start_date, self.dateformat) - end_date_text = DateService.format_date_for_display(entry.end_date, self.dateformat) - self.events_table.setItem(i, 1, QTableWidgetItem(start_date_text)) - self.events_table.setItem(i, 2, QTableWidgetItem(end_date_text)) - self.events_table.setItem(i, 3, QTableWidgetItem(entry.keyword)) - - # Relevant accounted time from stored time_period - relevant_text = getattr(entry, 'time_period', "") or "" - self.events_table.setItem(i, 4, QTableWidgetItem(relevant_text)) + # Add entries to table + entries = self.calendar_manager.list_entries() + for i, entry in enumerate(entries): + self.events_table.insertRow(i) + + # Set item data + self.events_table.setItem(i, 0, QTableWidgetItem(entry.id)) + + # Format dates using unified display format + start_date_text = DateService.format_date_for_display(entry.start_date, self.dateformat) + end_date_text = DateService.format_date_for_display(entry.end_date, self.dateformat) + self.events_table.setItem(i, 1, QTableWidgetItem(start_date_text)) + self.events_table.setItem(i, 2, QTableWidgetItem(end_date_text)) + # Keyword as dropdown (combobox) for safer selection + keyword_combo = QComboBox() + keyword_combo.setFont(self.app_font) + keyword_combo.addItems(EventConfig.KEYWORDS) + if entry.keyword in EventConfig.KEYWORDS: + keyword_combo.setCurrentIndex(EventConfig.KEYWORDS.index(entry.keyword)) + keyword_combo.currentTextChanged.connect(lambda new_kw, eid=entry.id: self.on_keyword_changed_in_table(eid, new_kw)) + self.events_table.setCellWidget(i, 3, keyword_combo) + + # Relevant accounted time from stored time_period + relevant_text = getattr(entry, 'time_period', "") or "" + relevant_item = QTableWidgetItem(relevant_text) + # Make computed column read-only + relevant_item.setFlags(relevant_item.flags() & ~Qt.ItemIsEditable) + self.events_table.setItem(i, 4, relevant_item) - # Commentary - commentary_text = getattr(entry, 'commentary', "") or "" - self.events_table.setItem(i, 5, QTableWidgetItem(commentary_text)) - - # Action buttons - actions_widget = QWidget() - actions_layout = QHBoxLayout() - actions_layout.setContentsMargins(0, 0, 0, 0) - - modify_button = QPushButton("Editieren") - modify_button.setFont(self.app_font) - delete_button = QPushButton("Löschen") - delete_button.setFont(self.app_font) - - # Use lambda with default argument to capture the correct event_id - modify_button.clicked.connect(lambda checked, eid=entry.id: self.modify_event(eid)) - delete_button.clicked.connect(lambda checked, eid=entry.id: self.delete_event(eid)) - - actions_layout.addWidget(modify_button) - actions_layout.addWidget(delete_button) - actions_widget.setLayout(actions_layout) - - self.events_table.setCellWidget(i, 6, actions_widget) + # Commentary + commentary_text = getattr(entry, 'commentary', "") or "" + commentary_item = QTableWidgetItem(commentary_text) + self.events_table.setItem(i, 5, commentary_item) + + # Action buttons + actions_widget = QWidget() + actions_layout = QHBoxLayout() + actions_layout.setContentsMargins(0, 0, 0, 0) + + modify_button = QPushButton("Editieren") + modify_button.setFont(self.app_font) + delete_button = QPushButton("Löschen") + delete_button.setFont(self.app_font) + + # Use lambda with default argument to capture the correct event_id + modify_button.clicked.connect(lambda checked, eid=entry.id: self.modify_event(eid)) + delete_button.clicked.connect(lambda checked, eid=entry.id: self.delete_event(eid)) + + actions_layout.addWidget(modify_button) + actions_layout.addWidget(delete_button) + actions_widget.setLayout(actions_layout) + + self.events_table.setCellWidget(i, 6, actions_widget) + # Adjust row heights after population to show wrapped text + self.events_table.resizeRowsToContents() + finally: + self._suppress_item_changed = False + + def on_events_item_changed(self, item): + # Avoid reacting to programmatic changes + if self._suppress_item_changed: + return + row = item.row() + col = item.column() + # Ignore non-editable/computed/action columns + if col in (0, 4, 6): + return + + # Retrieve entry id + id_item = self.events_table.item(row, 0) + if id_item is None: + return + event_id = id_item.text() + + # Collect current row values + start_text = self.events_table.item(row, 1).text() if self.events_table.item(row, 1) else "" + end_text = self.events_table.item(row, 2).text() if self.events_table.item(row, 2) else "" + # Keyword comes from the combobox cell widget + keyword_widget = self.events_table.cellWidget(row, 3) + keyword_text = keyword_widget.currentText() if isinstance(keyword_widget, QComboBox) else (self.events_table.item(row, 3).text() if self.events_table.item(row, 3) else "") + commentary_text = self.events_table.item(row, 5).text() if self.events_table.item(row, 5) else "" + + # Normalize to ISO strings for backend + try: + start_iso = DateService.format_date_for_iso(DateService.parse_date_from_string(start_text)) if start_text else None + end_iso = DateService.format_date_for_iso(DateService.parse_date_from_string(end_text)) if end_text else None + except Exception as e: + QMessageBox.warning(self, "Ungültiges Datum", f"Bitte ein gültiges Datum eingeben.\nFehler: {str(e)}") + # Revert table to backend values + self.update_events_table() + return + + # Validate before applying + errors = [] + if start_iso and end_iso: + errors = self.calendar_manager.validate_entry_data(start_iso, end_iso, keyword_text) + if errors: + QMessageBox.warning(self, "Validierungsfehler", "\n".join(errors)) + self.update_events_table() + return + + # Apply modification + modified = self.calendar_manager.modify_entry( + event_id, + start_date=start_iso if start_iso else None, + end_date=end_iso if end_iso else None, + keyword=keyword_text if keyword_text else None, + commentary=commentary_text if commentary_text is not None else None, + ) + if modified is None: + QMessageBox.critical(self, "Fehler", "Eintrag konnte nicht aktualisiert werden.") + self.update_events_table() + return + + # Refresh to reflect normalized display and recomputed values + self.update_events_table() + self.update_prediction() + + def on_keyword_changed_in_table(self, event_id, new_keyword): + # Validate using current start/end from the entry + entry = self.calendar_manager.get_entry_by_id(event_id) + if not entry: + return + start_iso = DateService.format_date_for_iso(entry.start_date) + end_iso = DateService.format_date_for_iso(entry.end_date) + errors = self.calendar_manager.validate_entry_data(start_iso, end_iso, new_keyword) + if errors: + QMessageBox.warning(self, "Validierungsfehler", "\n".join(errors)) + # Re-render to revert combo to valid value + self.update_events_table() + return + modified = self.calendar_manager.modify_entry(event_id, keyword=new_keyword) + if modified is None: + QMessageBox.critical(self, "Fehler", "Eintrag konnte nicht aktualisiert werden.") + self.update_events_table() + return + self.update_events_table() + self.update_prediction() def save_file(self): """Save the complete prediction state to a .prediction.json file""" -- cgit v1.1