bouquin 0.1.10__py3-none-any.whl → 0.2.1.2__py3-none-any.whl
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/db.py +34 -91
- bouquin/find_bar.py +208 -0
- bouquin/history_dialog.py +29 -28
- bouquin/key_prompt.py +6 -0
- bouquin/lock_overlay.py +8 -2
- bouquin/main_window.py +598 -119
- bouquin/markdown_editor.py +813 -0
- bouquin/save_dialog.py +3 -0
- bouquin/search.py +46 -31
- bouquin/settings_dialog.py +1 -1
- bouquin/theme.py +1 -2
- bouquin/toolbar.py +4 -41
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/METADATA +10 -7
- bouquin-0.2.1.2.dist-info/RECORD +21 -0
- bouquin/editor.py +0 -897
- bouquin-0.1.10.dist-info/RECORD +0 -20
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/LICENSE +0 -0
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/WHEEL +0 -0
- {bouquin-0.1.10.dist-info → bouquin-0.2.1.2.dist-info}/entry_points.txt +0 -0
bouquin/main_window.py
CHANGED
|
@@ -24,8 +24,10 @@ from PySide6.QtGui import (
|
|
|
24
24
|
QDesktopServices,
|
|
25
25
|
QFont,
|
|
26
26
|
QGuiApplication,
|
|
27
|
+
QKeySequence,
|
|
27
28
|
QPalette,
|
|
28
29
|
QTextCharFormat,
|
|
30
|
+
QTextCursor,
|
|
29
31
|
QTextListFormat,
|
|
30
32
|
)
|
|
31
33
|
from PySide6.QtWidgets import (
|
|
@@ -34,15 +36,19 @@ from PySide6.QtWidgets import (
|
|
|
34
36
|
QDialog,
|
|
35
37
|
QFileDialog,
|
|
36
38
|
QMainWindow,
|
|
39
|
+
QMenu,
|
|
37
40
|
QMessageBox,
|
|
38
41
|
QSizePolicy,
|
|
39
42
|
QSplitter,
|
|
43
|
+
QTableView,
|
|
44
|
+
QTabWidget,
|
|
40
45
|
QVBoxLayout,
|
|
41
46
|
QWidget,
|
|
42
47
|
)
|
|
43
48
|
|
|
44
49
|
from .db import DBManager
|
|
45
|
-
from .
|
|
50
|
+
from .markdown_editor import MarkdownEditor
|
|
51
|
+
from .find_bar import FindBar
|
|
46
52
|
from .history_dialog import HistoryDialog
|
|
47
53
|
from .key_prompt import KeyPrompt
|
|
48
54
|
from .lock_overlay import LockOverlay
|
|
@@ -95,33 +101,36 @@ class MainWindow(QMainWindow):
|
|
|
95
101
|
left_layout.addWidget(self.search)
|
|
96
102
|
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
|
|
97
103
|
|
|
98
|
-
#
|
|
99
|
-
self.
|
|
104
|
+
# Create tab widget to hold multiple editors
|
|
105
|
+
self.tab_widget = QTabWidget()
|
|
106
|
+
self.tab_widget.setTabsClosable(True)
|
|
107
|
+
self.tab_widget.tabCloseRequested.connect(self._close_tab)
|
|
108
|
+
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
|
100
109
|
|
|
101
110
|
# Toolbar for controlling styling
|
|
102
111
|
self.toolBar = ToolBar()
|
|
103
112
|
self.addToolBar(self.toolBar)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
self.
|
|
108
|
-
self.toolBar.strikeRequested.connect(self.editor.apply_strikethrough)
|
|
109
|
-
self.toolBar.codeRequested.connect(self.editor.apply_code)
|
|
110
|
-
self.toolBar.headingRequested.connect(self.editor.apply_heading)
|
|
111
|
-
self.toolBar.bulletsRequested.connect(self.editor.toggle_bullets)
|
|
112
|
-
self.toolBar.numbersRequested.connect(self.editor.toggle_numbers)
|
|
113
|
-
self.toolBar.checkboxesRequested.connect(self.editor.toggle_checkboxes)
|
|
114
|
-
self.toolBar.alignRequested.connect(self.editor.setAlignment)
|
|
115
|
-
self.toolBar.historyRequested.connect(self._open_history)
|
|
116
|
-
self.toolBar.insertImageRequested.connect(self._on_insert_image)
|
|
117
|
-
|
|
118
|
-
self.editor.currentCharFormatChanged.connect(lambda _f: self._sync_toolbar())
|
|
119
|
-
self.editor.cursorPositionChanged.connect(self._sync_toolbar)
|
|
113
|
+
self._bind_toolbar()
|
|
114
|
+
|
|
115
|
+
# Create the first editor tab
|
|
116
|
+
self._create_new_tab()
|
|
120
117
|
|
|
121
118
|
split = QSplitter()
|
|
122
119
|
split.addWidget(left_panel)
|
|
123
|
-
split.addWidget(self.
|
|
124
|
-
split.setStretchFactor(1, 1)
|
|
120
|
+
split.addWidget(self.tab_widget)
|
|
121
|
+
split.setStretchFactor(1, 1)
|
|
122
|
+
|
|
123
|
+
# Enable context menu on calendar for opening dates in new tabs
|
|
124
|
+
self.calendar.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
125
|
+
self.calendar.customContextMenuRequested.connect(
|
|
126
|
+
self._show_calendar_context_menu
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Flag to prevent _on_date_changed when showing context menu
|
|
130
|
+
self._showing_context_menu = False
|
|
131
|
+
|
|
132
|
+
# Install event filter to catch right-clicks before selectionChanged fires
|
|
133
|
+
self.calendar.installEventFilter(self)
|
|
125
134
|
|
|
126
135
|
container = QWidget()
|
|
127
136
|
lay = QVBoxLayout(container)
|
|
@@ -146,8 +155,26 @@ class MainWindow(QMainWindow):
|
|
|
146
155
|
|
|
147
156
|
QApplication.instance().installEventFilter(self)
|
|
148
157
|
|
|
158
|
+
# Focus on the editor
|
|
159
|
+
self.setFocusPolicy(Qt.StrongFocus)
|
|
160
|
+
self.editor.setFocusPolicy(Qt.StrongFocus)
|
|
161
|
+
self.toolBar.setFocusPolicy(Qt.NoFocus)
|
|
162
|
+
for w in self.toolBar.findChildren(QWidget):
|
|
163
|
+
w.setFocusPolicy(Qt.NoFocus)
|
|
164
|
+
QGuiApplication.instance().applicationStateChanged.connect(
|
|
165
|
+
self._on_app_state_changed
|
|
166
|
+
)
|
|
167
|
+
|
|
149
168
|
# Status bar for feedback
|
|
150
169
|
self.statusBar().showMessage("Ready", 800)
|
|
170
|
+
# Add findBar and add it to the statusBar
|
|
171
|
+
# FindBar will get the current editor dynamically via a callable
|
|
172
|
+
self.findBar = FindBar(
|
|
173
|
+
lambda: self.current_editor(), shortcut_parent=self, parent=self
|
|
174
|
+
)
|
|
175
|
+
self.statusBar().addPermanentWidget(self.findBar)
|
|
176
|
+
# When the findBar closes, put the caret back in the editor
|
|
177
|
+
self.findBar.closed.connect(self._focus_editor_now)
|
|
151
178
|
|
|
152
179
|
# Menu bar (File)
|
|
153
180
|
mb = self.menuBar()
|
|
@@ -202,6 +229,24 @@ class MainWindow(QMainWindow):
|
|
|
202
229
|
nav_menu.addAction(act_today)
|
|
203
230
|
self.addAction(act_today)
|
|
204
231
|
|
|
232
|
+
act_find = QAction("Find on page", self)
|
|
233
|
+
act_find.setShortcut(QKeySequence.Find)
|
|
234
|
+
act_find.triggered.connect(self.findBar.show_bar)
|
|
235
|
+
nav_menu.addAction(act_find)
|
|
236
|
+
self.addAction(act_find)
|
|
237
|
+
|
|
238
|
+
act_find_next = QAction("Find Next", self)
|
|
239
|
+
act_find_next.setShortcut(QKeySequence.FindNext)
|
|
240
|
+
act_find_next.triggered.connect(self.findBar.find_next)
|
|
241
|
+
nav_menu.addAction(act_find_next)
|
|
242
|
+
self.addAction(act_find_next)
|
|
243
|
+
|
|
244
|
+
act_find_prev = QAction("Find Previous", self)
|
|
245
|
+
act_find_prev.setShortcut(QKeySequence.FindPrevious)
|
|
246
|
+
act_find_prev.triggered.connect(self.findBar.find_prev)
|
|
247
|
+
nav_menu.addAction(act_find_prev)
|
|
248
|
+
self.addAction(act_find_prev)
|
|
249
|
+
|
|
205
250
|
# Help menu with drop-down
|
|
206
251
|
help_menu = mb.addMenu("&Help")
|
|
207
252
|
act_docs = QAction("Documentation", self)
|
|
@@ -222,7 +267,7 @@ class MainWindow(QMainWindow):
|
|
|
222
267
|
self._save_timer = QTimer(self)
|
|
223
268
|
self._save_timer.setSingleShot(True)
|
|
224
269
|
self._save_timer.timeout.connect(self._save_current)
|
|
225
|
-
|
|
270
|
+
# Note: textChanged will be connected per-editor in _create_new_tab
|
|
226
271
|
|
|
227
272
|
# First load + mark dates in calendar with content
|
|
228
273
|
if not self._load_yesterday_todos():
|
|
@@ -277,11 +322,282 @@ class MainWindow(QMainWindow):
|
|
|
277
322
|
if self._try_connect():
|
|
278
323
|
return True
|
|
279
324
|
|
|
325
|
+
# ----------------- Tab and date management ----------------- #
|
|
326
|
+
|
|
327
|
+
def _current_date_iso(self) -> str:
|
|
328
|
+
d = self.calendar.selectedDate()
|
|
329
|
+
return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
|
|
330
|
+
|
|
331
|
+
def _date_key(self, qd: QDate) -> tuple[int, int, int]:
|
|
332
|
+
return (qd.year(), qd.month(), qd.day())
|
|
333
|
+
|
|
334
|
+
def _index_for_date_insert(self, date: QDate) -> int:
|
|
335
|
+
"""Return the index where a tab for `date` should be inserted (ascending order)."""
|
|
336
|
+
key = self._date_key(date)
|
|
337
|
+
for i in range(self.tab_widget.count()):
|
|
338
|
+
w = self.tab_widget.widget(i)
|
|
339
|
+
d = getattr(w, "current_date", None)
|
|
340
|
+
if isinstance(d, QDate) and d.isValid():
|
|
341
|
+
if self._date_key(d) > key:
|
|
342
|
+
return i
|
|
343
|
+
return self.tab_widget.count()
|
|
344
|
+
|
|
345
|
+
def _reorder_tabs_by_date(self):
|
|
346
|
+
"""Reorder existing tabs by their date (ascending)."""
|
|
347
|
+
bar = self.tab_widget.tabBar()
|
|
348
|
+
dated, undated = [], []
|
|
349
|
+
|
|
350
|
+
for i in range(self.tab_widget.count()):
|
|
351
|
+
w = self.tab_widget.widget(i)
|
|
352
|
+
d = getattr(w, "current_date", None)
|
|
353
|
+
if isinstance(d, QDate) and d.isValid():
|
|
354
|
+
dated.append((d, w))
|
|
355
|
+
else:
|
|
356
|
+
undated.append(w)
|
|
357
|
+
|
|
358
|
+
dated.sort(key=lambda t: self._date_key(t[0]))
|
|
359
|
+
|
|
360
|
+
with QSignalBlocker(self.tab_widget):
|
|
361
|
+
# Update labels to yyyy-MM-dd
|
|
362
|
+
for d, w in dated:
|
|
363
|
+
idx = self.tab_widget.indexOf(w)
|
|
364
|
+
if idx != -1:
|
|
365
|
+
self.tab_widget.setTabText(idx, d.toString("yyyy-MM-dd"))
|
|
366
|
+
|
|
367
|
+
# Move dated tabs into target positions 0..len(dated)-1
|
|
368
|
+
for target_pos, (_, w) in enumerate(dated):
|
|
369
|
+
cur = self.tab_widget.indexOf(w)
|
|
370
|
+
if cur != -1 and cur != target_pos:
|
|
371
|
+
bar.moveTab(cur, target_pos)
|
|
372
|
+
|
|
373
|
+
# Keep any undated pages (if they ever exist) after the dated ones
|
|
374
|
+
start = len(dated)
|
|
375
|
+
for offset, w in enumerate(undated):
|
|
376
|
+
cur = self.tab_widget.indexOf(w)
|
|
377
|
+
target = start + offset
|
|
378
|
+
if cur != -1 and cur != target:
|
|
379
|
+
bar.moveTab(cur, target)
|
|
380
|
+
|
|
381
|
+
def _tab_index_for_date(self, date: QDate) -> int:
|
|
382
|
+
"""Return the index of the tab showing `date`, or -1 if none."""
|
|
383
|
+
iso = date.toString("yyyy-MM-dd")
|
|
384
|
+
for i in range(self.tab_widget.count()):
|
|
385
|
+
w = self.tab_widget.widget(i)
|
|
386
|
+
if (
|
|
387
|
+
hasattr(w, "current_date")
|
|
388
|
+
and w.current_date.toString("yyyy-MM-dd") == iso
|
|
389
|
+
):
|
|
390
|
+
return i
|
|
391
|
+
return -1
|
|
392
|
+
|
|
393
|
+
def _open_date_in_tab(self, date: QDate):
|
|
394
|
+
"""Focus existing tab for `date`, or create it if needed. Returns the editor."""
|
|
395
|
+
idx = self._tab_index_for_date(date)
|
|
396
|
+
if idx != -1:
|
|
397
|
+
self.tab_widget.setCurrentIndex(idx)
|
|
398
|
+
# keep calendar selection in sync (don’t trigger load)
|
|
399
|
+
from PySide6.QtCore import QSignalBlocker
|
|
400
|
+
|
|
401
|
+
with QSignalBlocker(self.calendar):
|
|
402
|
+
self.calendar.setSelectedDate(date)
|
|
403
|
+
QTimer.singleShot(0, self._focus_editor_now)
|
|
404
|
+
return self.tab_widget.widget(idx)
|
|
405
|
+
# not open yet -> create
|
|
406
|
+
return self._create_new_tab(date)
|
|
407
|
+
|
|
408
|
+
def _create_new_tab(self, date: QDate | None = None) -> MarkdownEditor:
|
|
409
|
+
if date is None:
|
|
410
|
+
date = self.calendar.selectedDate()
|
|
411
|
+
|
|
412
|
+
# Deduplicate: if already open, just jump there
|
|
413
|
+
existing = self._tab_index_for_date(date)
|
|
414
|
+
if existing != -1:
|
|
415
|
+
self.tab_widget.setCurrentIndex(existing)
|
|
416
|
+
return self.tab_widget.widget(existing)
|
|
417
|
+
|
|
418
|
+
"""Create a new editor tab and return the editor instance."""
|
|
419
|
+
editor = MarkdownEditor(self.themes)
|
|
420
|
+
|
|
421
|
+
# Set up the editor's event connections
|
|
422
|
+
editor.currentCharFormatChanged.connect(lambda _f: self._sync_toolbar())
|
|
423
|
+
editor.cursorPositionChanged.connect(self._sync_toolbar)
|
|
424
|
+
editor.textChanged.connect(self._on_text_changed)
|
|
425
|
+
|
|
426
|
+
# Set tab title
|
|
427
|
+
tab_title = date.toString("yyyy-MM-dd")
|
|
428
|
+
|
|
429
|
+
# Add the tab
|
|
430
|
+
index = self.tab_widget.addTab(editor, tab_title)
|
|
431
|
+
self.tab_widget.setCurrentIndex(index)
|
|
432
|
+
|
|
433
|
+
# Load the date's content
|
|
434
|
+
self._load_date_into_editor(date, editor)
|
|
435
|
+
|
|
436
|
+
# Store the date with the editor so we can save it later
|
|
437
|
+
editor.current_date = date
|
|
438
|
+
|
|
439
|
+
# Insert at sorted position
|
|
440
|
+
tab_title = date.toString("yyyy-MM-dd")
|
|
441
|
+
pos = self._index_for_date_insert(date)
|
|
442
|
+
index = self.tab_widget.insertTab(pos, editor, tab_title)
|
|
443
|
+
self.tab_widget.setCurrentIndex(index)
|
|
444
|
+
|
|
445
|
+
return editor
|
|
446
|
+
|
|
447
|
+
def _close_tab(self, index: int):
|
|
448
|
+
"""Close a tab at the given index."""
|
|
449
|
+
if self.tab_widget.count() <= 1:
|
|
450
|
+
# Don't close the last tab
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
editor = self.tab_widget.widget(index)
|
|
454
|
+
if editor:
|
|
455
|
+
# Save before closing
|
|
456
|
+
self._save_editor_content(editor)
|
|
457
|
+
|
|
458
|
+
self.tab_widget.removeTab(index)
|
|
459
|
+
|
|
460
|
+
def _on_tab_changed(self, index: int):
|
|
461
|
+
"""Handle tab change - reconnect toolbar and sync UI."""
|
|
462
|
+
if index < 0:
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
editor = self.tab_widget.widget(index)
|
|
466
|
+
if editor and hasattr(editor, "current_date"):
|
|
467
|
+
# Update calendar selection to match the tab
|
|
468
|
+
with QSignalBlocker(self.calendar):
|
|
469
|
+
self.calendar.setSelectedDate(editor.current_date)
|
|
470
|
+
|
|
471
|
+
# Reconnect toolbar to new active editor
|
|
472
|
+
self._sync_toolbar()
|
|
473
|
+
|
|
474
|
+
# Focus the editor
|
|
475
|
+
QTimer.singleShot(0, self._focus_editor_now)
|
|
476
|
+
|
|
477
|
+
def _call_editor(self, method_name, *args):
|
|
478
|
+
"""
|
|
479
|
+
Call the relevant method of the MarkdownEditor class on bind
|
|
480
|
+
"""
|
|
481
|
+
ed = self.current_editor()
|
|
482
|
+
if ed is None:
|
|
483
|
+
return
|
|
484
|
+
getattr(ed, method_name)(*args)
|
|
485
|
+
|
|
486
|
+
def current_editor(self) -> MarkdownEditor | None:
|
|
487
|
+
"""Get the currently active editor."""
|
|
488
|
+
return self.tab_widget.currentWidget()
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def editor(self) -> MarkdownEditor | None:
|
|
492
|
+
"""Compatibility property to get current editor (for existing code)."""
|
|
493
|
+
return self.current_editor()
|
|
494
|
+
|
|
495
|
+
def _date_from_calendar_pos(self, pos) -> QDate | None:
|
|
496
|
+
"""Translate a QCalendarWidget local pos to the QDate under the cursor."""
|
|
497
|
+
view: QTableView = self.calendar.findChild(
|
|
498
|
+
QTableView, "qt_calendar_calendarview"
|
|
499
|
+
)
|
|
500
|
+
if view is None:
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
# Map calendar-local pos -> viewport pos
|
|
504
|
+
vp_pos = view.viewport().mapFrom(self.calendar, pos)
|
|
505
|
+
idx = view.indexAt(vp_pos)
|
|
506
|
+
if not idx.isValid():
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
model = view.model()
|
|
510
|
+
|
|
511
|
+
# Account for optional headers
|
|
512
|
+
start_col = (
|
|
513
|
+
0
|
|
514
|
+
if self.calendar.verticalHeaderFormat() == QCalendarWidget.NoVerticalHeader
|
|
515
|
+
else 1
|
|
516
|
+
)
|
|
517
|
+
start_row = (
|
|
518
|
+
0
|
|
519
|
+
if self.calendar.horizontalHeaderFormat()
|
|
520
|
+
== QCalendarWidget.NoHorizontalHeader
|
|
521
|
+
else 1
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Find index of day 1 (first cell belonging to current month)
|
|
525
|
+
first_index = None
|
|
526
|
+
for r in range(start_row, model.rowCount()):
|
|
527
|
+
for c in range(start_col, model.columnCount()):
|
|
528
|
+
if model.index(r, c).data() == 1:
|
|
529
|
+
first_index = model.index(r, c)
|
|
530
|
+
break
|
|
531
|
+
if first_index:
|
|
532
|
+
break
|
|
533
|
+
if first_index is None:
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
# Find index of the last day of the current month
|
|
537
|
+
last_day = (
|
|
538
|
+
QDate(self.calendar.yearShown(), self.calendar.monthShown(), 1)
|
|
539
|
+
.addMonths(1)
|
|
540
|
+
.addDays(-1)
|
|
541
|
+
.day()
|
|
542
|
+
)
|
|
543
|
+
last_index = None
|
|
544
|
+
for r in range(model.rowCount() - 1, first_index.row() - 1, -1):
|
|
545
|
+
for c in range(model.columnCount() - 1, start_col - 1, -1):
|
|
546
|
+
if model.index(r, c).data() == last_day:
|
|
547
|
+
last_index = model.index(r, c)
|
|
548
|
+
break
|
|
549
|
+
if last_index:
|
|
550
|
+
break
|
|
551
|
+
if last_index is None:
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
# Determine if clicked cell belongs to prev/next month or current
|
|
555
|
+
day = int(idx.data())
|
|
556
|
+
year = self.calendar.yearShown()
|
|
557
|
+
month = self.calendar.monthShown()
|
|
558
|
+
|
|
559
|
+
before_first = (idx.row() < first_index.row()) or (
|
|
560
|
+
idx.row() == first_index.row() and idx.column() < first_index.column()
|
|
561
|
+
)
|
|
562
|
+
after_last = (idx.row() > last_index.row()) or (
|
|
563
|
+
idx.row() == last_index.row() and idx.column() > last_index.column()
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if before_first:
|
|
567
|
+
if month == 1:
|
|
568
|
+
month = 12
|
|
569
|
+
year -= 1
|
|
570
|
+
else:
|
|
571
|
+
month -= 1
|
|
572
|
+
elif after_last:
|
|
573
|
+
if month == 12:
|
|
574
|
+
month = 1
|
|
575
|
+
year += 1
|
|
576
|
+
else:
|
|
577
|
+
month += 1
|
|
578
|
+
|
|
579
|
+
qd = QDate(year, month, day)
|
|
580
|
+
return qd if qd.isValid() else None
|
|
581
|
+
|
|
582
|
+
def _show_calendar_context_menu(self, pos):
|
|
583
|
+
self._showing_context_menu = True # so selectionChanged handler doesn't fire
|
|
584
|
+
clicked_date = self._date_from_calendar_pos(pos)
|
|
585
|
+
|
|
586
|
+
menu = QMenu(self)
|
|
587
|
+
open_in_new_tab_action = menu.addAction("Open in New Tab")
|
|
588
|
+
action = menu.exec_(self.calendar.mapToGlobal(pos))
|
|
589
|
+
|
|
590
|
+
self._showing_context_menu = False
|
|
591
|
+
|
|
592
|
+
if action == open_in_new_tab_action and clicked_date and clicked_date.isValid():
|
|
593
|
+
self._open_date_in_tab(clicked_date)
|
|
594
|
+
|
|
595
|
+
# ----------------- Some theme helpers -------------------#
|
|
280
596
|
def _retheme_overrides(self):
|
|
281
597
|
if hasattr(self, "_lock_overlay"):
|
|
282
598
|
self._lock_overlay._apply_overlay_style()
|
|
283
599
|
self._apply_calendar_text_colors()
|
|
284
|
-
self._apply_link_css()
|
|
600
|
+
self._apply_link_css()
|
|
285
601
|
self._apply_search_highlights(getattr(self, "_search_highlighted_dates", set()))
|
|
286
602
|
self.calendar.update()
|
|
287
603
|
self.editor.viewport().update()
|
|
@@ -298,7 +614,6 @@ class MainWindow(QMainWindow):
|
|
|
298
614
|
css = "" # Default to no custom styling for links (system or light theme)
|
|
299
615
|
|
|
300
616
|
try:
|
|
301
|
-
# Apply to the editor (QTextEdit or any other relevant widgets)
|
|
302
617
|
self.editor.document().setDefaultStyleSheet(css)
|
|
303
618
|
except Exception:
|
|
304
619
|
pass
|
|
@@ -347,7 +662,6 @@ class MainWindow(QMainWindow):
|
|
|
347
662
|
self.calendar.setPalette(app_pal)
|
|
348
663
|
self.calendar.setStyleSheet("")
|
|
349
664
|
|
|
350
|
-
# Keep weekend text color in sync with the current palette
|
|
351
665
|
self._apply_calendar_text_colors()
|
|
352
666
|
self.calendar.update()
|
|
353
667
|
|
|
@@ -413,20 +727,47 @@ class MainWindow(QMainWindow):
|
|
|
413
727
|
|
|
414
728
|
# --- UI handlers ---------------------------------------------------------
|
|
415
729
|
|
|
730
|
+
def _bind_toolbar(self):
|
|
731
|
+
if getattr(self, "_toolbar_bound", False):
|
|
732
|
+
return
|
|
733
|
+
tb = self.toolBar
|
|
734
|
+
|
|
735
|
+
# keep refs so we never create new lambdas (prevents accidental dupes)
|
|
736
|
+
self._tb_bold = lambda: self._call_editor("apply_weight")
|
|
737
|
+
self._tb_italic = lambda: self._call_editor("apply_italic")
|
|
738
|
+
self._tb_strike = lambda: self._call_editor("apply_strikethrough")
|
|
739
|
+
self._tb_code = lambda: self._call_editor("apply_code")
|
|
740
|
+
self._tb_heading = lambda level: self._call_editor("apply_heading", level)
|
|
741
|
+
self._tb_bullets = lambda: self._call_editor("toggle_bullets")
|
|
742
|
+
self._tb_numbers = lambda: self._call_editor("toggle_numbers")
|
|
743
|
+
self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes")
|
|
744
|
+
|
|
745
|
+
tb.boldRequested.connect(self._tb_bold)
|
|
746
|
+
tb.italicRequested.connect(self._tb_italic)
|
|
747
|
+
tb.strikeRequested.connect(self._tb_strike)
|
|
748
|
+
tb.codeRequested.connect(self._tb_code)
|
|
749
|
+
tb.headingRequested.connect(self._tb_heading)
|
|
750
|
+
tb.bulletsRequested.connect(self._tb_bullets)
|
|
751
|
+
tb.numbersRequested.connect(self._tb_numbers)
|
|
752
|
+
tb.checkboxesRequested.connect(self._tb_checkboxes)
|
|
753
|
+
|
|
754
|
+
# these aren’t editor methods
|
|
755
|
+
tb.historyRequested.connect(self._open_history)
|
|
756
|
+
tb.insertImageRequested.connect(self._on_insert_image)
|
|
757
|
+
|
|
758
|
+
self._toolbar_bound = True
|
|
759
|
+
|
|
416
760
|
def _sync_toolbar(self):
|
|
417
761
|
fmt = self.editor.currentCharFormat()
|
|
418
762
|
c = self.editor.textCursor()
|
|
419
|
-
bf = c.blockFormat()
|
|
420
763
|
|
|
421
764
|
# Block signals so setChecked() doesn't re-trigger actions
|
|
422
765
|
QSignalBlocker(self.toolBar.actBold)
|
|
423
766
|
QSignalBlocker(self.toolBar.actItalic)
|
|
424
|
-
QSignalBlocker(self.toolBar.actUnderline)
|
|
425
767
|
QSignalBlocker(self.toolBar.actStrike)
|
|
426
768
|
|
|
427
769
|
self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold)
|
|
428
770
|
self.toolBar.actItalic.setChecked(fmt.fontItalic())
|
|
429
|
-
self.toolBar.actUnderline.setChecked(fmt.fontUnderline())
|
|
430
771
|
self.toolBar.actStrike.setChecked(fmt.fontStrikeOut())
|
|
431
772
|
|
|
432
773
|
# Headings: decide which to check by current point size
|
|
@@ -458,49 +799,60 @@ class MainWindow(QMainWindow):
|
|
|
458
799
|
self.toolBar.actBullets.setChecked(bool(bullets_on))
|
|
459
800
|
self.toolBar.actNumbers.setChecked(bool(numbers_on))
|
|
460
801
|
|
|
461
|
-
# Alignment
|
|
462
|
-
align = bf.alignment() & Qt.AlignHorizontal_Mask
|
|
463
|
-
QSignalBlocker(self.toolBar.actAlignL)
|
|
464
|
-
self.toolBar.actAlignL.setChecked(align == Qt.AlignLeft)
|
|
465
|
-
QSignalBlocker(self.toolBar.actAlignC)
|
|
466
|
-
self.toolBar.actAlignC.setChecked(align == Qt.AlignHCenter)
|
|
467
|
-
QSignalBlocker(self.toolBar.actAlignR)
|
|
468
|
-
self.toolBar.actAlignR.setChecked(align == Qt.AlignRight)
|
|
469
|
-
|
|
470
|
-
def _current_date_iso(self) -> str:
|
|
471
|
-
d = self.calendar.selectedDate()
|
|
472
|
-
return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
|
|
473
|
-
|
|
474
802
|
def _load_selected_date(self, date_iso=False, extra_data=False):
|
|
803
|
+
"""Load a date into the current editor"""
|
|
804
|
+
editor = self.current_editor()
|
|
805
|
+
if not editor:
|
|
806
|
+
return
|
|
807
|
+
|
|
475
808
|
if not date_iso:
|
|
476
809
|
date_iso = self._current_date_iso()
|
|
810
|
+
|
|
811
|
+
qd = QDate.fromString(date_iso, "yyyy-MM-dd")
|
|
812
|
+
self._load_date_into_editor(qd, editor, extra_data)
|
|
813
|
+
editor.current_date = qd
|
|
814
|
+
|
|
815
|
+
# Update tab title
|
|
816
|
+
current_index = self.tab_widget.currentIndex()
|
|
817
|
+
if current_index >= 0:
|
|
818
|
+
self.tab_widget.setTabText(current_index, date_iso)
|
|
819
|
+
|
|
820
|
+
# Keep tabs sorted by date
|
|
821
|
+
self._reorder_tabs_by_date()
|
|
822
|
+
|
|
823
|
+
def _load_date_into_editor(
|
|
824
|
+
self, date: QDate, editor: MarkdownEditor, extra_data=False
|
|
825
|
+
):
|
|
826
|
+
"""Load a specific date's content into a given editor."""
|
|
827
|
+
date_iso = date.toString("yyyy-MM-dd")
|
|
477
828
|
try:
|
|
478
829
|
text = self.db.get_entry(date_iso)
|
|
479
830
|
if extra_data:
|
|
480
|
-
#
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
text
|
|
486
|
-
self.editor.setHtml(text)
|
|
831
|
+
# Append extra data as markdown
|
|
832
|
+
if text and not text.endswith("\n"):
|
|
833
|
+
text += "\n"
|
|
834
|
+
text += extra_data
|
|
835
|
+
# Force a save now so we don't lose it.
|
|
836
|
+
self._set_editor_markdown_preserve_view(text, editor)
|
|
487
837
|
self._dirty = True
|
|
488
838
|
self._save_date(date_iso, True)
|
|
489
|
-
|
|
490
|
-
print("end")
|
|
491
839
|
except Exception as e:
|
|
492
840
|
QMessageBox.critical(self, "Read Error", str(e))
|
|
493
841
|
return
|
|
494
842
|
|
|
495
|
-
self.editor
|
|
496
|
-
self.editor.setHtml(text)
|
|
497
|
-
self.editor.blockSignals(False)
|
|
498
|
-
|
|
843
|
+
self._set_editor_markdown_preserve_view(text, editor)
|
|
499
844
|
self._dirty = False
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
845
|
+
|
|
846
|
+
def _save_editor_content(self, editor: MarkdownEditor):
|
|
847
|
+
"""Save a specific editor's content to its associated date."""
|
|
848
|
+
if not hasattr(editor, "current_date"):
|
|
849
|
+
return
|
|
850
|
+
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
|
851
|
+
try:
|
|
852
|
+
md = editor.to_markdown()
|
|
853
|
+
self.db.save_new_version(date_iso, md, note="autosave")
|
|
854
|
+
except Exception as e:
|
|
855
|
+
QMessageBox.critical(self, "Save Error", str(e))
|
|
504
856
|
|
|
505
857
|
def _on_text_changed(self):
|
|
506
858
|
self._dirty = True
|
|
@@ -514,7 +866,7 @@ class MainWindow(QMainWindow):
|
|
|
514
866
|
def _adjust_today(self):
|
|
515
867
|
"""Jump to today."""
|
|
516
868
|
today = QDate.currentDate()
|
|
517
|
-
self.
|
|
869
|
+
self._create_new_tab(today)
|
|
518
870
|
|
|
519
871
|
def _load_yesterday_todos(self):
|
|
520
872
|
try:
|
|
@@ -524,39 +876,33 @@ class MainWindow(QMainWindow):
|
|
|
524
876
|
text = self.db.get_entry(yesterday_str)
|
|
525
877
|
unchecked_items = []
|
|
526
878
|
|
|
527
|
-
#
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
879
|
+
# Split into lines and find unchecked checkbox items
|
|
880
|
+
lines = text.split("\n")
|
|
881
|
+
remaining_lines = []
|
|
882
|
+
|
|
883
|
+
for line in lines:
|
|
884
|
+
# Check for unchecked markdown checkboxes: - [ ] or - [☐]
|
|
885
|
+
if re.match(r"^\s*-\s*\[\s*\]\s+", line) or re.match(
|
|
886
|
+
r"^\s*-\s*\[☐\]\s+", line
|
|
887
|
+
):
|
|
888
|
+
# Extract the text after the checkbox
|
|
889
|
+
item_text = re.sub(r"^\s*-\s*\[[\s☐]\]\s+", "", line)
|
|
890
|
+
unchecked_items.append(f"- [ ] {item_text}")
|
|
891
|
+
else:
|
|
892
|
+
# Keep all other lines
|
|
893
|
+
remaining_lines.append(line)
|
|
894
|
+
|
|
895
|
+
# Save modified content back if we moved items
|
|
540
896
|
if unchecked_items:
|
|
541
|
-
|
|
542
|
-
uncheckbox_pattern = re.compile(
|
|
543
|
-
r"<span[^>]*>☐</span>\s*(.*?)</p>", re.DOTALL
|
|
544
|
-
)
|
|
545
|
-
modified_text = re.sub(
|
|
546
|
-
uncheckbox_pattern, "", text
|
|
547
|
-
) # Remove the checkbox lines
|
|
548
|
-
|
|
549
|
-
# Save the modified HTML back to the database
|
|
897
|
+
modified_text = "\n".join(remaining_lines)
|
|
550
898
|
self.db.save_new_version(
|
|
551
899
|
yesterday_str,
|
|
552
900
|
modified_text,
|
|
553
901
|
"Unchecked checkbox items moved to next day",
|
|
554
902
|
)
|
|
555
903
|
|
|
556
|
-
# Join unchecked items into
|
|
557
|
-
unchecked_str = "\n".join(
|
|
558
|
-
[f"<p>{item}</p>" for item in unchecked_items]
|
|
559
|
-
)
|
|
904
|
+
# Join unchecked items into markdown format
|
|
905
|
+
unchecked_str = "\n".join(unchecked_items) + "\n"
|
|
560
906
|
|
|
561
907
|
# Load the unchecked items into the current editor
|
|
562
908
|
self._load_selected_date(False, unchecked_str)
|
|
@@ -569,19 +915,65 @@ class MainWindow(QMainWindow):
|
|
|
569
915
|
def _on_date_changed(self):
|
|
570
916
|
"""
|
|
571
917
|
When the calendar selection changes, save the previous day's note if dirty,
|
|
572
|
-
so we don't lose that text, then load the newly selected day.
|
|
918
|
+
so we don't lose that text, then load the newly selected day into current tab.
|
|
573
919
|
"""
|
|
920
|
+
# Skip if we're showing a context menu (right-click shouldn't load dates)
|
|
921
|
+
if getattr(self, "_showing_context_menu", False):
|
|
922
|
+
return
|
|
923
|
+
|
|
924
|
+
editor = self.current_editor()
|
|
925
|
+
if not editor:
|
|
926
|
+
return
|
|
927
|
+
|
|
574
928
|
# Stop pending autosave and persist current buffer if needed
|
|
575
929
|
try:
|
|
576
930
|
self._save_timer.stop()
|
|
577
931
|
except Exception:
|
|
578
932
|
pass
|
|
579
|
-
prev = getattr(self, "_active_date_iso", None)
|
|
580
|
-
if prev and self._dirty:
|
|
581
|
-
self._save_date(prev, explicit=False)
|
|
582
|
-
# Now load the newly selected date
|
|
583
|
-
self._load_selected_date()
|
|
584
933
|
|
|
934
|
+
# Save the current editor's content if dirty
|
|
935
|
+
if hasattr(editor, "current_date") and self._dirty:
|
|
936
|
+
prev_date_iso = editor.current_date.toString("yyyy-MM-dd")
|
|
937
|
+
self._save_date(prev_date_iso, explicit=False)
|
|
938
|
+
|
|
939
|
+
# Now load the newly selected date into the current tab
|
|
940
|
+
new_date = self.calendar.selectedDate()
|
|
941
|
+
self._load_date_into_editor(new_date, editor)
|
|
942
|
+
editor.current_date = new_date
|
|
943
|
+
|
|
944
|
+
# Update tab title
|
|
945
|
+
current_index = self.tab_widget.currentIndex()
|
|
946
|
+
if current_index >= 0:
|
|
947
|
+
self.tab_widget.setTabText(current_index, new_date.toString("yyyy-MM-dd"))
|
|
948
|
+
|
|
949
|
+
# Keep tabs sorted by date
|
|
950
|
+
self._reorder_tabs_by_date()
|
|
951
|
+
|
|
952
|
+
# ----------- History handler ------------#
|
|
953
|
+
def _open_history(self):
|
|
954
|
+
date_iso = self._current_date_iso()
|
|
955
|
+
dlg = HistoryDialog(self.db, date_iso, self)
|
|
956
|
+
if dlg.exec() == QDialog.Accepted:
|
|
957
|
+
# refresh editor + calendar (head pointer may have changed)
|
|
958
|
+
self._load_selected_date(date_iso)
|
|
959
|
+
self._refresh_calendar_marks()
|
|
960
|
+
|
|
961
|
+
# ----------- Image insert handler ------------#
|
|
962
|
+
def _on_insert_image(self):
|
|
963
|
+
# Let the user pick one or many images
|
|
964
|
+
paths, _ = QFileDialog.getOpenFileNames(
|
|
965
|
+
self,
|
|
966
|
+
"Insert image(s)",
|
|
967
|
+
"",
|
|
968
|
+
"Images (*.png *.jpg *.jpeg *.bmp *.gif *.webp)",
|
|
969
|
+
)
|
|
970
|
+
if not paths:
|
|
971
|
+
return
|
|
972
|
+
# Insert each image
|
|
973
|
+
for path_str in paths:
|
|
974
|
+
self.editor.insert_image_from_path(Path(path_str))
|
|
975
|
+
|
|
976
|
+
# --------------- Database saving of content ---------------- #
|
|
585
977
|
def _save_date(self, date_iso: str, explicit: bool = False, note: str = "autosave"):
|
|
586
978
|
"""
|
|
587
979
|
Save editor contents into the given date. Shows status on success.
|
|
@@ -589,7 +981,7 @@ class MainWindow(QMainWindow):
|
|
|
589
981
|
"""
|
|
590
982
|
if not self._dirty and not explicit:
|
|
591
983
|
return
|
|
592
|
-
text = self.editor.
|
|
984
|
+
text = self.editor.to_markdown()
|
|
593
985
|
try:
|
|
594
986
|
self.db.save_new_version(date_iso, text, note)
|
|
595
987
|
except Exception as e:
|
|
@@ -605,10 +997,16 @@ class MainWindow(QMainWindow):
|
|
|
605
997
|
)
|
|
606
998
|
|
|
607
999
|
def _save_current(self, explicit: bool = False):
|
|
1000
|
+
"""Save the current editor's content."""
|
|
608
1001
|
try:
|
|
609
1002
|
self._save_timer.stop()
|
|
610
1003
|
except Exception:
|
|
611
1004
|
pass
|
|
1005
|
+
|
|
1006
|
+
editor = self.current_editor()
|
|
1007
|
+
if not editor or not hasattr(editor, "current_date"):
|
|
1008
|
+
return
|
|
1009
|
+
|
|
612
1010
|
if explicit:
|
|
613
1011
|
# Prompt for a note
|
|
614
1012
|
dlg = SaveDialog(self)
|
|
@@ -617,33 +1015,14 @@ class MainWindow(QMainWindow):
|
|
|
617
1015
|
note = dlg.note_text()
|
|
618
1016
|
else:
|
|
619
1017
|
note = "autosave"
|
|
620
|
-
#
|
|
621
|
-
|
|
1018
|
+
# Save the current editor's date
|
|
1019
|
+
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
|
1020
|
+
self._save_date(date_iso, explicit, note)
|
|
622
1021
|
try:
|
|
623
1022
|
self._save_timer.start()
|
|
624
1023
|
except Exception:
|
|
625
1024
|
pass
|
|
626
1025
|
|
|
627
|
-
def _open_history(self):
|
|
628
|
-
date_iso = self._current_date_iso()
|
|
629
|
-
dlg = HistoryDialog(self.db, date_iso, self)
|
|
630
|
-
if dlg.exec() == QDialog.Accepted:
|
|
631
|
-
# refresh editor + calendar (head pointer may have changed)
|
|
632
|
-
self._load_selected_date(date_iso)
|
|
633
|
-
self._refresh_calendar_marks()
|
|
634
|
-
|
|
635
|
-
def _on_insert_image(self):
|
|
636
|
-
# Let the user pick one or many images
|
|
637
|
-
paths, _ = QFileDialog.getOpenFileNames(
|
|
638
|
-
self,
|
|
639
|
-
"Insert image(s)",
|
|
640
|
-
"",
|
|
641
|
-
"Images (*.png *.jpg *.jpeg *.bmp *.gif *.webp)",
|
|
642
|
-
)
|
|
643
|
-
if not paths:
|
|
644
|
-
return
|
|
645
|
-
self.editor.insert_images(paths) # call into the editor
|
|
646
|
-
|
|
647
1026
|
# ----------- Settings handler ------------#
|
|
648
1027
|
def _open_settings(self):
|
|
649
1028
|
dlg = SettingsDialog(self.cfg, self.db, self)
|
|
@@ -708,7 +1087,7 @@ class MainWindow(QMainWindow):
|
|
|
708
1087
|
QGuiApplication.screenAt(QCursor.pos()) or QGuiApplication.primaryScreen()
|
|
709
1088
|
)
|
|
710
1089
|
r = screen.availableGeometry()
|
|
711
|
-
# Center the window in that screen
|
|
1090
|
+
# Center the window in that screen's available area
|
|
712
1091
|
self.move(r.center() - self.rect().center())
|
|
713
1092
|
|
|
714
1093
|
# ----------------- Export handler ----------------- #
|
|
@@ -775,7 +1154,7 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
775
1154
|
elif selected_filter.startswith("SQL"):
|
|
776
1155
|
self.db.export_sql(filename)
|
|
777
1156
|
else:
|
|
778
|
-
|
|
1157
|
+
raise ValueError("Unrecognised extension!")
|
|
779
1158
|
|
|
780
1159
|
QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}")
|
|
781
1160
|
except Exception as e:
|
|
@@ -837,7 +1216,7 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
837
1216
|
return
|
|
838
1217
|
if minutes == 0:
|
|
839
1218
|
self._idle_timer.stop()
|
|
840
|
-
# If
|
|
1219
|
+
# If currently locked, unlock when user disables the timer:
|
|
841
1220
|
if getattr(self, "_locked", False):
|
|
842
1221
|
try:
|
|
843
1222
|
self._locked = False
|
|
@@ -851,11 +1230,27 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
851
1230
|
self._idle_timer.start()
|
|
852
1231
|
|
|
853
1232
|
def eventFilter(self, obj, event):
|
|
1233
|
+
# Catch right-clicks on calendar BEFORE selectionChanged can fire
|
|
1234
|
+
if obj == self.calendar and event.type() == QEvent.MouseButtonPress:
|
|
1235
|
+
try:
|
|
1236
|
+
# QMouseEvent in PySide6
|
|
1237
|
+
if event.button() == Qt.RightButton:
|
|
1238
|
+
self._showing_context_menu = True
|
|
1239
|
+
except Exception:
|
|
1240
|
+
pass
|
|
1241
|
+
|
|
854
1242
|
if event.type() == QEvent.KeyPress and not self._locked:
|
|
855
1243
|
self._idle_timer.start()
|
|
1244
|
+
|
|
1245
|
+
if event.type() in (QEvent.ApplicationActivate, QEvent.WindowActivate):
|
|
1246
|
+
QTimer.singleShot(0, self._focus_editor_now)
|
|
1247
|
+
|
|
856
1248
|
return super().eventFilter(obj, event)
|
|
857
1249
|
|
|
858
1250
|
def _enter_lock(self):
|
|
1251
|
+
"""
|
|
1252
|
+
Trigger the lock overlay and disable widgets
|
|
1253
|
+
"""
|
|
859
1254
|
if self._locked:
|
|
860
1255
|
return
|
|
861
1256
|
self._locked = True
|
|
@@ -871,6 +1266,10 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
871
1266
|
|
|
872
1267
|
@Slot()
|
|
873
1268
|
def _on_unlock_clicked(self):
|
|
1269
|
+
"""
|
|
1270
|
+
Prompt for key to unlock screen
|
|
1271
|
+
If successful, re-enable widgets
|
|
1272
|
+
"""
|
|
874
1273
|
try:
|
|
875
1274
|
ok = self._prompt_for_key_until_valid(first_time=False)
|
|
876
1275
|
except Exception as e:
|
|
@@ -887,6 +1286,7 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
887
1286
|
if tb:
|
|
888
1287
|
tb.setEnabled(True)
|
|
889
1288
|
self._idle_timer.start()
|
|
1289
|
+
QTimer.singleShot(0, self._focus_editor_now)
|
|
890
1290
|
|
|
891
1291
|
# ----------------- Close handlers ----------------- #
|
|
892
1292
|
def closeEvent(self, event):
|
|
@@ -896,9 +1296,88 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
896
1296
|
self.settings.setValue("main/windowState", self.saveState())
|
|
897
1297
|
self.settings.setValue("main/maximized", self.isMaximized())
|
|
898
1298
|
|
|
899
|
-
# Ensure we save
|
|
900
|
-
self.
|
|
1299
|
+
# Ensure we save all tabs before closing
|
|
1300
|
+
for i in range(self.tab_widget.count()):
|
|
1301
|
+
editor = self.tab_widget.widget(i)
|
|
1302
|
+
if editor:
|
|
1303
|
+
self._save_editor_content(editor)
|
|
901
1304
|
self.db.close()
|
|
902
1305
|
except Exception:
|
|
903
1306
|
pass
|
|
904
1307
|
super().closeEvent(event)
|
|
1308
|
+
|
|
1309
|
+
# ----------------- Below logic helps focus the editor ----------------- #
|
|
1310
|
+
|
|
1311
|
+
def _focus_editor_now(self):
|
|
1312
|
+
"""Give focus to the editor and ensure the caret is visible."""
|
|
1313
|
+
if getattr(self, "_locked", False):
|
|
1314
|
+
return
|
|
1315
|
+
if not self.isActiveWindow():
|
|
1316
|
+
return
|
|
1317
|
+
editor = self.current_editor()
|
|
1318
|
+
if not editor:
|
|
1319
|
+
return
|
|
1320
|
+
# Belt-and-suspenders: do it now and once more on the next tick
|
|
1321
|
+
editor.setFocus(Qt.ActiveWindowFocusReason)
|
|
1322
|
+
editor.ensureCursorVisible()
|
|
1323
|
+
QTimer.singleShot(
|
|
1324
|
+
0,
|
|
1325
|
+
lambda: (
|
|
1326
|
+
editor.setFocus(Qt.ActiveWindowFocusReason) if editor else None,
|
|
1327
|
+
editor.ensureCursorVisible() if editor else None,
|
|
1328
|
+
),
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
def _on_app_state_changed(self, state):
|
|
1332
|
+
# Called on macOS/Wayland/Windows when the whole app re-activates
|
|
1333
|
+
if state == Qt.ApplicationActive and self.isActiveWindow():
|
|
1334
|
+
QTimer.singleShot(0, self._focus_editor_now)
|
|
1335
|
+
|
|
1336
|
+
def changeEvent(self, ev):
|
|
1337
|
+
# Called on some platforms when the window's activation state flips
|
|
1338
|
+
super().changeEvent(ev)
|
|
1339
|
+
if ev.type() == QEvent.ActivationChange and self.isActiveWindow():
|
|
1340
|
+
QTimer.singleShot(0, self._focus_editor_now)
|
|
1341
|
+
|
|
1342
|
+
def _set_editor_markdown_preserve_view(
|
|
1343
|
+
self, markdown: str, editor: MarkdownEditor | None = None
|
|
1344
|
+
):
|
|
1345
|
+
if editor is None:
|
|
1346
|
+
editor = self.current_editor()
|
|
1347
|
+
if not editor:
|
|
1348
|
+
return
|
|
1349
|
+
|
|
1350
|
+
ed = editor
|
|
1351
|
+
|
|
1352
|
+
# Save caret/selection and scroll
|
|
1353
|
+
cur = ed.textCursor()
|
|
1354
|
+
old_pos, old_anchor = cur.position(), cur.anchor()
|
|
1355
|
+
v = ed.verticalScrollBar().value()
|
|
1356
|
+
h = ed.horizontalScrollBar().value()
|
|
1357
|
+
|
|
1358
|
+
# Only touch the doc if it actually changed
|
|
1359
|
+
ed.blockSignals(True)
|
|
1360
|
+
if ed.to_markdown() != markdown:
|
|
1361
|
+
ed.from_markdown(markdown)
|
|
1362
|
+
ed.blockSignals(False)
|
|
1363
|
+
|
|
1364
|
+
# Restore scroll first
|
|
1365
|
+
ed.verticalScrollBar().setValue(v)
|
|
1366
|
+
ed.horizontalScrollBar().setValue(h)
|
|
1367
|
+
|
|
1368
|
+
# Restore caret/selection (bounded to new doc length)
|
|
1369
|
+
doc_length = ed.document().characterCount() - 1
|
|
1370
|
+
old_pos = min(old_pos, doc_length)
|
|
1371
|
+
old_anchor = min(old_anchor, doc_length)
|
|
1372
|
+
|
|
1373
|
+
cur = ed.textCursor()
|
|
1374
|
+
cur.setPosition(old_anchor)
|
|
1375
|
+
mode = (
|
|
1376
|
+
QTextCursor.KeepAnchor if old_anchor != old_pos else QTextCursor.MoveAnchor
|
|
1377
|
+
)
|
|
1378
|
+
cur.setPosition(old_pos, mode)
|
|
1379
|
+
ed.setTextCursor(cur)
|
|
1380
|
+
|
|
1381
|
+
# Refresh highlights if the theme changed
|
|
1382
|
+
if hasattr(self, "findBar"):
|
|
1383
|
+
self.findBar.refresh()
|