diff --git a/CHANGELOG.md b/CHANGELOG.md index f668299..5109618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Add Tag relationship visualisation tool * Improve size of checkboxes * Convert bullet - to actual unicode bullets + * Add alarm option to set reminders # 0.3.2 diff --git a/README.md b/README.md index 2df84cf..b241452 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,16 @@ report from within the app. ## Screenshots +### General view ![Screenshot of Bouquin](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/screenshot.png) + +### History panes ![Screenshot of Bouquin History Preview pane](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/history_preview.png) ![Screenshot of Bouquin History Diff pane](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/history_diff.png) +### Tag relationship visualiser +![Screenshot of Tag Relationship Visualiser](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/bouquin_tag_relationship_graph.png) + ## Features * Data is encrypted at rest @@ -41,6 +47,7 @@ report from within the app. * Automatically generate checkboxes when typing 'TODO' * It is possible to automatically move unchecked checkboxes from yesterday to today, on startup * English, French and Italian locales provided + * Ability to set reminder alarms in the app against the current line of text on today's date ## How to install diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json index 19cf85d..69bb0b4 100644 --- a/bouquin/locales/en.json +++ b/bouquin/locales/en.json @@ -155,5 +155,12 @@ "bug_report_empty": "Please enter some details about the bug before sending.", "bug_report_send_failed": "Could not send bug report.", "bug_report_sent_ok": "Bug report sent. Thank you!", - "send": "Send" + "send": "Send", + "set_reminder": "Set reminder prompt", + "set_reminder_prompt": "Enter a time", + "reminder_no_text_fallback": "You scheduled a reminder to alert you now!", + "invalid_time_title": "Invalid time", + "invalid_time_message": "Please enter a time in the format HH:MM", + "dismiss": "Dismiss", + "toolbar_alarm": "Set reminder alarm" } diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 59581fb..0763f29 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -16,6 +16,8 @@ from PySide6.QtCore import ( QUrl, QEvent, QSignalBlocker, + QDateTime, + QTime, ) from PySide6.QtGui import ( QAction, @@ -43,6 +45,10 @@ from PySide6.QtWidgets import ( QTabWidget, QVBoxLayout, QWidget, + QInputDialog, + QLabel, + QPushButton, + QApplication, ) from .db import DBManager @@ -164,8 +170,6 @@ class MainWindow(QMainWindow): self._locked = False # reset idle timer on any key press anywhere in the app - from PySide6.QtWidgets import QApplication - QApplication.instance().installEventFilter(self) # Focus on the editor @@ -292,7 +296,9 @@ class MainWindow(QMainWindow): self._save_timer = QTimer(self) self._save_timer.setSingleShot(True) self._save_timer.timeout.connect(self._save_current) - # Note: textChanged will be connected per-editor in _create_new_tab + + # Reminders / alarms + self._reminder_timers: list[QTimer] = [] # First load + mark dates in calendar with content if not self._load_yesterday_todos(): @@ -310,6 +316,9 @@ class MainWindow(QMainWindow): # apply once on startup so links / calendar colors are set immediately self._retheme_overrides() + # Build any alarms for *today* from stored markdown + self._rebuild_reminders_for_today() + @property def editor(self) -> MarkdownEditor | None: """Get the currently active editor.""" @@ -966,6 +975,7 @@ class MainWindow(QMainWindow): self._tb_bullets = lambda: self._call_editor("toggle_bullets") self._tb_numbers = lambda: self._call_editor("toggle_numbers") self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes") + self._tb_alarm = self._on_alarm_requested tb.boldRequested.connect(self._tb_bold) tb.italicRequested.connect(self._tb_italic) @@ -975,9 +985,9 @@ class MainWindow(QMainWindow): tb.bulletsRequested.connect(self._tb_bullets) tb.numbersRequested.connect(self._tb_numbers) tb.checkboxesRequested.connect(self._tb_checkboxes) - - tb.historyRequested.connect(self._open_history) + tb.alarmRequested.connect(self._tb_alarm) tb.insertImageRequested.connect(self._on_insert_image) + tb.historyRequested.connect(self._open_history) self._toolbar_bound = True @@ -1023,6 +1033,177 @@ class MainWindow(QMainWindow): self.toolBar.actBullets.setChecked(bool(bullets_on)) self.toolBar.actNumbers.setChecked(bool(numbers_on)) + # ----------- Alarms handler ------------# + def _on_alarm_requested(self): + """Create a one-shot reminder based on the current line in the editor.""" + editor = getattr(self, "editor", None) + if editor is None: + return + + # Use the current line in the markdown editor as the reminder text + try: + line_text = editor.get_current_line_text().strip() + except AttributeError: + c = editor.textCursor() + line_text = c.block().text().strip() + + # Ask user for a time today in HH:MM format + time_str, ok = QInputDialog.getText( + self, + strings._("set_reminder"), + strings._("set_reminder_prompt") + " (HH:MM)", + ) + if not ok or not time_str.strip(): + return + + try: + hour, minute = map(int, time_str.strip().split(":", 1)) + except ValueError: + QMessageBox.warning( + self, + strings._("invalid_time_title"), + strings._("invalid_time_message"), + ) + return + + now = QDateTime.currentDateTime() + target = QDateTime(now.date(), QTime(hour, minute)) + + t = QTime(hour, minute) + if not t.isValid(): + QMessageBox.warning( + self, + strings._("invalid_time_title"), + strings._("invalid_time_message"), + ) + return + + # Normalise to HH:MM + time_str = f"{t.hour():02d}:{t.minute():02d}" + + # Insert / update ⏰ in the editor text + if hasattr(editor, "insert_alarm_marker"): + editor.insert_alarm_marker(time_str) + + # Rebuild timers, but only if this page is for "today" + self._rebuild_reminders_for_today() + + def _show_flashing_reminder(self, text: str): + """ + Show a small flashing dialog and request attention from the OS. + Called by reminder timers. + """ + # Ask OS to flash / bounce our app in the dock/taskbar + QApplication.alert(self, 0) + + # Try to bring the window to the front + self.showNormal() + self.raise_() + self.activateWindow() + + # Simple dialog with a flashing background to reinforce the alert + dlg = QDialog(self) + dlg.setWindowTitle(strings._("reminder")) + dlg.setModal(True) + + layout = QVBoxLayout(dlg) + label = QLabel(text) + label.setWordWrap(True) + layout.addWidget(label) + + btn = QPushButton(strings._("dismiss")) + btn.clicked.connect(dlg.accept) + layout.addWidget(btn) + + flash_timer = QTimer(dlg) + flash_state = {"on": False} + + def toggle(): + flash_state["on"] = not flash_state["on"] + if flash_state["on"]: + dlg.setStyleSheet("background-color: #3B3B3B;") + else: + dlg.setStyleSheet("") + + flash_timer.timeout.connect(toggle) + flash_timer.start(500) # ms + + dlg.exec() + + flash_timer.stop() + + def _clear_reminder_timers(self): + """Stop and delete any existing reminder timers.""" + for t in self._reminder_timers: + try: + t.stop() + t.deleteLater() + except Exception: + pass + self._reminder_timers = [] + + def _rebuild_reminders_for_today(self): + """ + Scan the markdown for today's date and create QTimers + only for alarms on the *current day* (system date). + """ + # We only ever set timers for the real current date + today = QDate.currentDate() + today_iso = today.toString("yyyy-MM-dd") + + # Clear any previously scheduled "today" reminders + self._clear_reminder_timers() + + # Prefer live editor content if it is showing today's page + text = "" + if ( + hasattr(self, "editor") + and hasattr(self.editor, "current_date") + and self.editor.current_date == today + ): + text = self.editor.to_markdown() + else: + # Fallback to DB: still only today's date + text = self.db.get_entry(today_iso) if hasattr(self, "db") else "" + + if not text: + return + + now = QDateTime.currentDateTime() + + for line in text.splitlines(): + # Look for "⏰ HH:MM" anywhere in the line + m = re.search(r"⏰\s*(\d{1,2}):(\d{2})", line) + if not m: + continue + + hour = int(m.group(1)) + minute = int(m.group(2)) + + t = QTime(hour, minute) + if not t.isValid(): + continue + + target = QDateTime(today, t) + + # Skip alarms that are already in the past + if target <= now: + continue + + # The reminder text is the part before the symbol + reminder_text = line.split("⏰", 1)[0].strip() + if not reminder_text: + reminder_text = strings._("reminder_no_text_fallback") + + msecs = now.msecsTo(target) + timer = QTimer(self) + timer.setSingleShot(True) + timer.timeout.connect( + lambda txt=reminder_text: self._show_flashing_reminder(txt) + ) + timer.start(msecs) + self._reminder_timers.append(timer) + # ----------- History handler ------------# def _open_history(self): if hasattr(self.editor, "current_date"): diff --git a/bouquin/markdown_editor.py b/bouquin/markdown_editor.py index 9212229..5cd357c 100644 --- a/bouquin/markdown_editor.py +++ b/bouquin/markdown_editor.py @@ -391,12 +391,41 @@ class MarkdownEditor(QTextEdit): cursor.setPosition(match.end(), QTextCursor.MoveMode.KeepAnchor) cursor.insertImage(img_format) + def insert_alarm_marker(self, time_str: str) -> None: + """ + Append or replace an ⏰ HH:MM marker on the current line. + time_str is expected to be 'HH:MM'. + """ + cursor = self.textCursor() + block = cursor.block() + line = block.text() + + # Strip any existing ⏰ HH:MM at the end of the line + new_line = re.sub(r"\s*⏰\s*\d{1,2}:\d{2}\s*$", "", line).rstrip() + + # Append the new marker + new_line = f"{new_line} ⏰ {time_str}" + + bc = QTextCursor(block) + bc.beginEditBlock() + bc.select(QTextCursor.SelectionType.BlockUnderCursor) + bc.insertText(new_line) + bc.endEditBlock() + + # Move cursor to end of the edited line + cursor.setPosition(block.position() + len(new_line)) + self.setTextCursor(cursor) + def _get_current_line(self) -> str: """Get the text of the current line.""" cursor = self.textCursor() cursor.select(QTextCursor.SelectionType.LineUnderCursor) return cursor.selectedText() + def get_current_line_text(self) -> str: + """Public wrapper used by MainWindow for reminders.""" + return self._get_current_line() + def _detect_list_type(self, line: str) -> tuple[str | None, str]: """ Detect if line is a list item. Returns (list_type, prefix). diff --git a/bouquin/tag_graph_dialog.py b/bouquin/tag_graph_dialog.py index ef2d0cc..a4065cb 100644 --- a/bouquin/tag_graph_dialog.py +++ b/bouquin/tag_graph_dialog.py @@ -281,7 +281,7 @@ class TagGraphDialog(QDialog): # Update labels for i, label in enumerate(self._label_items): - label.setPos(float(pos[i, 0]), float(pos[i, 1]) + 0.30) + label.setPos(float(pos[i, 0]), float(pos[i, 1]) + 0.15) # Update halo positions to match nodes if self._halo_sizes and self._halo_brushes: diff --git a/bouquin/toolbar.py b/bouquin/toolbar.py index 89999b8..9764246 100644 --- a/bouquin/toolbar.py +++ b/bouquin/toolbar.py @@ -18,6 +18,7 @@ class ToolBar(QToolBar): checkboxesRequested = Signal() historyRequested = Signal() insertImageRequested = Signal() + alarmRequested = Signal() def __init__(self, parent=None): super().__init__(strings._("toolbar_format"), parent) @@ -85,6 +86,11 @@ class ToolBar(QToolBar): self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes")) self.actCheckboxes.triggered.connect(self.checkboxesRequested) + # Alarm / reminder + self.actAlarm = QAction("⏰", self) + self.actAlarm.setToolTip(strings._("toolbar_alarm")) + self.actAlarm.triggered.connect(self.alarmRequested) + # Images self.actInsertImg = QAction(strings._("images"), self) self.actInsertImg.setToolTip(strings._("insert_images")) @@ -129,6 +135,7 @@ class ToolBar(QToolBar): self.actBullets, self.actNumbers, self.actCheckboxes, + self.actAlarm, self.actInsertImg, self.actHistory, ] @@ -151,6 +158,8 @@ class ToolBar(QToolBar): # Lists self._style_letter_button(self.actBullets, "•") self._style_letter_button(self.actNumbers, "1.") + self._style_letter_button(self.actCheckboxes, "☐") + self._style_letter_button(self.actAlarm, "⏰") # History self._style_letter_button(self.actHistory, strings._("view_history")) diff --git a/screenshots/bouquin_tag_relationship_graph.png b/screenshots/bouquin_tag_relationship_graph.png new file mode 100644 index 0000000..083bf6a Binary files /dev/null and b/screenshots/bouquin_tag_relationship_graph.png differ