bouquin 0.1.12__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 +4 -41
- bouquin/find_bar.py +33 -11
- bouquin/history_dialog.py +27 -26
- bouquin/main_window.py +495 -120
- bouquin/markdown_editor.py +813 -0
- bouquin/search.py +46 -30
- bouquin/toolbar.py +0 -42
- {bouquin-0.1.12.dist-info → bouquin-0.2.1.2.dist-info}/METADATA +5 -7
- bouquin-0.2.1.2.dist-info/RECORD +21 -0
- bouquin/editor.py +0 -1009
- bouquin-0.1.12.dist-info/RECORD +0 -21
- {bouquin-0.1.12.dist-info → bouquin-0.2.1.2.dist-info}/LICENSE +0 -0
- {bouquin-0.1.12.dist-info → bouquin-0.2.1.2.dist-info}/WHEEL +0 -0
- {bouquin-0.1.12.dist-info → bouquin-0.2.1.2.dist-info}/entry_points.txt +0 -0
bouquin/main_window.py
CHANGED
|
@@ -36,15 +36,18 @@ from PySide6.QtWidgets import (
|
|
|
36
36
|
QDialog,
|
|
37
37
|
QFileDialog,
|
|
38
38
|
QMainWindow,
|
|
39
|
+
QMenu,
|
|
39
40
|
QMessageBox,
|
|
40
41
|
QSizePolicy,
|
|
41
42
|
QSplitter,
|
|
43
|
+
QTableView,
|
|
44
|
+
QTabWidget,
|
|
42
45
|
QVBoxLayout,
|
|
43
46
|
QWidget,
|
|
44
47
|
)
|
|
45
48
|
|
|
46
49
|
from .db import DBManager
|
|
47
|
-
from .
|
|
50
|
+
from .markdown_editor import MarkdownEditor
|
|
48
51
|
from .find_bar import FindBar
|
|
49
52
|
from .history_dialog import HistoryDialog
|
|
50
53
|
from .key_prompt import KeyPrompt
|
|
@@ -98,34 +101,37 @@ class MainWindow(QMainWindow):
|
|
|
98
101
|
left_layout.addWidget(self.search)
|
|
99
102
|
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
|
|
100
103
|
|
|
101
|
-
#
|
|
102
|
-
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)
|
|
103
109
|
|
|
104
110
|
# Toolbar for controlling styling
|
|
105
111
|
self.toolBar = ToolBar()
|
|
106
112
|
self.addToolBar(self.toolBar)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self.
|
|
111
|
-
self.toolBar.strikeRequested.connect(self.editor.apply_strikethrough)
|
|
112
|
-
self.toolBar.codeRequested.connect(self.editor.apply_code)
|
|
113
|
-
self.toolBar.headingRequested.connect(self.editor.apply_heading)
|
|
114
|
-
self.toolBar.bulletsRequested.connect(self.editor.toggle_bullets)
|
|
115
|
-
self.toolBar.numbersRequested.connect(self.editor.toggle_numbers)
|
|
116
|
-
self.toolBar.checkboxesRequested.connect(self.editor.toggle_checkboxes)
|
|
117
|
-
self.toolBar.alignRequested.connect(self.editor.setAlignment)
|
|
118
|
-
self.toolBar.historyRequested.connect(self._open_history)
|
|
119
|
-
self.toolBar.insertImageRequested.connect(self._on_insert_image)
|
|
120
|
-
|
|
121
|
-
self.editor.currentCharFormatChanged.connect(lambda _f: self._sync_toolbar())
|
|
122
|
-
self.editor.cursorPositionChanged.connect(self._sync_toolbar)
|
|
113
|
+
self._bind_toolbar()
|
|
114
|
+
|
|
115
|
+
# Create the first editor tab
|
|
116
|
+
self._create_new_tab()
|
|
123
117
|
|
|
124
118
|
split = QSplitter()
|
|
125
119
|
split.addWidget(left_panel)
|
|
126
|
-
split.addWidget(self.
|
|
120
|
+
split.addWidget(self.tab_widget)
|
|
127
121
|
split.setStretchFactor(1, 1)
|
|
128
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)
|
|
134
|
+
|
|
129
135
|
container = QWidget()
|
|
130
136
|
lay = QVBoxLayout(container)
|
|
131
137
|
lay.addWidget(split)
|
|
@@ -162,7 +168,10 @@ class MainWindow(QMainWindow):
|
|
|
162
168
|
# Status bar for feedback
|
|
163
169
|
self.statusBar().showMessage("Ready", 800)
|
|
164
170
|
# Add findBar and add it to the statusBar
|
|
165
|
-
|
|
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
|
+
)
|
|
166
175
|
self.statusBar().addPermanentWidget(self.findBar)
|
|
167
176
|
# When the findBar closes, put the caret back in the editor
|
|
168
177
|
self.findBar.closed.connect(self._focus_editor_now)
|
|
@@ -258,7 +267,7 @@ class MainWindow(QMainWindow):
|
|
|
258
267
|
self._save_timer = QTimer(self)
|
|
259
268
|
self._save_timer.setSingleShot(True)
|
|
260
269
|
self._save_timer.timeout.connect(self._save_current)
|
|
261
|
-
|
|
270
|
+
# Note: textChanged will be connected per-editor in _create_new_tab
|
|
262
271
|
|
|
263
272
|
# First load + mark dates in calendar with content
|
|
264
273
|
if not self._load_yesterday_todos():
|
|
@@ -313,6 +322,277 @@ class MainWindow(QMainWindow):
|
|
|
313
322
|
if self._try_connect():
|
|
314
323
|
return True
|
|
315
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 -------------------#
|
|
316
596
|
def _retheme_overrides(self):
|
|
317
597
|
if hasattr(self, "_lock_overlay"):
|
|
318
598
|
self._lock_overlay._apply_overlay_style()
|
|
@@ -447,20 +727,47 @@ class MainWindow(QMainWindow):
|
|
|
447
727
|
|
|
448
728
|
# --- UI handlers ---------------------------------------------------------
|
|
449
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
|
+
|
|
450
760
|
def _sync_toolbar(self):
|
|
451
761
|
fmt = self.editor.currentCharFormat()
|
|
452
762
|
c = self.editor.textCursor()
|
|
453
|
-
bf = c.blockFormat()
|
|
454
763
|
|
|
455
764
|
# Block signals so setChecked() doesn't re-trigger actions
|
|
456
765
|
QSignalBlocker(self.toolBar.actBold)
|
|
457
766
|
QSignalBlocker(self.toolBar.actItalic)
|
|
458
|
-
QSignalBlocker(self.toolBar.actUnderline)
|
|
459
767
|
QSignalBlocker(self.toolBar.actStrike)
|
|
460
768
|
|
|
461
769
|
self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold)
|
|
462
770
|
self.toolBar.actItalic.setChecked(fmt.fontItalic())
|
|
463
|
-
self.toolBar.actUnderline.setChecked(fmt.fontUnderline())
|
|
464
771
|
self.toolBar.actStrike.setChecked(fmt.fontStrikeOut())
|
|
465
772
|
|
|
466
773
|
# Headings: decide which to check by current point size
|
|
@@ -492,47 +799,60 @@ class MainWindow(QMainWindow):
|
|
|
492
799
|
self.toolBar.actBullets.setChecked(bool(bullets_on))
|
|
493
800
|
self.toolBar.actNumbers.setChecked(bool(numbers_on))
|
|
494
801
|
|
|
495
|
-
# Alignment
|
|
496
|
-
align = bf.alignment() & Qt.AlignHorizontal_Mask
|
|
497
|
-
QSignalBlocker(self.toolBar.actAlignL)
|
|
498
|
-
self.toolBar.actAlignL.setChecked(align == Qt.AlignLeft)
|
|
499
|
-
QSignalBlocker(self.toolBar.actAlignC)
|
|
500
|
-
self.toolBar.actAlignC.setChecked(align == Qt.AlignHCenter)
|
|
501
|
-
QSignalBlocker(self.toolBar.actAlignR)
|
|
502
|
-
self.toolBar.actAlignR.setChecked(align == Qt.AlignRight)
|
|
503
|
-
|
|
504
|
-
def _current_date_iso(self) -> str:
|
|
505
|
-
d = self.calendar.selectedDate()
|
|
506
|
-
return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
|
|
507
|
-
|
|
508
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
|
+
|
|
509
808
|
if not date_iso:
|
|
510
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")
|
|
511
828
|
try:
|
|
512
829
|
text = self.db.get_entry(date_iso)
|
|
513
830
|
if extra_data:
|
|
514
|
-
#
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
modified = re.sub(r"(<\/body><\/html>)", extra_data_html + r"\1", text)
|
|
519
|
-
text = modified
|
|
831
|
+
# Append extra data as markdown
|
|
832
|
+
if text and not text.endswith("\n"):
|
|
833
|
+
text += "\n"
|
|
834
|
+
text += extra_data
|
|
520
835
|
# Force a save now so we don't lose it.
|
|
521
|
-
self.
|
|
836
|
+
self._set_editor_markdown_preserve_view(text, editor)
|
|
522
837
|
self._dirty = True
|
|
523
838
|
self._save_date(date_iso, True)
|
|
524
|
-
|
|
525
839
|
except Exception as e:
|
|
526
840
|
QMessageBox.critical(self, "Read Error", str(e))
|
|
527
841
|
return
|
|
528
842
|
|
|
529
|
-
self.
|
|
530
|
-
|
|
843
|
+
self._set_editor_markdown_preserve_view(text, editor)
|
|
531
844
|
self._dirty = False
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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))
|
|
536
856
|
|
|
537
857
|
def _on_text_changed(self):
|
|
538
858
|
self._dirty = True
|
|
@@ -546,7 +866,7 @@ class MainWindow(QMainWindow):
|
|
|
546
866
|
def _adjust_today(self):
|
|
547
867
|
"""Jump to today."""
|
|
548
868
|
today = QDate.currentDate()
|
|
549
|
-
self.
|
|
869
|
+
self._create_new_tab(today)
|
|
550
870
|
|
|
551
871
|
def _load_yesterday_todos(self):
|
|
552
872
|
try:
|
|
@@ -556,39 +876,33 @@ class MainWindow(QMainWindow):
|
|
|
556
876
|
text = self.db.get_entry(yesterday_str)
|
|
557
877
|
unchecked_items = []
|
|
558
878
|
|
|
559
|
-
#
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
572
896
|
if unchecked_items:
|
|
573
|
-
|
|
574
|
-
uncheckbox_pattern = re.compile(
|
|
575
|
-
r"<span[^>]*>☐</span>\s*(.*?)</p>", re.DOTALL
|
|
576
|
-
)
|
|
577
|
-
modified_text = re.sub(
|
|
578
|
-
uncheckbox_pattern, "", text
|
|
579
|
-
) # Remove the checkbox lines
|
|
580
|
-
|
|
581
|
-
# Save the modified HTML back to the database
|
|
897
|
+
modified_text = "\n".join(remaining_lines)
|
|
582
898
|
self.db.save_new_version(
|
|
583
899
|
yesterday_str,
|
|
584
900
|
modified_text,
|
|
585
901
|
"Unchecked checkbox items moved to next day",
|
|
586
902
|
)
|
|
587
903
|
|
|
588
|
-
# Join unchecked items into
|
|
589
|
-
unchecked_str = "\n".join(
|
|
590
|
-
[f"<p>{item}</p>" for item in unchecked_items]
|
|
591
|
-
)
|
|
904
|
+
# Join unchecked items into markdown format
|
|
905
|
+
unchecked_str = "\n".join(unchecked_items) + "\n"
|
|
592
906
|
|
|
593
907
|
# Load the unchecked items into the current editor
|
|
594
908
|
self._load_selected_date(False, unchecked_str)
|
|
@@ -601,19 +915,65 @@ class MainWindow(QMainWindow):
|
|
|
601
915
|
def _on_date_changed(self):
|
|
602
916
|
"""
|
|
603
917
|
When the calendar selection changes, save the previous day's note if dirty,
|
|
604
|
-
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.
|
|
605
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
|
+
|
|
606
928
|
# Stop pending autosave and persist current buffer if needed
|
|
607
929
|
try:
|
|
608
930
|
self._save_timer.stop()
|
|
609
931
|
except Exception:
|
|
610
932
|
pass
|
|
611
|
-
prev = getattr(self, "_active_date_iso", None)
|
|
612
|
-
if prev and self._dirty:
|
|
613
|
-
self._save_date(prev, explicit=False)
|
|
614
|
-
# Now load the newly selected date
|
|
615
|
-
self._load_selected_date()
|
|
616
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 ---------------- #
|
|
617
977
|
def _save_date(self, date_iso: str, explicit: bool = False, note: str = "autosave"):
|
|
618
978
|
"""
|
|
619
979
|
Save editor contents into the given date. Shows status on success.
|
|
@@ -621,7 +981,7 @@ class MainWindow(QMainWindow):
|
|
|
621
981
|
"""
|
|
622
982
|
if not self._dirty and not explicit:
|
|
623
983
|
return
|
|
624
|
-
text = self.editor.
|
|
984
|
+
text = self.editor.to_markdown()
|
|
625
985
|
try:
|
|
626
986
|
self.db.save_new_version(date_iso, text, note)
|
|
627
987
|
except Exception as e:
|
|
@@ -637,10 +997,16 @@ class MainWindow(QMainWindow):
|
|
|
637
997
|
)
|
|
638
998
|
|
|
639
999
|
def _save_current(self, explicit: bool = False):
|
|
1000
|
+
"""Save the current editor's content."""
|
|
640
1001
|
try:
|
|
641
1002
|
self._save_timer.stop()
|
|
642
1003
|
except Exception:
|
|
643
1004
|
pass
|
|
1005
|
+
|
|
1006
|
+
editor = self.current_editor()
|
|
1007
|
+
if not editor or not hasattr(editor, "current_date"):
|
|
1008
|
+
return
|
|
1009
|
+
|
|
644
1010
|
if explicit:
|
|
645
1011
|
# Prompt for a note
|
|
646
1012
|
dlg = SaveDialog(self)
|
|
@@ -649,33 +1015,14 @@ class MainWindow(QMainWindow):
|
|
|
649
1015
|
note = dlg.note_text()
|
|
650
1016
|
else:
|
|
651
1017
|
note = "autosave"
|
|
652
|
-
#
|
|
653
|
-
|
|
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)
|
|
654
1021
|
try:
|
|
655
1022
|
self._save_timer.start()
|
|
656
1023
|
except Exception:
|
|
657
1024
|
pass
|
|
658
1025
|
|
|
659
|
-
def _open_history(self):
|
|
660
|
-
date_iso = self._current_date_iso()
|
|
661
|
-
dlg = HistoryDialog(self.db, date_iso, self)
|
|
662
|
-
if dlg.exec() == QDialog.Accepted:
|
|
663
|
-
# refresh editor + calendar (head pointer may have changed)
|
|
664
|
-
self._load_selected_date(date_iso)
|
|
665
|
-
self._refresh_calendar_marks()
|
|
666
|
-
|
|
667
|
-
def _on_insert_image(self):
|
|
668
|
-
# Let the user pick one or many images
|
|
669
|
-
paths, _ = QFileDialog.getOpenFileNames(
|
|
670
|
-
self,
|
|
671
|
-
"Insert image(s)",
|
|
672
|
-
"",
|
|
673
|
-
"Images (*.png *.jpg *.jpeg *.bmp *.gif *.webp)",
|
|
674
|
-
)
|
|
675
|
-
if not paths:
|
|
676
|
-
return
|
|
677
|
-
self.editor.insert_images(paths) # call into the editor
|
|
678
|
-
|
|
679
1026
|
# ----------- Settings handler ------------#
|
|
680
1027
|
def _open_settings(self):
|
|
681
1028
|
dlg = SettingsDialog(self.cfg, self.db, self)
|
|
@@ -807,7 +1154,7 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
807
1154
|
elif selected_filter.startswith("SQL"):
|
|
808
1155
|
self.db.export_sql(filename)
|
|
809
1156
|
else:
|
|
810
|
-
|
|
1157
|
+
raise ValueError("Unrecognised extension!")
|
|
811
1158
|
|
|
812
1159
|
QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}")
|
|
813
1160
|
except Exception as e:
|
|
@@ -883,10 +1230,21 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
883
1230
|
self._idle_timer.start()
|
|
884
1231
|
|
|
885
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
|
+
|
|
886
1242
|
if event.type() == QEvent.KeyPress and not self._locked:
|
|
887
1243
|
self._idle_timer.start()
|
|
1244
|
+
|
|
888
1245
|
if event.type() in (QEvent.ApplicationActivate, QEvent.WindowActivate):
|
|
889
1246
|
QTimer.singleShot(0, self._focus_editor_now)
|
|
1247
|
+
|
|
890
1248
|
return super().eventFilter(obj, event)
|
|
891
1249
|
|
|
892
1250
|
def _enter_lock(self):
|
|
@@ -938,8 +1296,11 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
938
1296
|
self.settings.setValue("main/windowState", self.saveState())
|
|
939
1297
|
self.settings.setValue("main/maximized", self.isMaximized())
|
|
940
1298
|
|
|
941
|
-
# Ensure we save
|
|
942
|
-
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)
|
|
943
1304
|
self.db.close()
|
|
944
1305
|
except Exception:
|
|
945
1306
|
pass
|
|
@@ -953,14 +1314,17 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
953
1314
|
return
|
|
954
1315
|
if not self.isActiveWindow():
|
|
955
1316
|
return
|
|
1317
|
+
editor = self.current_editor()
|
|
1318
|
+
if not editor:
|
|
1319
|
+
return
|
|
956
1320
|
# Belt-and-suspenders: do it now and once more on the next tick
|
|
957
|
-
|
|
958
|
-
|
|
1321
|
+
editor.setFocus(Qt.ActiveWindowFocusReason)
|
|
1322
|
+
editor.ensureCursorVisible()
|
|
959
1323
|
QTimer.singleShot(
|
|
960
1324
|
0,
|
|
961
1325
|
lambda: (
|
|
962
|
-
|
|
963
|
-
|
|
1326
|
+
editor.setFocus(Qt.ActiveWindowFocusReason) if editor else None,
|
|
1327
|
+
editor.ensureCursorVisible() if editor else None,
|
|
964
1328
|
),
|
|
965
1329
|
)
|
|
966
1330
|
|
|
@@ -975,8 +1339,15 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
975
1339
|
if ev.type() == QEvent.ActivationChange and self.isActiveWindow():
|
|
976
1340
|
QTimer.singleShot(0, self._focus_editor_now)
|
|
977
1341
|
|
|
978
|
-
def
|
|
979
|
-
|
|
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
|
|
980
1351
|
|
|
981
1352
|
# Save caret/selection and scroll
|
|
982
1353
|
cur = ed.textCursor()
|
|
@@ -986,15 +1357,19 @@ If you want an encrypted backup, choose Backup instead of Export.
|
|
|
986
1357
|
|
|
987
1358
|
# Only touch the doc if it actually changed
|
|
988
1359
|
ed.blockSignals(True)
|
|
989
|
-
if ed.
|
|
990
|
-
ed.
|
|
1360
|
+
if ed.to_markdown() != markdown:
|
|
1361
|
+
ed.from_markdown(markdown)
|
|
991
1362
|
ed.blockSignals(False)
|
|
992
1363
|
|
|
993
1364
|
# Restore scroll first
|
|
994
1365
|
ed.verticalScrollBar().setValue(v)
|
|
995
1366
|
ed.horizontalScrollBar().setValue(h)
|
|
996
1367
|
|
|
997
|
-
# Restore caret/selection
|
|
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
|
+
|
|
998
1373
|
cur = ed.textCursor()
|
|
999
1374
|
cur.setPosition(old_anchor)
|
|
1000
1375
|
mode = (
|