Compare commits

..

3 commits

Author SHA1 Message Date
01963ed6a7
Fix indentation in locales file
All checks were successful
CI / test (push) Successful in 4m10s
Lint / test (push) Successful in 29s
Trivy / test (push) Successful in 23s
2025-11-20 14:34:32 +11:00
a7d2c5500e
Allow time log entries to be edited directly in their table cells 2025-11-20 14:33:32 +11:00
0bc5a37605
Bump version 2025-11-19 21:21:40 +11:00
4 changed files with 190 additions and 96 deletions

View file

@ -1,3 +1,7 @@
# 0.4.1
* Allow time log entries to be edited directly in their table cells
# 0.4 # 0.4
* Remove screenshot tool * Remove screenshot tool

View file

@ -181,6 +181,9 @@ class TimeLogDialog(QDialog):
self._db = db self._db = db
self._date_iso = date_iso self._date_iso = date_iso
self._current_entry_id: Optional[int] = None self._current_entry_id: Optional[int] = None
# Guard flag used when repopulating the table so we dont treat
# programmatic item changes as user edits.
self._reloading_entries: bool = False
self.setWindowTitle(strings._("time_log_for").format(date=date_iso)) self.setWindowTitle(strings._("time_log_for").format(date=date_iso))
self.resize(900, 600) self.resize(900, 600)
@ -264,6 +267,8 @@ class TimeLogDialog(QDialog):
self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SingleSelection) self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.itemSelectionChanged.connect(self._on_row_selected) self.table.itemSelectionChanged.connect(self._on_row_selected)
# When a cell is edited inline, commit the change back to the DB.
self.table.itemChanged.connect(self._on_table_item_changed)
root.addWidget(self.table, 1) root.addWidget(self.table, 1)
# --- Close button # --- Close button
@ -293,6 +298,14 @@ class TimeLogDialog(QDialog):
self.activity_edit.setCompleter(completer) self.activity_edit.setCompleter(completer)
def _reload_entries(self) -> None: def _reload_entries(self) -> None:
"""Reload the table from the database.
While we are repopulating the QTableWidget we temporarily disable the
itemChanged handler so that programmatic changes do not get written
back to the database.
"""
self._reloading_entries = True
try:
rows = self._db.time_log_for_date(self._date_iso) rows = self._db.time_log_for_date(self._date_iso)
self.table.setRowCount(len(rows)) self.table.setRowCount(len(rows))
for row_idx, r in enumerate(rows): for row_idx, r in enumerate(rows):
@ -315,6 +328,8 @@ class TimeLogDialog(QDialog):
self.table.setItem(row_idx, 1, item_act) self.table.setItem(row_idx, 1, item_act)
self.table.setItem(row_idx, 2, item_note) self.table.setItem(row_idx, 2, item_note)
self.table.setItem(row_idx, 3, item_hours) self.table.setItem(row_idx, 3, item_hours)
finally:
self._reloading_entries = False
self._current_entry_id = None self._current_entry_id = None
self.delete_btn.setEnabled(False) self.delete_btn.setEnabled(False)
@ -405,6 +420,81 @@ class TimeLogDialog(QDialog):
self.note.setText(note) self.note.setText(note)
self.hours_spin.setValue(hours) self.hours_spin.setValue(hours)
def _on_table_item_changed(self, item: QTableWidgetItem) -> None:
"""Commit inline edits in the table back to the database.
Editing a cell should behave like selecting that row and pressing
the Add/Update button, so we reuse the same validation and DB logic.
"""
if self._reloading_entries:
# Ignore changes that come from _reload_entries().
return
if item is None:
return
row = item.row()
proj_item = self.table.item(row, 0)
act_item = self.table.item(row, 1)
note_item = self.table.item(row, 2)
hours_item = self.table.item(row, 3)
if proj_item is None or act_item is None or hours_item is None:
# Incomplete row nothing to do.
return
# Recover the entry id from the hidden UserRole on the project cell
entry_id = proj_item.data(Qt.ItemDataRole.UserRole)
self._current_entry_id = int(entry_id) if entry_id is not None else None
# Push values into the editors (similar to _on_row_selected).
proj_name = proj_item.text()
act_name = act_item.text()
note_text = note_item.text() if note_item is not None else ""
hours_text = hours_item.text()
# Set project combo by name, creating a project on the fly if needed.
idx = self.project_combo.findText(proj_name, Qt.MatchFixedString)
if idx < 0 and proj_name:
# Allow creating a new project directly from the table.
proj_id = self._db.add_project(proj_name)
self._reload_projects()
idx = self.project_combo.findData(proj_id)
if idx >= 0:
self.project_combo.setCurrentIndex(idx)
else:
self.project_combo.setCurrentIndex(-1)
self.activity_edit.setText(act_name)
self.note.setText(note_text)
# Parse hours; if invalid, show the same style of warning as elsewhere.
try:
hours = float(hours_text)
except ValueError:
QMessageBox.warning(
self,
strings._("invalid_time_title"),
strings._("invalid_time_message"),
)
# Reset table back to the last known-good state.
self._reload_entries()
return
self.hours_spin.setValue(hours)
# Mirror button state to reflect whether we're updating or adding.
if self._current_entry_id is None:
self.delete_btn.setEnabled(False)
self.add_update_btn.setText(strings._("add_time_entry"))
else:
self.delete_btn.setEnabled(True)
self.add_update_btn.setText(strings._("update_time_entry"))
# Finally, reuse the existing validation + DB logic.
self._on_add_or_update()
def _on_delete_entry(self) -> None: def _on_delete_entry(self) -> None:
if self._current_entry_id is None: if self._current_entry_id is None:
return return

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "bouquin" name = "bouquin"
version = "0.3.2" version = "0.4.1"
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
authors = ["Miguel Jacq <mig@mig5.net>"] authors = ["Miguel Jacq <mig@mig5.net>"]
readme = "README.md" readme = "README.md"