0.2.1 with tabs
This commit is contained in:
parent
fa23cf4da9
commit
f023224074
9 changed files with 651 additions and 60 deletions
|
|
@ -31,14 +31,19 @@ class FindBar(QWidget):
|
|||
shortcut_parent: QWidget | None = None,
|
||||
parent: QWidget | None = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.editor = editor
|
||||
|
||||
# UI
|
||||
super().__init__(parent)
|
||||
|
||||
# store how to get the current editor
|
||||
self._editor_getter = editor if callable(editor) else (lambda: editor)
|
||||
self.shortcut_parent = shortcut_parent
|
||||
|
||||
# UI (build ONCE)
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(6, 0, 6, 0)
|
||||
|
||||
layout.addWidget(QLabel("Find:"))
|
||||
|
||||
self.edit = QLineEdit(self)
|
||||
self.edit.setPlaceholderText("Type to search…")
|
||||
layout.addWidget(self.edit)
|
||||
|
|
@ -56,11 +61,15 @@ class FindBar(QWidget):
|
|||
|
||||
self.setVisible(False)
|
||||
|
||||
# Shortcut escape key to close findBar
|
||||
sp = shortcut_parent if shortcut_parent is not None else (parent or self)
|
||||
self._scEsc = QShortcut(Qt.Key_Escape, sp, activated=self._maybe_hide)
|
||||
# Shortcut (press Esc to hide bar)
|
||||
sp = (
|
||||
self.shortcut_parent
|
||||
if self.shortcut_parent is not None
|
||||
else (self.parent() or self)
|
||||
)
|
||||
QShortcut(Qt.Key_Escape, sp, activated=self._maybe_hide)
|
||||
|
||||
# Signals
|
||||
# Signals (connect ONCE)
|
||||
self.edit.returnPressed.connect(self.find_next)
|
||||
self.edit.textChanged.connect(self._update_highlight)
|
||||
self.case.toggled.connect(self._update_highlight)
|
||||
|
|
@ -68,10 +77,17 @@ class FindBar(QWidget):
|
|||
self.prevBtn.clicked.connect(self.find_prev)
|
||||
self.closeBtn.clicked.connect(self.hide_bar)
|
||||
|
||||
@property
|
||||
def editor(self) -> QTextEdit | None:
|
||||
"""Get the current editor (no side effects)."""
|
||||
return self._editor_getter()
|
||||
|
||||
# ----- Public API -----
|
||||
|
||||
def show_bar(self):
|
||||
"""Show the bar, seed with current selection if sensible, focus the line edit."""
|
||||
if not self.editor:
|
||||
return
|
||||
tc = self.editor.textCursor()
|
||||
sel = tc.selectedText().strip()
|
||||
if sel and "\u2029" not in sel: # ignore multi-paragraph selections
|
||||
|
|
@ -105,6 +121,8 @@ class FindBar(QWidget):
|
|||
return flags
|
||||
|
||||
def find_next(self):
|
||||
if not self.editor:
|
||||
return
|
||||
txt = self.edit.text()
|
||||
if not txt:
|
||||
return
|
||||
|
|
@ -130,6 +148,8 @@ class FindBar(QWidget):
|
|||
self._update_highlight()
|
||||
|
||||
def find_prev(self):
|
||||
if not self.editor:
|
||||
return
|
||||
txt = self.edit.text()
|
||||
if not txt:
|
||||
return
|
||||
|
|
@ -155,6 +175,8 @@ class FindBar(QWidget):
|
|||
self._update_highlight()
|
||||
|
||||
def _update_highlight(self):
|
||||
if not self.editor:
|
||||
return
|
||||
txt = self.edit.text()
|
||||
if not txt:
|
||||
self._clear_highlight()
|
||||
|
|
@ -183,4 +205,5 @@ class FindBar(QWidget):
|
|||
self.editor.setExtraSelections(selections)
|
||||
|
||||
def _clear_highlight(self):
|
||||
self.editor.setExtraSelections([])
|
||||
if self.editor:
|
||||
self.editor.setExtraSelections([])
|
||||
|
|
|
|||
|
|
@ -36,9 +36,12 @@ from PySide6.QtWidgets import (
|
|||
QDialog,
|
||||
QFileDialog,
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QSizePolicy,
|
||||
QSplitter,
|
||||
QTableView,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
|
@ -98,34 +101,37 @@ class MainWindow(QMainWindow):
|
|||
left_layout.addWidget(self.search)
|
||||
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
|
||||
|
||||
# This is the note-taking editor
|
||||
self.editor = MarkdownEditor(self.themes)
|
||||
# Create tab widget to hold multiple editors
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setTabsClosable(True)
|
||||
self.tab_widget.tabCloseRequested.connect(self._close_tab)
|
||||
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
||||
|
||||
# Toolbar for controlling styling
|
||||
self.toolBar = ToolBar()
|
||||
self.addToolBar(self.toolBar)
|
||||
# Wire toolbar intents to editor methods
|
||||
self.toolBar.boldRequested.connect(self.editor.apply_weight)
|
||||
self.toolBar.italicRequested.connect(self.editor.apply_italic)
|
||||
# Note: Markdown doesn't support underline, so we skip underlineRequested
|
||||
self.toolBar.strikeRequested.connect(self.editor.apply_strikethrough)
|
||||
self.toolBar.codeRequested.connect(self.editor.apply_code)
|
||||
self.toolBar.headingRequested.connect(self.editor.apply_heading)
|
||||
self.toolBar.bulletsRequested.connect(self.editor.toggle_bullets)
|
||||
self.toolBar.numbersRequested.connect(self.editor.toggle_numbers)
|
||||
self.toolBar.checkboxesRequested.connect(self.editor.toggle_checkboxes)
|
||||
# Note: Markdown doesn't natively support alignment, removing alignRequested
|
||||
self.toolBar.historyRequested.connect(self._open_history)
|
||||
self.toolBar.insertImageRequested.connect(self._on_insert_image)
|
||||
self._bind_toolbar()
|
||||
|
||||
self.editor.currentCharFormatChanged.connect(lambda _f: self._sync_toolbar())
|
||||
self.editor.cursorPositionChanged.connect(self._sync_toolbar)
|
||||
# Create the first editor tab
|
||||
self._create_new_tab()
|
||||
|
||||
split = QSplitter()
|
||||
split.addWidget(left_panel)
|
||||
split.addWidget(self.editor)
|
||||
split.addWidget(self.tab_widget)
|
||||
split.setStretchFactor(1, 1)
|
||||
|
||||
# Enable context menu on calendar for opening dates in new tabs
|
||||
self.calendar.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.calendar.customContextMenuRequested.connect(
|
||||
self._show_calendar_context_menu
|
||||
)
|
||||
|
||||
# Flag to prevent _on_date_changed when showing context menu
|
||||
self._showing_context_menu = False
|
||||
|
||||
# Install event filter to catch right-clicks before selectionChanged fires
|
||||
self.calendar.installEventFilter(self)
|
||||
|
||||
container = QWidget()
|
||||
lay = QVBoxLayout(container)
|
||||
lay.addWidget(split)
|
||||
|
|
@ -162,7 +168,10 @@ class MainWindow(QMainWindow):
|
|||
# Status bar for feedback
|
||||
self.statusBar().showMessage("Ready", 800)
|
||||
# Add findBar and add it to the statusBar
|
||||
self.findBar = FindBar(self.editor, shortcut_parent=self, parent=self)
|
||||
# FindBar will get the current editor dynamically via a callable
|
||||
self.findBar = FindBar(
|
||||
lambda: self.current_editor(), shortcut_parent=self, parent=self
|
||||
)
|
||||
self.statusBar().addPermanentWidget(self.findBar)
|
||||
# When the findBar closes, put the caret back in the editor
|
||||
self.findBar.closed.connect(self._focus_editor_now)
|
||||
|
|
@ -258,7 +267,7 @@ class MainWindow(QMainWindow):
|
|||
self._save_timer = QTimer(self)
|
||||
self._save_timer.setSingleShot(True)
|
||||
self._save_timer.timeout.connect(self._save_current)
|
||||
self.editor.textChanged.connect(self._on_text_changed)
|
||||
# Note: textChanged will be connected per-editor in _create_new_tab
|
||||
|
||||
# First load + mark dates in calendar with content
|
||||
if not self._load_yesterday_todos():
|
||||
|
|
@ -313,6 +322,248 @@ class MainWindow(QMainWindow):
|
|||
if self._try_connect():
|
||||
return True
|
||||
|
||||
# ----------------- Tab management ----------------- #
|
||||
|
||||
def _tab_index_for_date(self, date: QDate) -> int:
|
||||
"""Return the index of the tab showing `date`, or -1 if none."""
|
||||
iso = date.toString("yyyy-MM-dd")
|
||||
for i in range(self.tab_widget.count()):
|
||||
w = self.tab_widget.widget(i)
|
||||
if (
|
||||
hasattr(w, "current_date")
|
||||
and w.current_date.toString("yyyy-MM-dd") == iso
|
||||
):
|
||||
return i
|
||||
return -1
|
||||
|
||||
def _open_date_in_tab(self, date: QDate):
|
||||
"""Focus existing tab for `date`, or create it if needed. Returns the editor."""
|
||||
idx = self._tab_index_for_date(date)
|
||||
if idx != -1:
|
||||
self.tab_widget.setCurrentIndex(idx)
|
||||
# keep calendar selection in sync (don’t trigger load)
|
||||
from PySide6.QtCore import QSignalBlocker
|
||||
|
||||
with QSignalBlocker(self.calendar):
|
||||
self.calendar.setSelectedDate(date)
|
||||
QTimer.singleShot(0, self._focus_editor_now)
|
||||
return self.tab_widget.widget(idx)
|
||||
# not open yet -> create
|
||||
return self._create_new_tab(date)
|
||||
|
||||
def _create_new_tab(self, date: QDate | None = None) -> MarkdownEditor:
|
||||
if date is None:
|
||||
date = self.calendar.selectedDate()
|
||||
|
||||
# Deduplicate: if already open, just jump there
|
||||
existing = self._tab_index_for_date(date)
|
||||
if existing != -1:
|
||||
self.tab_widget.setCurrentIndex(existing)
|
||||
return self.tab_widget.widget(existing)
|
||||
|
||||
"""Create a new editor tab and return the editor instance."""
|
||||
editor = MarkdownEditor(self.themes)
|
||||
|
||||
# Set up the editor's event connections
|
||||
editor.currentCharFormatChanged.connect(lambda _f: self._sync_toolbar())
|
||||
editor.cursorPositionChanged.connect(self._sync_toolbar)
|
||||
editor.textChanged.connect(self._on_text_changed)
|
||||
|
||||
# Determine tab title
|
||||
if date is None:
|
||||
date = self.calendar.selectedDate()
|
||||
tab_title = date.toString("yyyy-MM-dd")
|
||||
|
||||
# Add the tab
|
||||
index = self.tab_widget.addTab(editor, tab_title)
|
||||
self.tab_widget.setCurrentIndex(index)
|
||||
|
||||
# Load the date's content
|
||||
self._load_date_into_editor(date, editor)
|
||||
|
||||
# Store the date with the editor so we can save it later
|
||||
editor.current_date = date
|
||||
|
||||
return editor
|
||||
|
||||
def _close_tab(self, index: int):
|
||||
"""Close a tab at the given index."""
|
||||
if self.tab_widget.count() <= 1:
|
||||
# Don't close the last tab
|
||||
return
|
||||
|
||||
editor = self.tab_widget.widget(index)
|
||||
if editor:
|
||||
# Save before closing
|
||||
self._save_editor_content(editor)
|
||||
|
||||
self.tab_widget.removeTab(index)
|
||||
|
||||
def _on_tab_changed(self, index: int):
|
||||
"""Handle tab change - reconnect toolbar and sync UI."""
|
||||
if index < 0:
|
||||
return
|
||||
|
||||
editor = self.tab_widget.widget(index)
|
||||
if editor and hasattr(editor, "current_date"):
|
||||
# Update calendar selection to match the tab
|
||||
with QSignalBlocker(self.calendar):
|
||||
self.calendar.setSelectedDate(editor.current_date)
|
||||
|
||||
# Reconnect toolbar to new active editor
|
||||
self._sync_toolbar()
|
||||
|
||||
# Focus the editor
|
||||
QTimer.singleShot(0, self._focus_editor_now)
|
||||
|
||||
def _call_editor(self, method_name, *args):
|
||||
"""
|
||||
Call the relevant method of the MarkdownEditor class on bind
|
||||
"""
|
||||
ed = self.current_editor()
|
||||
if ed is None:
|
||||
return
|
||||
getattr(ed, method_name)(*args)
|
||||
|
||||
def _bind_toolbar(self):
|
||||
if getattr(self, "_toolbar_bound", False):
|
||||
return
|
||||
tb = self.toolBar
|
||||
|
||||
# keep refs so we never create new lambdas (prevents accidental dupes)
|
||||
self._tb_bold = lambda: self._call_editor("apply_weight")
|
||||
self._tb_italic = lambda: self._call_editor("apply_italic")
|
||||
self._tb_strike = lambda: self._call_editor("apply_strikethrough")
|
||||
self._tb_code = lambda: self._call_editor("apply_code")
|
||||
self._tb_heading = lambda level: self._call_editor("apply_heading", level)
|
||||
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")
|
||||
|
||||
tb.boldRequested.connect(self._tb_bold)
|
||||
tb.italicRequested.connect(self._tb_italic)
|
||||
tb.strikeRequested.connect(self._tb_strike)
|
||||
tb.codeRequested.connect(self._tb_code)
|
||||
tb.headingRequested.connect(self._tb_heading)
|
||||
tb.bulletsRequested.connect(self._tb_bullets)
|
||||
tb.numbersRequested.connect(self._tb_numbers)
|
||||
tb.checkboxesRequested.connect(self._tb_checkboxes)
|
||||
|
||||
# these aren’t editor methods
|
||||
tb.historyRequested.connect(self._open_history)
|
||||
tb.insertImageRequested.connect(self._on_insert_image)
|
||||
|
||||
self._toolbar_bound = True
|
||||
|
||||
def current_editor(self) -> MarkdownEditor | None:
|
||||
"""Get the currently active editor."""
|
||||
return self.tab_widget.currentWidget()
|
||||
|
||||
@property
|
||||
def editor(self) -> MarkdownEditor | None:
|
||||
"""Compatibility property to get current editor (for existing code)."""
|
||||
return self.current_editor()
|
||||
|
||||
def _date_from_calendar_pos(self, pos) -> QDate | None:
|
||||
"""Translate a QCalendarWidget local pos to the QDate under the cursor."""
|
||||
view: QTableView = self.calendar.findChild(
|
||||
QTableView, "qt_calendar_calendarview"
|
||||
)
|
||||
if view is None:
|
||||
return None
|
||||
|
||||
# Map calendar-local pos -> viewport pos
|
||||
vp_pos = view.viewport().mapFrom(self.calendar, pos)
|
||||
idx = view.indexAt(vp_pos)
|
||||
if not idx.isValid():
|
||||
return None
|
||||
|
||||
model = view.model()
|
||||
|
||||
# Account for optional headers
|
||||
start_col = (
|
||||
0
|
||||
if self.calendar.verticalHeaderFormat() == QCalendarWidget.NoVerticalHeader
|
||||
else 1
|
||||
)
|
||||
start_row = (
|
||||
0
|
||||
if self.calendar.horizontalHeaderFormat()
|
||||
== QCalendarWidget.NoHorizontalHeader
|
||||
else 1
|
||||
)
|
||||
|
||||
# Find index of day 1 (first cell belonging to current month)
|
||||
first_index = None
|
||||
for r in range(start_row, model.rowCount()):
|
||||
for c in range(start_col, model.columnCount()):
|
||||
if model.index(r, c).data() == 1:
|
||||
first_index = model.index(r, c)
|
||||
break
|
||||
if first_index:
|
||||
break
|
||||
if first_index is None:
|
||||
return None
|
||||
|
||||
# Find index of the last day of the current month
|
||||
last_day = (
|
||||
QDate(self.calendar.yearShown(), self.calendar.monthShown(), 1)
|
||||
.addMonths(1)
|
||||
.addDays(-1)
|
||||
.day()
|
||||
)
|
||||
last_index = None
|
||||
for r in range(model.rowCount() - 1, first_index.row() - 1, -1):
|
||||
for c in range(model.columnCount() - 1, start_col - 1, -1):
|
||||
if model.index(r, c).data() == last_day:
|
||||
last_index = model.index(r, c)
|
||||
break
|
||||
if last_index:
|
||||
break
|
||||
if last_index is None:
|
||||
return None
|
||||
|
||||
# Determine if clicked cell belongs to prev/next month or current
|
||||
day = int(idx.data())
|
||||
year = self.calendar.yearShown()
|
||||
month = self.calendar.monthShown()
|
||||
|
||||
before_first = (idx.row() < first_index.row()) or (
|
||||
idx.row() == first_index.row() and idx.column() < first_index.column()
|
||||
)
|
||||
after_last = (idx.row() > last_index.row()) or (
|
||||
idx.row() == last_index.row() and idx.column() > last_index.column()
|
||||
)
|
||||
|
||||
if before_first:
|
||||
if month == 1:
|
||||
month = 12
|
||||
year -= 1
|
||||
else:
|
||||
month -= 1
|
||||
elif after_last:
|
||||
if month == 12:
|
||||
month = 1
|
||||
year += 1
|
||||
else:
|
||||
month += 1
|
||||
|
||||
qd = QDate(year, month, day)
|
||||
return qd if qd.isValid() else None
|
||||
|
||||
def _show_calendar_context_menu(self, pos):
|
||||
self._showing_context_menu = True # so selectionChanged handler doesn't fire
|
||||
clicked_date = self._date_from_calendar_pos(pos)
|
||||
|
||||
menu = QMenu(self)
|
||||
open_in_new_tab_action = menu.addAction("Open in New Tab")
|
||||
action = menu.exec_(self.calendar.mapToGlobal(pos))
|
||||
|
||||
self._showing_context_menu = False
|
||||
|
||||
if action == open_in_new_tab_action and clicked_date and clicked_date.isValid():
|
||||
self._open_date_in_tab(clicked_date)
|
||||
|
||||
def _retheme_overrides(self):
|
||||
if hasattr(self, "_lock_overlay"):
|
||||
self._lock_overlay._apply_overlay_style()
|
||||
|
|
@ -494,8 +745,28 @@ class MainWindow(QMainWindow):
|
|||
return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
|
||||
|
||||
def _load_selected_date(self, date_iso=False, extra_data=False):
|
||||
"""Load a date into the current editor (backward compatibility)."""
|
||||
editor = self.current_editor()
|
||||
if not editor:
|
||||
return
|
||||
|
||||
if not date_iso:
|
||||
date_iso = self._current_date_iso()
|
||||
|
||||
qd = QDate.fromString(date_iso, "yyyy-MM-dd")
|
||||
self._load_date_into_editor(qd, editor, extra_data)
|
||||
editor.current_date = qd
|
||||
|
||||
# Update tab title
|
||||
current_index = self.tab_widget.currentIndex()
|
||||
if current_index >= 0:
|
||||
self.tab_widget.setTabText(current_index, date_iso)
|
||||
|
||||
def _load_date_into_editor(
|
||||
self, date: QDate, editor: MarkdownEditor, extra_data=False
|
||||
):
|
||||
"""Load a specific date's content into a given editor."""
|
||||
date_iso = date.toString("yyyy-MM-dd")
|
||||
try:
|
||||
text = self.db.get_entry(date_iso)
|
||||
if extra_data:
|
||||
|
|
@ -504,21 +775,26 @@ class MainWindow(QMainWindow):
|
|||
text += "\n"
|
||||
text += extra_data
|
||||
# Force a save now so we don't lose it.
|
||||
self._set_editor_markdown_preserve_view(text)
|
||||
self._set_editor_markdown_preserve_view(text, editor)
|
||||
self._dirty = True
|
||||
self._save_date(date_iso, True)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Read Error", str(e))
|
||||
return
|
||||
|
||||
self._set_editor_markdown_preserve_view(text)
|
||||
|
||||
self._set_editor_markdown_preserve_view(text, editor)
|
||||
self._dirty = False
|
||||
# track which date the editor currently represents
|
||||
self._active_date_iso = date_iso
|
||||
qd = QDate.fromString(date_iso, "yyyy-MM-dd")
|
||||
self.calendar.setSelectedDate(qd)
|
||||
|
||||
def _save_editor_content(self, editor: MarkdownEditor):
|
||||
"""Save a specific editor's content to its associated date."""
|
||||
if not hasattr(editor, "current_date"):
|
||||
return
|
||||
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
||||
try:
|
||||
md = editor.to_markdown()
|
||||
self.db.save_new_version(date_iso, md, note="autosave")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Save Error", str(e))
|
||||
|
||||
def _on_text_changed(self):
|
||||
self._dirty = True
|
||||
|
|
@ -581,18 +857,36 @@ class MainWindow(QMainWindow):
|
|||
def _on_date_changed(self):
|
||||
"""
|
||||
When the calendar selection changes, save the previous day's note if dirty,
|
||||
so we don't lose that text, then load the newly selected day.
|
||||
so we don't lose that text, then load the newly selected day into current tab.
|
||||
"""
|
||||
# Skip if we're showing a context menu (right-click shouldn't load dates)
|
||||
if getattr(self, "_showing_context_menu", False):
|
||||
return
|
||||
|
||||
editor = self.current_editor()
|
||||
if not editor:
|
||||
return
|
||||
|
||||
# Stop pending autosave and persist current buffer if needed
|
||||
try:
|
||||
self._save_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
prev = getattr(self, "_active_date_iso", None)
|
||||
if prev and self._dirty:
|
||||
self._save_date(prev, explicit=False)
|
||||
# Now load the newly selected date
|
||||
self._load_selected_date()
|
||||
|
||||
# Save the current editor's content if dirty
|
||||
if hasattr(editor, "current_date") and self._dirty:
|
||||
prev_date_iso = editor.current_date.toString("yyyy-MM-dd")
|
||||
self._save_date(prev_date_iso, explicit=False)
|
||||
|
||||
# Now load the newly selected date into the current tab
|
||||
new_date = self.calendar.selectedDate()
|
||||
self._load_date_into_editor(new_date, editor)
|
||||
editor.current_date = new_date
|
||||
|
||||
# Update tab title
|
||||
current_index = self.tab_widget.currentIndex()
|
||||
if current_index >= 0:
|
||||
self.tab_widget.setTabText(current_index, new_date.toString("yyyy-MM-dd"))
|
||||
|
||||
def _save_date(self, date_iso: str, explicit: bool = False, note: str = "autosave"):
|
||||
"""
|
||||
|
|
@ -617,10 +911,16 @@ class MainWindow(QMainWindow):
|
|||
)
|
||||
|
||||
def _save_current(self, explicit: bool = False):
|
||||
"""Save the current editor's content."""
|
||||
try:
|
||||
self._save_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
editor = self.current_editor()
|
||||
if not editor or not hasattr(editor, "current_date"):
|
||||
return
|
||||
|
||||
if explicit:
|
||||
# Prompt for a note
|
||||
dlg = SaveDialog(self)
|
||||
|
|
@ -629,8 +929,9 @@ class MainWindow(QMainWindow):
|
|||
note = dlg.note_text()
|
||||
else:
|
||||
note = "autosave"
|
||||
# Delegate to _save_date for the currently selected date
|
||||
self._save_date(self._current_date_iso(), explicit, note)
|
||||
# Save the current editor's date
|
||||
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
||||
self._save_date(date_iso, explicit, note)
|
||||
try:
|
||||
self._save_timer.start()
|
||||
except Exception:
|
||||
|
|
@ -865,10 +1166,21 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|||
self._idle_timer.start()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
# Catch right-clicks on calendar BEFORE selectionChanged can fire
|
||||
if obj == self.calendar and event.type() == QEvent.MouseButtonPress:
|
||||
try:
|
||||
# QMouseEvent in PySide6
|
||||
if event.button() == Qt.RightButton:
|
||||
self._showing_context_menu = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if event.type() == QEvent.KeyPress and not self._locked:
|
||||
self._idle_timer.start()
|
||||
|
||||
if event.type() in (QEvent.ApplicationActivate, QEvent.WindowActivate):
|
||||
QTimer.singleShot(0, self._focus_editor_now)
|
||||
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def _enter_lock(self):
|
||||
|
|
@ -920,8 +1232,11 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|||
self.settings.setValue("main/windowState", self.saveState())
|
||||
self.settings.setValue("main/maximized", self.isMaximized())
|
||||
|
||||
# Ensure we save any last pending edits to the db
|
||||
self._save_current()
|
||||
# Ensure we save all tabs before closing
|
||||
for i in range(self.tab_widget.count()):
|
||||
editor = self.tab_widget.widget(i)
|
||||
if editor:
|
||||
self._save_editor_content(editor)
|
||||
self.db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -935,14 +1250,17 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|||
return
|
||||
if not self.isActiveWindow():
|
||||
return
|
||||
editor = self.current_editor()
|
||||
if not editor:
|
||||
return
|
||||
# Belt-and-suspenders: do it now and once more on the next tick
|
||||
self.editor.setFocus(Qt.ActiveWindowFocusReason)
|
||||
self.editor.ensureCursorVisible()
|
||||
editor.setFocus(Qt.ActiveWindowFocusReason)
|
||||
editor.ensureCursorVisible()
|
||||
QTimer.singleShot(
|
||||
0,
|
||||
lambda: (
|
||||
self.editor.setFocus(Qt.ActiveWindowFocusReason),
|
||||
self.editor.ensureCursorVisible(),
|
||||
editor.setFocus(Qt.ActiveWindowFocusReason) if editor else None,
|
||||
editor.ensureCursorVisible() if editor else None,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -957,8 +1275,15 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|||
if ev.type() == QEvent.ActivationChange and self.isActiveWindow():
|
||||
QTimer.singleShot(0, self._focus_editor_now)
|
||||
|
||||
def _set_editor_markdown_preserve_view(self, markdown: str):
|
||||
ed = self.editor
|
||||
def _set_editor_markdown_preserve_view(
|
||||
self, markdown: str, editor: MarkdownEditor | None = None
|
||||
):
|
||||
if editor is None:
|
||||
editor = self.current_editor()
|
||||
if not editor:
|
||||
return
|
||||
|
||||
ed = editor
|
||||
|
||||
# Save caret/selection and scroll
|
||||
cur = ed.textCursor()
|
||||
|
|
|
|||
|
|
@ -238,6 +238,12 @@ class MarkdownEditor(QTextEdit):
|
|||
# Enable mouse tracking for checkbox clicking
|
||||
self.viewport().setMouseTracking(True)
|
||||
|
||||
def setDocument(self, doc):
|
||||
super().setDocument(doc)
|
||||
# reattach the highlighter to the new document
|
||||
if hasattr(self, "highlighter") and self.highlighter:
|
||||
self.highlighter.setDocument(self.document())
|
||||
|
||||
def _on_text_changed(self):
|
||||
"""Handle live formatting updates - convert checkbox markdown to Unicode."""
|
||||
if self._updating:
|
||||
|
|
@ -257,7 +263,7 @@ class MarkdownEditor(QTextEdit):
|
|||
s = s.replace("- [x] ", f"- {self._CHECK_CHECKED_DISPLAY} ")
|
||||
s = s.replace("- [ ] ", f"- {self._CHECK_UNCHECKED_DISPLAY} ")
|
||||
s = re.sub(
|
||||
r'^([ \t]*)TODO\b[:\-]?\s+',
|
||||
r"^([ \t]*)TODO\b[:\-]?\s+",
|
||||
lambda m: f"{m.group(1)}- {self._CHECK_UNCHECKED_DISPLAY} ",
|
||||
s,
|
||||
)
|
||||
|
|
@ -273,8 +279,9 @@ class MarkdownEditor(QTextEdit):
|
|||
bc.endEditBlock()
|
||||
|
||||
# Restore cursor near its original visual position in the edited line
|
||||
new_pos = min(block.position() + len(new_line),
|
||||
block.position() + pos_in_block)
|
||||
new_pos = min(
|
||||
block.position() + len(new_line), block.position() + pos_in_block
|
||||
)
|
||||
c.setPosition(new_pos)
|
||||
self.setTextCursor(c)
|
||||
finally:
|
||||
|
|
@ -344,6 +351,8 @@ class MarkdownEditor(QTextEdit):
|
|||
self._updating = True
|
||||
try:
|
||||
self.setPlainText(display_text)
|
||||
if hasattr(self, "highlighter") and self.highlighter:
|
||||
self.highlighter.rehighlight()
|
||||
finally:
|
||||
self._updating = False
|
||||
|
||||
|
|
@ -445,6 +454,30 @@ class MarkdownEditor(QTextEdit):
|
|||
|
||||
# Check if we're in a code block
|
||||
current_block = cursor.block()
|
||||
line_text = current_block.text()
|
||||
pos_in_block = cursor.position() - current_block.position()
|
||||
|
||||
moved = False
|
||||
i = 0
|
||||
patterns = ["**", "__", "~~", "`", "*", "_"] # bold, italic, strike, code
|
||||
# Consume stacked markers like **` if present
|
||||
while True:
|
||||
matched = False
|
||||
for pat in patterns:
|
||||
L = len(pat)
|
||||
if line_text[pos_in_block + i : pos_in_block + i + L] == pat:
|
||||
i += L
|
||||
matched = True
|
||||
moved = True
|
||||
break
|
||||
if not matched:
|
||||
break
|
||||
if moved:
|
||||
cursor.movePosition(
|
||||
QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.MoveAnchor, i
|
||||
)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
block_state = current_block.userState()
|
||||
|
||||
# If current line is opening code fence, or we're inside a code block
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue