bouquin 0.4__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bouquin-0.4 → bouquin-0.4.1}/PKG-INFO +1 -1
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/locales/en.json +77 -76
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/main_window.py +13 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/markdown_editor.py +139 -38
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/tag_browser.py +4 -4
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/time_log.py +127 -38
- {bouquin-0.4 → bouquin-0.4.1}/pyproject.toml +1 -1
- {bouquin-0.4 → bouquin-0.4.1}/LICENSE +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/README.md +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/__init__.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/__main__.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/bug_report_dialog.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/db.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/find_bar.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/flow_layout.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/history_dialog.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/key_prompt.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/locales/fr.json +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/locales/it.json +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/lock_overlay.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/main.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/markdown_highlighter.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/save_dialog.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/search.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/settings.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/settings_dialog.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/statistics_dialog.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/strings.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/tags_widget.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/theme.py +0 -0
- {bouquin-0.4 → bouquin-0.4.1}/bouquin/toolbar.py +0 -0
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"behaviour": "Behaviour",
|
|
37
37
|
"never": "Never",
|
|
38
38
|
"browse": "Browse",
|
|
39
|
+
"close_tab": "Close tab",
|
|
39
40
|
"previous": "Previous",
|
|
40
41
|
"previous_day": "Previous day",
|
|
41
42
|
"next": "Next",
|
|
@@ -162,80 +163,80 @@
|
|
|
162
163
|
"invalid_time_message": "Please enter a time in the format HH:MM",
|
|
163
164
|
"dismiss": "Dismiss",
|
|
164
165
|
"toolbar_alarm": "Set reminder alarm",
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
166
|
+
"activities": "Activities",
|
|
167
|
+
"activity": "Activity",
|
|
168
|
+
"note": "Note",
|
|
169
|
+
"activity_delete_error_message": "A problem occurred deleting the activity",
|
|
170
|
+
"activity_delete_error_title": "Problem deleting activity",
|
|
171
|
+
"activity_rename_error_message": "A problem occurred renaming the activity",
|
|
172
|
+
"activity_rename_error_title": "Problem renaming activity",
|
|
173
|
+
"activity_required_message": "An activity name is required",
|
|
174
|
+
"activity_required_title": "Activity name required",
|
|
175
|
+
"add_activity": "Add activity",
|
|
176
|
+
"add_project": "Add project",
|
|
177
|
+
"add_time_entry": "Add time entry",
|
|
178
|
+
"time_period": "Time period",
|
|
179
|
+
"by_day": "by day",
|
|
180
|
+
"by_month": "by month",
|
|
181
|
+
"by_week": "by week",
|
|
182
|
+
"date_range": "Date range",
|
|
183
|
+
"delete_activity": "Delete activity",
|
|
184
|
+
"delete_activity_confirm": "Are you sure you want to delete this activity?",
|
|
185
|
+
"delete_activity_title": "Delete activity - are you sure?",
|
|
186
|
+
"delete_project": "Delete project",
|
|
187
|
+
"delete_project_confirm": "Are you sure you want to delete this project?",
|
|
188
|
+
"delete_project_title": "Delete project - are you sure?",
|
|
189
|
+
"delete_time_entry": "Delete time entry",
|
|
190
|
+
"group_by": "Group by",
|
|
191
|
+
"hours": "Hours",
|
|
192
|
+
"invalid_activity_message": "The activity is invalid",
|
|
193
|
+
"invalid_activity_title": "Invalid activity",
|
|
194
|
+
"invalid_project_message": "The project is invalid",
|
|
195
|
+
"invalid_project_title": "Invalid project",
|
|
196
|
+
"label_key": "Label",
|
|
197
|
+
"manage_activities": "Manage activities",
|
|
198
|
+
"manage_projects": "Manage projects",
|
|
199
|
+
"manage_projects_activities": "Manage project activities",
|
|
200
|
+
"open_time_log": "Open time log",
|
|
201
|
+
"project": "Project",
|
|
202
|
+
"project_delete_error_message": "A problem occurred deleting the project",
|
|
203
|
+
"project_delete_error_title": "Problem deleting project",
|
|
204
|
+
"project_rename_error_message": "A problem occurred renaming the project",
|
|
205
|
+
"project_rename_error_title": "Problem renaming project",
|
|
206
|
+
"project_required_message": "A project is required",
|
|
207
|
+
"project_required_title": "Project required",
|
|
208
|
+
"projects": "Projects",
|
|
209
|
+
"rename_activity": "Rename activity",
|
|
210
|
+
"rename_project": "Rename project",
|
|
211
|
+
"run_report": "Run report",
|
|
212
|
+
"add_project_label": "Add a project",
|
|
213
|
+
"add_activity_label": "Add an activity",
|
|
214
|
+
"select_activity_message": "Select an activity",
|
|
215
|
+
"select_activity_title": "Select activity",
|
|
216
|
+
"select_project_message": "Select a project",
|
|
217
|
+
"select_project_title": "Select project",
|
|
218
|
+
"time_log": "Time log",
|
|
219
|
+
"time_log_collapsed_hint": "Time log",
|
|
220
|
+
"time_log_date_label": "Time log date: {date}",
|
|
221
|
+
"time_log_for": "Time log for {date}",
|
|
222
|
+
"time_log_no_date": "Time log",
|
|
223
|
+
"time_log_no_entries": "No time entries yet",
|
|
224
|
+
"time_log_report": "Time log report",
|
|
225
|
+
"time_log_report_title": "Time log for {project}",
|
|
226
|
+
"time_log_report_meta": "From {start} to {end}, grouped {granularity}",
|
|
227
|
+
"time_log_total_hours": "Total time spent",
|
|
228
|
+
"time_log_with_total": "Time log ({hours:.2f}h)",
|
|
229
|
+
"time_log_total_hours": "Total for day: {hours:.2f}h",
|
|
230
|
+
"title_key": "title",
|
|
231
|
+
"update_time_entry": "Update time entry",
|
|
232
|
+
"time_report_total": "Total: {hours:.2f} hours",
|
|
233
|
+
"no_report_title": "No report",
|
|
234
|
+
"no_report_message": "Please run a report before exporting.",
|
|
235
|
+
"total": "Total",
|
|
236
|
+
"export_csv": "Export CSV",
|
|
237
|
+
"export_csv_error_title": "Export failed",
|
|
238
|
+
"export_csv_error_message": "Could not write CSV file:\n{error}",
|
|
239
|
+
"export_pdf": "Export PDF",
|
|
240
|
+
"export_pdf_error_title": "PDF export failed",
|
|
241
|
+
"export_pdf_error_message": "Could not write PDF file:\n{error}"
|
|
241
242
|
}
|
|
@@ -260,6 +260,13 @@ class MainWindow(QMainWindow):
|
|
|
260
260
|
nav_menu.addAction(act_today)
|
|
261
261
|
self.addAction(act_today)
|
|
262
262
|
|
|
263
|
+
act_close_tab = QAction(strings._("close_tab"), self)
|
|
264
|
+
act_close_tab.setShortcut("Ctrl+W")
|
|
265
|
+
act_close_tab.setShortcutContext(Qt.ApplicationShortcut)
|
|
266
|
+
act_close_tab.triggered.connect(self._close_current_tab)
|
|
267
|
+
nav_menu.addAction(act_close_tab)
|
|
268
|
+
self.addAction(act_close_tab)
|
|
269
|
+
|
|
263
270
|
act_find = QAction(strings._("find_on_page"), self)
|
|
264
271
|
act_find.setShortcut(QKeySequence.Find)
|
|
265
272
|
act_find.triggered.connect(self.findBar.show_bar)
|
|
@@ -520,6 +527,12 @@ class MainWindow(QMainWindow):
|
|
|
520
527
|
|
|
521
528
|
self.tab_widget.removeTab(index)
|
|
522
529
|
|
|
530
|
+
def _close_current_tab(self):
|
|
531
|
+
"""Close the currently active tab via shortcuts (Ctrl+W)."""
|
|
532
|
+
idx = self.tab_widget.currentIndex()
|
|
533
|
+
if idx >= 0:
|
|
534
|
+
self._close_tab(idx)
|
|
535
|
+
|
|
523
536
|
def _on_tab_changed(self, index: int):
|
|
524
537
|
"""Handle tab change - reconnect toolbar and sync UI."""
|
|
525
538
|
if index < 0:
|
|
@@ -406,9 +406,14 @@ class MarkdownEditor(QTextEdit):
|
|
|
406
406
|
# Append the new marker
|
|
407
407
|
new_line = f"{new_line} ⏰ {time_str}"
|
|
408
408
|
|
|
409
|
-
|
|
409
|
+
# --- : only replace the block's text, not its newline ---
|
|
410
|
+
block_start = block.position()
|
|
411
|
+
block_end = block_start + len(line)
|
|
412
|
+
|
|
413
|
+
bc = QTextCursor(self.document())
|
|
410
414
|
bc.beginEditBlock()
|
|
411
|
-
bc.
|
|
415
|
+
bc.setPosition(block_start)
|
|
416
|
+
bc.setPosition(block_end, QTextCursor.KeepAnchor)
|
|
412
417
|
bc.insertText(new_line)
|
|
413
418
|
bc.endEditBlock()
|
|
414
419
|
|
|
@@ -426,6 +431,35 @@ class MarkdownEditor(QTextEdit):
|
|
|
426
431
|
"""Public wrapper used by MainWindow for reminders."""
|
|
427
432
|
return self._get_current_line()
|
|
428
433
|
|
|
434
|
+
def _list_prefix_length_for_block(self, block) -> int:
|
|
435
|
+
"""Return the length (in chars) of the visual list prefix for the given
|
|
436
|
+
block (including leading indentation), or 0 if it's not a list item.
|
|
437
|
+
"""
|
|
438
|
+
line = block.text()
|
|
439
|
+
stripped = line.lstrip()
|
|
440
|
+
leading_spaces = len(line) - len(stripped)
|
|
441
|
+
|
|
442
|
+
# Checkbox (Unicode display)
|
|
443
|
+
if stripped.startswith(
|
|
444
|
+
f"{self._CHECK_UNCHECKED_DISPLAY} "
|
|
445
|
+
) or stripped.startswith(f"{self._CHECK_CHECKED_DISPLAY} "):
|
|
446
|
+
return leading_spaces + 2 # icon + space
|
|
447
|
+
|
|
448
|
+
# Unicode bullet
|
|
449
|
+
if stripped.startswith(f"{self._BULLET_DISPLAY} "):
|
|
450
|
+
return leading_spaces + 2 # bullet + space
|
|
451
|
+
|
|
452
|
+
# Markdown bullet list (-, *, +)
|
|
453
|
+
if re.match(r"^[-*+]\s", stripped):
|
|
454
|
+
return leading_spaces + 2 # marker + space
|
|
455
|
+
|
|
456
|
+
# Numbered list: e.g. "1. "
|
|
457
|
+
m = re.match(r"^(\d+\.\s)", stripped)
|
|
458
|
+
if m:
|
|
459
|
+
return leading_spaces + leading_spaces + (len(m.group(1)) - leading_spaces)
|
|
460
|
+
|
|
461
|
+
return 0
|
|
462
|
+
|
|
429
463
|
def _detect_list_type(self, line: str) -> tuple[str | None, str]:
|
|
430
464
|
"""
|
|
431
465
|
Detect if line is a list item. Returns (list_type, prefix).
|
|
@@ -559,48 +593,102 @@ class MarkdownEditor(QTextEdit):
|
|
|
559
593
|
self._update_code_block_row_backgrounds()
|
|
560
594
|
return
|
|
561
595
|
|
|
562
|
-
# Handle
|
|
596
|
+
# Handle Backspace on empty list items so the marker itself can be deleted
|
|
597
|
+
if event.key() == Qt.Key.Key_Backspace:
|
|
598
|
+
cursor = self.textCursor()
|
|
599
|
+
# Let Backspace behave normally when deleting a selection.
|
|
600
|
+
if not cursor.hasSelection():
|
|
601
|
+
block = cursor.block()
|
|
602
|
+
prefix_len = self._list_prefix_length_for_block(block)
|
|
603
|
+
|
|
604
|
+
if prefix_len > 0:
|
|
605
|
+
block_start = block.position()
|
|
606
|
+
line = block.text()
|
|
607
|
+
pos_in_block = cursor.position() - block_start
|
|
608
|
+
after_text = line[prefix_len:]
|
|
609
|
+
|
|
610
|
+
# If there is no real content after the marker, treat Backspace
|
|
611
|
+
# as "remove the list marker".
|
|
612
|
+
if after_text.strip() == "" and pos_in_block >= prefix_len:
|
|
613
|
+
cursor.beginEditBlock()
|
|
614
|
+
cursor.setPosition(block_start)
|
|
615
|
+
cursor.setPosition(
|
|
616
|
+
block_start + prefix_len, QTextCursor.KeepAnchor
|
|
617
|
+
)
|
|
618
|
+
cursor.removeSelectedText()
|
|
619
|
+
cursor.endEditBlock()
|
|
620
|
+
self.setTextCursor(cursor)
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
# Handle Home and Left arrow keys to keep the caret to the *right*
|
|
624
|
+
# of list prefixes (checkboxes / bullets / numbers).
|
|
563
625
|
if event.key() in (Qt.Key.Key_Home, Qt.Key.Key_Left):
|
|
626
|
+
# Let Ctrl+Home / Ctrl+Left keep their usual meaning (start of
|
|
627
|
+
# document / word-left) – we don't interfere with those.
|
|
628
|
+
if event.modifiers() & Qt.ControlModifier:
|
|
629
|
+
pass
|
|
630
|
+
else:
|
|
631
|
+
cursor = self.textCursor()
|
|
632
|
+
block = cursor.block()
|
|
633
|
+
prefix_len = self._list_prefix_length_for_block(block)
|
|
634
|
+
|
|
635
|
+
if prefix_len > 0:
|
|
636
|
+
block_start = block.position()
|
|
637
|
+
pos_in_block = cursor.position() - block_start
|
|
638
|
+
target = block_start + prefix_len
|
|
639
|
+
|
|
640
|
+
if event.key() == Qt.Key.Key_Home:
|
|
641
|
+
# Home should jump to just after the prefix; with Shift
|
|
642
|
+
# it should *select* back to that position.
|
|
643
|
+
if event.modifiers() & Qt.ShiftModifier:
|
|
644
|
+
cursor.setPosition(target, QTextCursor.KeepAnchor)
|
|
645
|
+
else:
|
|
646
|
+
cursor.setPosition(target)
|
|
647
|
+
self.setTextCursor(cursor)
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
# Left arrow: don't allow the caret to move into the prefix
|
|
651
|
+
# region; snap it to just after the marker instead.
|
|
652
|
+
if event.key() == Qt.Key.Key_Left and pos_in_block <= prefix_len:
|
|
653
|
+
if event.modifiers() & Qt.ShiftModifier:
|
|
654
|
+
cursor.setPosition(target, QTextCursor.KeepAnchor)
|
|
655
|
+
else:
|
|
656
|
+
cursor.setPosition(target)
|
|
657
|
+
self.setTextCursor(cursor)
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
# After moving vertically, make sure we don't land *inside* a list
|
|
661
|
+
# prefix. We let QTextEdit perform the move first and then adjust.
|
|
662
|
+
if event.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not (
|
|
663
|
+
event.modifiers() & Qt.ControlModifier
|
|
664
|
+
):
|
|
665
|
+
super().keyPressEvent(event)
|
|
666
|
+
|
|
564
667
|
cursor = self.textCursor()
|
|
565
668
|
block = cursor.block()
|
|
566
|
-
line = block.text()
|
|
567
|
-
pos_in_block = cursor.position() - block.position()
|
|
568
|
-
|
|
569
|
-
# Detect list prefix length
|
|
570
|
-
prefix_len = 0
|
|
571
|
-
stripped = line.lstrip()
|
|
572
|
-
leading_spaces = len(line) - len(stripped)
|
|
573
|
-
|
|
574
|
-
# Check for checkbox (Unicode display format)
|
|
575
|
-
if stripped.startswith(
|
|
576
|
-
f"{self._CHECK_UNCHECKED_DISPLAY} "
|
|
577
|
-
) or stripped.startswith(f"{self._CHECK_CHECKED_DISPLAY} "):
|
|
578
|
-
prefix_len = leading_spaces + 2 # icon + space
|
|
579
|
-
# Check for Unicode bullet
|
|
580
|
-
elif stripped.startswith(f"{self._BULLET_DISPLAY} "):
|
|
581
|
-
prefix_len = leading_spaces + 2 # bullet + space
|
|
582
|
-
# Check for markdown bullet list (-, *, +)
|
|
583
|
-
elif re.match(r"^[-*+]\s", stripped):
|
|
584
|
-
prefix_len = leading_spaces + 2 # marker + space
|
|
585
|
-
# Check for numbered list
|
|
586
|
-
elif re.match(r"^\d+\.\s", stripped):
|
|
587
|
-
match = re.match(r"^(\d+\.\s)", stripped)
|
|
588
|
-
if match:
|
|
589
|
-
prefix_len = leading_spaces + len(match.group(1))
|
|
590
669
|
|
|
670
|
+
# Don't interfere with code blocks (they can contain literal
|
|
671
|
+
# markdown-looking text).
|
|
672
|
+
if self._is_inside_code_block(block):
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
prefix_len = self._list_prefix_length_for_block(block)
|
|
591
676
|
if prefix_len > 0:
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
677
|
+
block_start = block.position()
|
|
678
|
+
pos_in_block = cursor.position() - block_start
|
|
679
|
+
if pos_in_block < prefix_len:
|
|
680
|
+
target = block_start + prefix_len
|
|
681
|
+
if event.modifiers() & Qt.ShiftModifier:
|
|
682
|
+
# Preserve the current anchor while snapping the visual
|
|
683
|
+
# caret to just after the marker.
|
|
684
|
+
anchor = cursor.anchor()
|
|
685
|
+
cursor.setPosition(anchor)
|
|
686
|
+
cursor.setPosition(target, QTextCursor.KeepAnchor)
|
|
687
|
+
else:
|
|
688
|
+
cursor.setPosition(target)
|
|
595
689
|
self.setTextCursor(cursor)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
# Prevent moving left of the list marker
|
|
599
|
-
if pos_in_block > prefix_len:
|
|
600
|
-
# Allow normal left movement if we're past the prefix
|
|
601
|
-
super().keyPressEvent(event)
|
|
602
|
-
# Otherwise block the movement
|
|
603
|
-
return
|
|
690
|
+
|
|
691
|
+
return
|
|
604
692
|
|
|
605
693
|
# Handle Enter key for smart list continuation AND code blocks
|
|
606
694
|
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
|
@@ -665,6 +753,19 @@ class MarkdownEditor(QTextEdit):
|
|
|
665
753
|
super().keyPressEvent(event)
|
|
666
754
|
return
|
|
667
755
|
|
|
756
|
+
# Auto-insert an extra blank line after headings (#, ##, ###)
|
|
757
|
+
# when pressing Enter at the end of the line.
|
|
758
|
+
if re.match(r"^#{1,3}\s+", stripped) and pos_in_block >= len(line_text):
|
|
759
|
+
cursor.beginEditBlock()
|
|
760
|
+
# First blank line: visual separator between heading and body
|
|
761
|
+
cursor.insertBlock()
|
|
762
|
+
# Second blank line: where body text will start (caret ends here)
|
|
763
|
+
cursor.insertBlock()
|
|
764
|
+
cursor.endEditBlock()
|
|
765
|
+
|
|
766
|
+
self.setTextCursor(cursor)
|
|
767
|
+
return
|
|
768
|
+
|
|
668
769
|
# Check for list continuation
|
|
669
770
|
list_type, prefix = self._detect_list_type(current_line)
|
|
670
771
|
|
|
@@ -52,21 +52,21 @@ class TagBrowserDialog(QDialog):
|
|
|
52
52
|
# Tag management buttons
|
|
53
53
|
btn_row = QHBoxLayout()
|
|
54
54
|
|
|
55
|
-
self.add_tag_btn = QPushButton(strings._("add_a_tag"))
|
|
55
|
+
self.add_tag_btn = QPushButton("&" + strings._("add_a_tag"))
|
|
56
56
|
self.add_tag_btn.clicked.connect(self._add_a_tag)
|
|
57
57
|
btn_row.addWidget(self.add_tag_btn)
|
|
58
58
|
|
|
59
|
-
self.edit_name_btn = QPushButton(strings._("edit_tag_name"))
|
|
59
|
+
self.edit_name_btn = QPushButton("&" + strings._("edit_tag_name"))
|
|
60
60
|
self.edit_name_btn.clicked.connect(self._edit_tag_name)
|
|
61
61
|
self.edit_name_btn.setEnabled(False)
|
|
62
62
|
btn_row.addWidget(self.edit_name_btn)
|
|
63
63
|
|
|
64
|
-
self.change_color_btn = QPushButton(strings._("change_color"))
|
|
64
|
+
self.change_color_btn = QPushButton("&" + strings._("change_color"))
|
|
65
65
|
self.change_color_btn.clicked.connect(self._change_tag_color)
|
|
66
66
|
self.change_color_btn.setEnabled(False)
|
|
67
67
|
btn_row.addWidget(self.change_color_btn)
|
|
68
68
|
|
|
69
|
-
self.delete_btn = QPushButton(strings._("delete_tag"))
|
|
69
|
+
self.delete_btn = QPushButton("&" + strings._("delete_tag"))
|
|
70
70
|
self.delete_btn.clicked.connect(self._delete_tag)
|
|
71
71
|
self.delete_btn.setEnabled(False)
|
|
72
72
|
btn_row.addWidget(self.delete_btn)
|
|
@@ -181,6 +181,9 @@ class TimeLogDialog(QDialog):
|
|
|
181
181
|
self._db = db
|
|
182
182
|
self._date_iso = date_iso
|
|
183
183
|
self._current_entry_id: Optional[int] = None
|
|
184
|
+
# Guard flag used when repopulating the table so we don’t treat
|
|
185
|
+
# programmatic item changes as user edits.
|
|
186
|
+
self._reloading_entries: bool = False
|
|
184
187
|
|
|
185
188
|
self.setWindowTitle(strings._("time_log_for").format(date=date_iso))
|
|
186
189
|
self.resize(900, 600)
|
|
@@ -228,14 +231,14 @@ class TimeLogDialog(QDialog):
|
|
|
228
231
|
|
|
229
232
|
# --- Buttons for entry
|
|
230
233
|
btn_row = QHBoxLayout()
|
|
231
|
-
self.add_update_btn = QPushButton(strings._("add_time_entry"))
|
|
234
|
+
self.add_update_btn = QPushButton("&" + strings._("add_time_entry"))
|
|
232
235
|
self.add_update_btn.clicked.connect(self._on_add_or_update)
|
|
233
236
|
|
|
234
|
-
self.delete_btn = QPushButton(strings._("delete_time_entry"))
|
|
237
|
+
self.delete_btn = QPushButton("&" + strings._("delete_time_entry"))
|
|
235
238
|
self.delete_btn.clicked.connect(self._on_delete_entry)
|
|
236
239
|
self.delete_btn.setEnabled(False)
|
|
237
240
|
|
|
238
|
-
self.report_btn = QPushButton(strings._("run_report"))
|
|
241
|
+
self.report_btn = QPushButton("&" + strings._("run_report"))
|
|
239
242
|
self.report_btn.clicked.connect(self._on_run_report)
|
|
240
243
|
|
|
241
244
|
btn_row.addStretch(1)
|
|
@@ -264,12 +267,14 @@ class TimeLogDialog(QDialog):
|
|
|
264
267
|
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
265
268
|
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
266
269
|
self.table.itemSelectionChanged.connect(self._on_row_selected)
|
|
270
|
+
# When a cell is edited inline, commit the change back to the DB.
|
|
271
|
+
self.table.itemChanged.connect(self._on_table_item_changed)
|
|
267
272
|
root.addWidget(self.table, 1)
|
|
268
273
|
|
|
269
274
|
# --- Close button
|
|
270
275
|
close_row = QHBoxLayout()
|
|
271
276
|
close_row.addStretch(1)
|
|
272
|
-
close_btn = QPushButton(strings._("close"))
|
|
277
|
+
close_btn = QPushButton("&" + strings._("close"))
|
|
273
278
|
close_btn.clicked.connect(self.accept)
|
|
274
279
|
close_row.addWidget(close_btn)
|
|
275
280
|
root.addLayout(close_row)
|
|
@@ -293,32 +298,42 @@ class TimeLogDialog(QDialog):
|
|
|
293
298
|
self.activity_edit.setCompleter(completer)
|
|
294
299
|
|
|
295
300
|
def _reload_entries(self) -> None:
|
|
296
|
-
|
|
297
|
-
self.table.setRowCount(len(rows))
|
|
298
|
-
for row_idx, r in enumerate(rows):
|
|
299
|
-
entry_id = r[0]
|
|
300
|
-
project_name = r[3]
|
|
301
|
-
activity_name = r[5]
|
|
302
|
-
note = r[7] or ""
|
|
303
|
-
minutes = r[6]
|
|
304
|
-
hours = minutes / 60.0
|
|
305
|
-
|
|
306
|
-
item_proj = QTableWidgetItem(project_name)
|
|
307
|
-
item_act = QTableWidgetItem(activity_name)
|
|
308
|
-
item_note = QTableWidgetItem(note)
|
|
309
|
-
item_hours = QTableWidgetItem(f"{hours:.2f}")
|
|
301
|
+
"""Reload the table from the database.
|
|
310
302
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
self.
|
|
303
|
+
While we are repopulating the QTableWidget we temporarily disable the
|
|
304
|
+
itemChanged handler so that programmatic changes do not get written
|
|
305
|
+
back to the database.
|
|
306
|
+
"""
|
|
307
|
+
self._reloading_entries = True
|
|
308
|
+
try:
|
|
309
|
+
rows = self._db.time_log_for_date(self._date_iso)
|
|
310
|
+
self.table.setRowCount(len(rows))
|
|
311
|
+
for row_idx, r in enumerate(rows):
|
|
312
|
+
entry_id = r[0]
|
|
313
|
+
project_name = r[3]
|
|
314
|
+
activity_name = r[5]
|
|
315
|
+
note = r[7] or ""
|
|
316
|
+
minutes = r[6]
|
|
317
|
+
hours = minutes / 60.0
|
|
318
|
+
|
|
319
|
+
item_proj = QTableWidgetItem(project_name)
|
|
320
|
+
item_act = QTableWidgetItem(activity_name)
|
|
321
|
+
item_note = QTableWidgetItem(note)
|
|
322
|
+
item_hours = QTableWidgetItem(f"{hours:.2f}")
|
|
323
|
+
|
|
324
|
+
# store the entry id on the first column
|
|
325
|
+
item_proj.setData(Qt.ItemDataRole.UserRole, entry_id)
|
|
326
|
+
|
|
327
|
+
self.table.setItem(row_idx, 0, item_proj)
|
|
328
|
+
self.table.setItem(row_idx, 1, item_act)
|
|
329
|
+
self.table.setItem(row_idx, 2, item_note)
|
|
330
|
+
self.table.setItem(row_idx, 3, item_hours)
|
|
331
|
+
finally:
|
|
332
|
+
self._reloading_entries = False
|
|
318
333
|
|
|
319
334
|
self._current_entry_id = None
|
|
320
335
|
self.delete_btn.setEnabled(False)
|
|
321
|
-
self.add_update_btn.setText(strings._("add_time_entry"))
|
|
336
|
+
self.add_update_btn.setText("&" + strings._("add_time_entry"))
|
|
322
337
|
|
|
323
338
|
# ----- Actions -----------------------------------------------------
|
|
324
339
|
|
|
@@ -368,7 +383,6 @@ class TimeLogDialog(QDialog):
|
|
|
368
383
|
self._current_entry_id, proj_id, activity_id, minutes, note
|
|
369
384
|
)
|
|
370
385
|
|
|
371
|
-
self.note.setText("")
|
|
372
386
|
self._reload_entries()
|
|
373
387
|
|
|
374
388
|
def _on_row_selected(self) -> None:
|
|
@@ -376,7 +390,7 @@ class TimeLogDialog(QDialog):
|
|
|
376
390
|
if not items:
|
|
377
391
|
self._current_entry_id = None
|
|
378
392
|
self.delete_btn.setEnabled(False)
|
|
379
|
-
self.add_update_btn.setText(strings._("add_time_entry"))
|
|
393
|
+
self.add_update_btn.setText("&" + strings._("add_time_entry"))
|
|
380
394
|
return
|
|
381
395
|
|
|
382
396
|
row = items[0].row()
|
|
@@ -388,7 +402,7 @@ class TimeLogDialog(QDialog):
|
|
|
388
402
|
|
|
389
403
|
self._current_entry_id = int(entry_id)
|
|
390
404
|
self.delete_btn.setEnabled(True)
|
|
391
|
-
self.add_update_btn.setText(strings._("update_time_entry"))
|
|
405
|
+
self.add_update_btn.setText("&" + strings._("update_time_entry"))
|
|
392
406
|
|
|
393
407
|
# push values into the editors
|
|
394
408
|
proj_name = proj_item.text()
|
|
@@ -405,6 +419,81 @@ class TimeLogDialog(QDialog):
|
|
|
405
419
|
self.note.setText(note)
|
|
406
420
|
self.hours_spin.setValue(hours)
|
|
407
421
|
|
|
422
|
+
def _on_table_item_changed(self, item: QTableWidgetItem) -> None:
|
|
423
|
+
"""Commit inline edits in the table back to the database.
|
|
424
|
+
|
|
425
|
+
Editing a cell should behave like selecting that row and pressing
|
|
426
|
+
the Add/Update button, so we reuse the same validation and DB logic.
|
|
427
|
+
"""
|
|
428
|
+
if self._reloading_entries:
|
|
429
|
+
# Ignore changes that come from _reload_entries().
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
if item is None:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
row = item.row()
|
|
436
|
+
|
|
437
|
+
proj_item = self.table.item(row, 0)
|
|
438
|
+
act_item = self.table.item(row, 1)
|
|
439
|
+
note_item = self.table.item(row, 2)
|
|
440
|
+
hours_item = self.table.item(row, 3)
|
|
441
|
+
|
|
442
|
+
if proj_item is None or act_item is None or hours_item is None:
|
|
443
|
+
# Incomplete row – nothing to do.
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
# Recover the entry id from the hidden UserRole on the project cell
|
|
447
|
+
entry_id = proj_item.data(Qt.ItemDataRole.UserRole)
|
|
448
|
+
self._current_entry_id = int(entry_id) if entry_id is not None else None
|
|
449
|
+
|
|
450
|
+
# Push values into the editors (similar to _on_row_selected).
|
|
451
|
+
proj_name = proj_item.text()
|
|
452
|
+
act_name = act_item.text()
|
|
453
|
+
note_text = note_item.text() if note_item is not None else ""
|
|
454
|
+
hours_text = hours_item.text()
|
|
455
|
+
|
|
456
|
+
# Set project combo by name, creating a project on the fly if needed.
|
|
457
|
+
idx = self.project_combo.findText(proj_name, Qt.MatchFixedString)
|
|
458
|
+
if idx < 0 and proj_name:
|
|
459
|
+
# Allow creating a new project directly from the table.
|
|
460
|
+
proj_id = self._db.add_project(proj_name)
|
|
461
|
+
self._reload_projects()
|
|
462
|
+
idx = self.project_combo.findData(proj_id)
|
|
463
|
+
if idx >= 0:
|
|
464
|
+
self.project_combo.setCurrentIndex(idx)
|
|
465
|
+
else:
|
|
466
|
+
self.project_combo.setCurrentIndex(-1)
|
|
467
|
+
|
|
468
|
+
self.activity_edit.setText(act_name)
|
|
469
|
+
self.note.setText(note_text)
|
|
470
|
+
|
|
471
|
+
# Parse hours; if invalid, show the same style of warning as elsewhere.
|
|
472
|
+
try:
|
|
473
|
+
hours = float(hours_text)
|
|
474
|
+
except ValueError:
|
|
475
|
+
QMessageBox.warning(
|
|
476
|
+
self,
|
|
477
|
+
strings._("invalid_time_title"),
|
|
478
|
+
strings._("invalid_time_message"),
|
|
479
|
+
)
|
|
480
|
+
# Reset table back to the last known-good state.
|
|
481
|
+
self._reload_entries()
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
self.hours_spin.setValue(hours)
|
|
485
|
+
|
|
486
|
+
# Mirror button state to reflect whether we're updating or adding.
|
|
487
|
+
if self._current_entry_id is None:
|
|
488
|
+
self.delete_btn.setEnabled(False)
|
|
489
|
+
self.add_update_btn.setText(strings._("add_time_entry"))
|
|
490
|
+
else:
|
|
491
|
+
self.delete_btn.setEnabled(True)
|
|
492
|
+
self.add_update_btn.setText(strings._("update_time_entry"))
|
|
493
|
+
|
|
494
|
+
# Finally, reuse the existing validation + DB logic.
|
|
495
|
+
self._on_add_or_update()
|
|
496
|
+
|
|
408
497
|
def _on_delete_entry(self) -> None:
|
|
409
498
|
if self._current_entry_id is None:
|
|
410
499
|
return
|
|
@@ -453,15 +542,15 @@ class TimeCodeManagerDialog(QDialog):
|
|
|
453
542
|
proj_layout.addWidget(self.project_list, 1)
|
|
454
543
|
|
|
455
544
|
proj_btn_row = QHBoxLayout()
|
|
456
|
-
self.proj_add_btn = QPushButton(strings._("add_project"))
|
|
457
|
-
self.proj_rename_btn = QPushButton(strings._("rename_project"))
|
|
458
|
-
self.proj_delete_btn = QPushButton(strings._("delete_project"))
|
|
545
|
+
self.proj_add_btn = QPushButton("&" + strings._("add_project"))
|
|
546
|
+
self.proj_rename_btn = QPushButton("&" + strings._("rename_project"))
|
|
547
|
+
self.proj_delete_btn = QPushButton("&" + strings._("delete_project"))
|
|
459
548
|
proj_btn_row.addWidget(self.proj_add_btn)
|
|
460
549
|
proj_btn_row.addWidget(self.proj_rename_btn)
|
|
461
550
|
proj_btn_row.addWidget(self.proj_delete_btn)
|
|
462
551
|
proj_layout.addLayout(proj_btn_row)
|
|
463
552
|
|
|
464
|
-
self.tabs.addTab(proj_tab, strings._("projects"))
|
|
553
|
+
self.tabs.addTab(proj_tab, "&" + strings._("projects"))
|
|
465
554
|
|
|
466
555
|
# Activities tab
|
|
467
556
|
act_tab = QWidget()
|
|
@@ -470,9 +559,9 @@ class TimeCodeManagerDialog(QDialog):
|
|
|
470
559
|
act_layout.addWidget(self.activity_list, 1)
|
|
471
560
|
|
|
472
561
|
act_btn_row = QHBoxLayout()
|
|
473
|
-
self.act_add_btn = QPushButton(strings._("add_activity"))
|
|
474
|
-
self.act_rename_btn = QPushButton(strings._("rename_activity"))
|
|
475
|
-
self.act_delete_btn = QPushButton(strings._("delete_activity"))
|
|
562
|
+
self.act_add_btn = QPushButton("&" + strings._("add_activity"))
|
|
563
|
+
self.act_rename_btn = QPushButton("&" + strings._("rename_activity"))
|
|
564
|
+
self.act_delete_btn = QPushButton("&" + strings._("delete_activity"))
|
|
476
565
|
act_btn_row.addWidget(self.act_add_btn)
|
|
477
566
|
act_btn_row.addWidget(self.act_rename_btn)
|
|
478
567
|
act_btn_row.addWidget(self.act_delete_btn)
|
|
@@ -483,7 +572,7 @@ class TimeCodeManagerDialog(QDialog):
|
|
|
483
572
|
# Close
|
|
484
573
|
close_row = QHBoxLayout()
|
|
485
574
|
close_row.addStretch(1)
|
|
486
|
-
close_btn = QPushButton(strings._("close"))
|
|
575
|
+
close_btn = QPushButton("&" + strings._("close"))
|
|
487
576
|
close_btn.clicked.connect(self.accept)
|
|
488
577
|
close_row.addWidget(close_btn)
|
|
489
578
|
root.addLayout(close_row)
|
|
@@ -827,7 +916,7 @@ class TimeReportDialog(QDialog):
|
|
|
827
916
|
# Close
|
|
828
917
|
close_row = QHBoxLayout()
|
|
829
918
|
close_row.addStretch(1)
|
|
830
|
-
close_btn = QPushButton(strings._("close"))
|
|
919
|
+
close_btn = QPushButton("&" + strings._("close"))
|
|
831
920
|
close_btn.clicked.connect(self.accept)
|
|
832
921
|
close_row.addWidget(close_btn)
|
|
833
922
|
root.addLayout(close_row)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|