Compare commits

..

No commits in common. "01963ed6a72ad26d209deee93cc77a2ec00a96f3" and "6bc6fe4b8360d6dd29141f302095688b0b6ba7a0" have entirely different histories.

4 changed files with 96 additions and 190 deletions

View file

@ -1,7 +1,3 @@
# 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

@ -162,80 +162,80 @@
"invalid_time_message": "Please enter a time in the format HH:MM", "invalid_time_message": "Please enter a time in the format HH:MM",
"dismiss": "Dismiss", "dismiss": "Dismiss",
"toolbar_alarm": "Set reminder alarm", "toolbar_alarm": "Set reminder alarm",
"activities": "Activities", "activities": "Activities",
"activity": "Activity", "activity": "Activity",
"note": "Note", "note": "Note",
"activity_delete_error_message": "A problem occurred deleting the activity", "activity_delete_error_message": "A problem occurred deleting the activity",
"activity_delete_error_title": "Problem deleting activity", "activity_delete_error_title": "Problem deleting activity",
"activity_rename_error_message": "A problem occurred renaming the activity", "activity_rename_error_message": "A problem occurred renaming the activity",
"activity_rename_error_title": "Problem renaming activity", "activity_rename_error_title": "Problem renaming activity",
"activity_required_message": "An activity name is required", "activity_required_message": "An activity name is required",
"activity_required_title": "Activity name required", "activity_required_title": "Activity name required",
"add_activity": "Add activity", "add_activity": "Add activity",
"add_project": "Add project", "add_project": "Add project",
"add_time_entry": "Add time entry", "add_time_entry": "Add time entry",
"time_period": "Time period", "time_period": "Time period",
"by_day": "by day", "by_day": "by day",
"by_month": "by month", "by_month": "by month",
"by_week": "by week", "by_week": "by week",
"date_range": "Date range", "date_range": "Date range",
"delete_activity": "Delete activity", "delete_activity": "Delete activity",
"delete_activity_confirm": "Are you sure you want to delete this activity?", "delete_activity_confirm": "Are you sure you want to delete this activity?",
"delete_activity_title": "Delete activity - are you sure?", "delete_activity_title": "Delete activity - are you sure?",
"delete_project": "Delete project", "delete_project": "Delete project",
"delete_project_confirm": "Are you sure you want to delete this project?", "delete_project_confirm": "Are you sure you want to delete this project?",
"delete_project_title": "Delete project - are you sure?", "delete_project_title": "Delete project - are you sure?",
"delete_time_entry": "Delete time entry", "delete_time_entry": "Delete time entry",
"group_by": "Group by", "group_by": "Group by",
"hours": "Hours", "hours": "Hours",
"invalid_activity_message": "The activity is invalid", "invalid_activity_message": "The activity is invalid",
"invalid_activity_title": "Invalid activity", "invalid_activity_title": "Invalid activity",
"invalid_project_message": "The project is invalid", "invalid_project_message": "The project is invalid",
"invalid_project_title": "Invalid project", "invalid_project_title": "Invalid project",
"label_key": "Label", "label_key": "Label",
"manage_activities": "Manage activities", "manage_activities": "Manage activities",
"manage_projects": "Manage projects", "manage_projects": "Manage projects",
"manage_projects_activities": "Manage project activities", "manage_projects_activities": "Manage project activities",
"open_time_log": "Open time log", "open_time_log": "Open time log",
"project": "Project", "project": "Project",
"project_delete_error_message": "A problem occurred deleting the project", "project_delete_error_message": "A problem occurred deleting the project",
"project_delete_error_title": "Problem deleting project", "project_delete_error_title": "Problem deleting project",
"project_rename_error_message": "A problem occurred renaming the project", "project_rename_error_message": "A problem occurred renaming the project",
"project_rename_error_title": "Problem renaming project", "project_rename_error_title": "Problem renaming project",
"project_required_message": "A project is required", "project_required_message": "A project is required",
"project_required_title": "Project required", "project_required_title": "Project required",
"projects": "Projects", "projects": "Projects",
"rename_activity": "Rename activity", "rename_activity": "Rename activity",
"rename_project": "Rename project", "rename_project": "Rename project",
"run_report": "Run report", "run_report": "Run report",
"add_project_label": "Add a project", "add_project_label": "Add a project",
"add_activity_label": "Add an activity", "add_activity_label": "Add an activity",
"select_activity_message": "Select an activity", "select_activity_message": "Select an activity",
"select_activity_title": "Select activity", "select_activity_title": "Select activity",
"select_project_message": "Select a project", "select_project_message": "Select a project",
"select_project_title": "Select project", "select_project_title": "Select project",
"time_log": "Time log", "time_log": "Time log",
"time_log_collapsed_hint": "Time log", "time_log_collapsed_hint": "Time log",
"time_log_date_label": "Time log date: {date}", "time_log_date_label": "Time log date: {date}",
"time_log_for": "Time log for {date}", "time_log_for": "Time log for {date}",
"time_log_no_date": "Time log", "time_log_no_date": "Time log",
"time_log_no_entries": "No time entries yet", "time_log_no_entries": "No time entries yet",
"time_log_report": "Time log report", "time_log_report": "Time log report",
"time_log_report_title": "Time log for {project}", "time_log_report_title": "Time log for {project}",
"time_log_report_meta": "From {start} to {end}, grouped {granularity}", "time_log_report_meta": "From {start} to {end}, grouped {granularity}",
"time_log_total_hours": "Total time spent", "time_log_total_hours": "Total time spent",
"time_log_with_total": "Time log ({hours:.2f}h)", "time_log_with_total": "Time log ({hours:.2f}h)",
"time_log_total_hours": "Total for day: {hours:.2f}h", "time_log_total_hours": "Total for day: {hours:.2f}h",
"title_key": "title", "title_key": "title",
"update_time_entry": "Update time entry", "update_time_entry": "Update time entry",
"time_report_total": "Total: {hours:.2f} hours", "time_report_total": "Total: {hours:.2f} hours",
"no_report_title": "No report", "no_report_title": "No report",
"no_report_message": "Please run a report before exporting.", "no_report_message": "Please run a report before exporting.",
"total": "Total", "total": "Total",
"export_csv": "Export CSV", "export_csv": "Export CSV",
"export_csv_error_title": "Export failed", "export_csv_error_title": "Export failed",
"export_csv_error_message": "Could not write CSV file:\n{error}", "export_csv_error_message": "Could not write CSV file:\n{error}",
"export_pdf": "Export PDF", "export_pdf": "Export PDF",
"export_pdf_error_title": "PDF export failed", "export_pdf_error_title": "PDF export failed",
"export_pdf_error_message": "Could not write PDF file:\n{error}" "export_pdf_error_message": "Could not write PDF file:\n{error}"
} }

View file

@ -181,9 +181,6 @@ 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)
@ -267,8 +264,6 @@ 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
@ -298,38 +293,28 @@ 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. rows = self._db.time_log_for_date(self._date_iso)
self.table.setRowCount(len(rows))
for row_idx, r in enumerate(rows):
entry_id = r[0]
project_name = r[3]
activity_name = r[5]
note = r[7] or ""
minutes = r[6]
hours = minutes / 60.0
While we are repopulating the QTableWidget we temporarily disable the item_proj = QTableWidgetItem(project_name)
itemChanged handler so that programmatic changes do not get written item_act = QTableWidgetItem(activity_name)
back to the database. item_note = QTableWidgetItem(note)
""" item_hours = QTableWidgetItem(f"{hours:.2f}")
self._reloading_entries = True
try:
rows = self._db.time_log_for_date(self._date_iso)
self.table.setRowCount(len(rows))
for row_idx, r in enumerate(rows):
entry_id = r[0]
project_name = r[3]
activity_name = r[5]
note = r[7] or ""
minutes = r[6]
hours = minutes / 60.0
item_proj = QTableWidgetItem(project_name) # store the entry id on the first column
item_act = QTableWidgetItem(activity_name) item_proj.setData(Qt.ItemDataRole.UserRole, entry_id)
item_note = QTableWidgetItem(note)
item_hours = QTableWidgetItem(f"{hours:.2f}")
# store the entry id on the first column self.table.setItem(row_idx, 0, item_proj)
item_proj.setData(Qt.ItemDataRole.UserRole, entry_id) self.table.setItem(row_idx, 1, item_act)
self.table.setItem(row_idx, 2, item_note)
self.table.setItem(row_idx, 0, item_proj) self.table.setItem(row_idx, 3, item_hours)
self.table.setItem(row_idx, 1, item_act)
self.table.setItem(row_idx, 2, item_note)
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)
@ -420,81 +405,6 @@ 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.4.1" version = "0.3.2"
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"