Add ability to set alarm reminders
This commit is contained in:
parent
63cf561bfe
commit
83f25405db
8 changed files with 241 additions and 7 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue