diff --git a/CHANGELOG.md b/CHANGELOG.md index 52542b5..cbea884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,3 @@ -# 0.4.1 - - * Allow time log entries to be edited directly in their table cells - # 0.4 * Remove screenshot tool diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json index f7a38ea..cce6673 100644 --- a/bouquin/locales/en.json +++ b/bouquin/locales/en.json @@ -162,80 +162,80 @@ "invalid_time_message": "Please enter a time in the format HH:MM", "dismiss": "Dismiss", "toolbar_alarm": "Set reminder alarm", - "activities": "Activities", - "activity": "Activity", - "note": "Note", - "activity_delete_error_message": "A problem occurred deleting the activity", - "activity_delete_error_title": "Problem deleting activity", - "activity_rename_error_message": "A problem occurred renaming the activity", - "activity_rename_error_title": "Problem renaming activity", - "activity_required_message": "An activity name is required", - "activity_required_title": "Activity name required", - "add_activity": "Add activity", - "add_project": "Add project", - "add_time_entry": "Add time entry", - "time_period": "Time period", - "by_day": "by day", - "by_month": "by month", - "by_week": "by week", - "date_range": "Date range", - "delete_activity": "Delete activity", - "delete_activity_confirm": "Are you sure you want to delete this activity?", - "delete_activity_title": "Delete activity - are you sure?", - "delete_project": "Delete project", - "delete_project_confirm": "Are you sure you want to delete this project?", - "delete_project_title": "Delete project - are you sure?", - "delete_time_entry": "Delete time entry", - "group_by": "Group by", - "hours": "Hours", - "invalid_activity_message": "The activity is invalid", - "invalid_activity_title": "Invalid activity", - "invalid_project_message": "The project is invalid", - "invalid_project_title": "Invalid project", - "label_key": "Label", - "manage_activities": "Manage activities", - "manage_projects": "Manage projects", - "manage_projects_activities": "Manage project activities", - "open_time_log": "Open time log", - "project": "Project", - "project_delete_error_message": "A problem occurred deleting the project", - "project_delete_error_title": "Problem deleting project", - "project_rename_error_message": "A problem occurred renaming the project", - "project_rename_error_title": "Problem renaming project", - "project_required_message": "A project is required", - "project_required_title": "Project required", - "projects": "Projects", - "rename_activity": "Rename activity", - "rename_project": "Rename project", - "run_report": "Run report", - "add_project_label": "Add a project", - "add_activity_label": "Add an activity", - "select_activity_message": "Select an activity", - "select_activity_title": "Select activity", - "select_project_message": "Select a project", - "select_project_title": "Select project", - "time_log": "Time log", - "time_log_collapsed_hint": "Time log", - "time_log_date_label": "Time log date: {date}", - "time_log_for": "Time log for {date}", - "time_log_no_date": "Time log", - "time_log_no_entries": "No time entries yet", - "time_log_report": "Time log report", - "time_log_report_title": "Time log for {project}", - "time_log_report_meta": "From {start} to {end}, grouped {granularity}", - "time_log_total_hours": "Total time spent", - "time_log_with_total": "Time log ({hours:.2f}h)", - "time_log_total_hours": "Total for day: {hours:.2f}h", - "title_key": "title", - "update_time_entry": "Update time entry", - "time_report_total": "Total: {hours:.2f} hours", - "no_report_title": "No report", - "no_report_message": "Please run a report before exporting.", - "total": "Total", - "export_csv": "Export CSV", - "export_csv_error_title": "Export failed", - "export_csv_error_message": "Could not write CSV file:\n{error}", - "export_pdf": "Export PDF", - "export_pdf_error_title": "PDF export failed", - "export_pdf_error_message": "Could not write PDF file:\n{error}" + "activities": "Activities", + "activity": "Activity", + "note": "Note", + "activity_delete_error_message": "A problem occurred deleting the activity", + "activity_delete_error_title": "Problem deleting activity", + "activity_rename_error_message": "A problem occurred renaming the activity", + "activity_rename_error_title": "Problem renaming activity", + "activity_required_message": "An activity name is required", + "activity_required_title": "Activity name required", + "add_activity": "Add activity", + "add_project": "Add project", + "add_time_entry": "Add time entry", + "time_period": "Time period", + "by_day": "by day", + "by_month": "by month", + "by_week": "by week", + "date_range": "Date range", + "delete_activity": "Delete activity", + "delete_activity_confirm": "Are you sure you want to delete this activity?", + "delete_activity_title": "Delete activity - are you sure?", + "delete_project": "Delete project", + "delete_project_confirm": "Are you sure you want to delete this project?", + "delete_project_title": "Delete project - are you sure?", + "delete_time_entry": "Delete time entry", + "group_by": "Group by", + "hours": "Hours", + "invalid_activity_message": "The activity is invalid", + "invalid_activity_title": "Invalid activity", + "invalid_project_message": "The project is invalid", + "invalid_project_title": "Invalid project", + "label_key": "Label", + "manage_activities": "Manage activities", + "manage_projects": "Manage projects", + "manage_projects_activities": "Manage project activities", + "open_time_log": "Open time log", + "project": "Project", + "project_delete_error_message": "A problem occurred deleting the project", + "project_delete_error_title": "Problem deleting project", + "project_rename_error_message": "A problem occurred renaming the project", + "project_rename_error_title": "Problem renaming project", + "project_required_message": "A project is required", + "project_required_title": "Project required", + "projects": "Projects", + "rename_activity": "Rename activity", + "rename_project": "Rename project", + "run_report": "Run report", + "add_project_label": "Add a project", + "add_activity_label": "Add an activity", + "select_activity_message": "Select an activity", + "select_activity_title": "Select activity", + "select_project_message": "Select a project", + "select_project_title": "Select project", + "time_log": "Time log", + "time_log_collapsed_hint": "Time log", + "time_log_date_label": "Time log date: {date}", + "time_log_for": "Time log for {date}", + "time_log_no_date": "Time log", + "time_log_no_entries": "No time entries yet", + "time_log_report": "Time log report", + "time_log_report_title": "Time log for {project}", + "time_log_report_meta": "From {start} to {end}, grouped {granularity}", + "time_log_total_hours": "Total time spent", + "time_log_with_total": "Time log ({hours:.2f}h)", + "time_log_total_hours": "Total for day: {hours:.2f}h", + "title_key": "title", + "update_time_entry": "Update time entry", + "time_report_total": "Total: {hours:.2f} hours", + "no_report_title": "No report", + "no_report_message": "Please run a report before exporting.", + "total": "Total", + "export_csv": "Export CSV", + "export_csv_error_title": "Export failed", + "export_csv_error_message": "Could not write CSV file:\n{error}", + "export_pdf": "Export PDF", + "export_pdf_error_title": "PDF export failed", + "export_pdf_error_message": "Could not write PDF file:\n{error}" } diff --git a/bouquin/time_log.py b/bouquin/time_log.py index 3cb30bf..8dac971 100644 --- a/bouquin/time_log.py +++ b/bouquin/time_log.py @@ -181,9 +181,6 @@ class TimeLogDialog(QDialog): self._db = db self._date_iso = date_iso self._current_entry_id: Optional[int] = None - # Guard flag used when repopulating the table so we don’t treat - # programmatic item changes as user edits. - self._reloading_entries: bool = False self.setWindowTitle(strings._("time_log_for").format(date=date_iso)) self.resize(900, 600) @@ -267,8 +264,6 @@ class TimeLogDialog(QDialog): self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setSelectionMode(QAbstractItemView.SingleSelection) 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) # --- Close button @@ -298,38 +293,28 @@ class TimeLogDialog(QDialog): self.activity_edit.setCompleter(completer) 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 - 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) - 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) + item_act = QTableWidgetItem(activity_name) + item_note = QTableWidgetItem(note) + item_hours = QTableWidgetItem(f"{hours:.2f}") - item_proj = QTableWidgetItem(project_name) - item_act = QTableWidgetItem(activity_name) - item_note = QTableWidgetItem(note) - item_hours = QTableWidgetItem(f"{hours:.2f}") + # store the entry id on the first column + item_proj.setData(Qt.ItemDataRole.UserRole, entry_id) - # store the entry id on the first column - item_proj.setData(Qt.ItemDataRole.UserRole, entry_id) - - self.table.setItem(row_idx, 0, item_proj) - 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.table.setItem(row_idx, 0, item_proj) + self.table.setItem(row_idx, 1, item_act) + self.table.setItem(row_idx, 2, item_note) + self.table.setItem(row_idx, 3, item_hours) self._current_entry_id = None self.delete_btn.setEnabled(False) @@ -420,81 +405,6 @@ class TimeLogDialog(QDialog): self.note.setText(note) 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: if self._current_entry_id is None: return diff --git a/pyproject.toml b/pyproject.toml index 3e0bbff..0057d21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.4.1" +version = "0.3.2" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md"