bouquin 0.1.5__py3-none-any.whl → 0.1.9__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.
Potentially problematic release.
This version of bouquin might be problematic. Click here for more details.
- bouquin/db.py +31 -2
- bouquin/editor.py +482 -92
- bouquin/history_dialog.py +12 -15
- bouquin/main_window.py +158 -17
- bouquin/settings_dialog.py +58 -9
- bouquin/toolbar.py +80 -21
- {bouquin-0.1.5.dist-info → bouquin-0.1.9.dist-info}/METADATA +7 -7
- bouquin-0.1.9.dist-info/RECORD +18 -0
- bouquin-0.1.5.dist-info/RECORD +0 -18
- {bouquin-0.1.5.dist-info → bouquin-0.1.9.dist-info}/LICENSE +0 -0
- {bouquin-0.1.5.dist-info → bouquin-0.1.9.dist-info}/WHEEL +0 -0
- {bouquin-0.1.5.dist-info → bouquin-0.1.9.dist-info}/entry_points.txt +0 -0
bouquin/history_dialog.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import difflib, re, html as _html
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from PySide6.QtCore import Qt, Slot
|
|
5
6
|
from PySide6.QtWidgets import (
|
|
6
7
|
QDialog,
|
|
@@ -77,14 +78,14 @@ class HistoryDialog(QDialog):
|
|
|
77
78
|
self.list.currentItemChanged.connect(self._on_select)
|
|
78
79
|
top.addWidget(self.list, 1)
|
|
79
80
|
|
|
80
|
-
# Right: tabs (Preview / Diff
|
|
81
|
+
# Right: tabs (Preview / Diff)
|
|
81
82
|
self.tabs = QTabWidget()
|
|
82
83
|
self.preview = QTextBrowser()
|
|
83
84
|
self.preview.setOpenExternalLinks(True)
|
|
84
85
|
self.diff = QTextBrowser()
|
|
85
86
|
self.diff.setOpenExternalLinks(False)
|
|
86
87
|
self.tabs.addTab(self.preview, "Preview")
|
|
87
|
-
self.tabs.addTab(self.diff, "Diff
|
|
88
|
+
self.tabs.addTab(self.diff, "Diff")
|
|
88
89
|
self.tabs.setMinimumSize(500, 650)
|
|
89
90
|
top.addWidget(self.tabs, 2)
|
|
90
91
|
|
|
@@ -104,6 +105,14 @@ class HistoryDialog(QDialog):
|
|
|
104
105
|
self._load_versions()
|
|
105
106
|
|
|
106
107
|
# --- Data/UX helpers ---
|
|
108
|
+
def _fmt_local(self, iso_utc: str) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Convert UTC in the database to user's local tz
|
|
111
|
+
"""
|
|
112
|
+
dt = datetime.fromisoformat(iso_utc.replace("Z", "+00:00"))
|
|
113
|
+
local = dt.astimezone()
|
|
114
|
+
return local.strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
115
|
+
|
|
107
116
|
def _load_versions(self):
|
|
108
117
|
self._versions = self._db.list_versions(
|
|
109
118
|
self._date
|
|
@@ -113,7 +122,7 @@ class HistoryDialog(QDialog):
|
|
|
113
122
|
)
|
|
114
123
|
self.list.clear()
|
|
115
124
|
for v in self._versions:
|
|
116
|
-
label = f"v{v['version_no']} — {v['created_at']}"
|
|
125
|
+
label = f"v{v['version_no']} — {self._fmt_local(v['created_at'])}"
|
|
117
126
|
if v.get("note"):
|
|
118
127
|
label += f" · {v['note']}"
|
|
119
128
|
if v["is_current"]:
|
|
@@ -158,22 +167,10 @@ class HistoryDialog(QDialog):
|
|
|
158
167
|
return
|
|
159
168
|
sel = self._db.get_version(version_id=sel_id)
|
|
160
169
|
vno = sel["version_no"]
|
|
161
|
-
# Confirm
|
|
162
|
-
if (
|
|
163
|
-
QMessageBox.question(
|
|
164
|
-
self,
|
|
165
|
-
"Revert",
|
|
166
|
-
f"Revert {self._date} to version v{vno}?\n\nYou can always change your mind later.",
|
|
167
|
-
QMessageBox.Yes | QMessageBox.No,
|
|
168
|
-
)
|
|
169
|
-
!= QMessageBox.Yes
|
|
170
|
-
):
|
|
171
|
-
return
|
|
172
170
|
# Flip head pointer
|
|
173
171
|
try:
|
|
174
172
|
self._db.revert_to_version(self._date, version_id=sel_id)
|
|
175
173
|
except Exception as e:
|
|
176
174
|
QMessageBox.critical(self, "Revert failed", str(e))
|
|
177
175
|
return
|
|
178
|
-
QMessageBox.information(self, "Reverted", f"{self._date} is now at v{vno}.")
|
|
179
176
|
self.accept() # let the caller refresh the editor
|
bouquin/main_window.py
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from PySide6.QtCore import
|
|
8
|
+
from PySide6.QtCore import (
|
|
9
|
+
QDate,
|
|
10
|
+
QTimer,
|
|
11
|
+
Qt,
|
|
12
|
+
QSettings,
|
|
13
|
+
Slot,
|
|
14
|
+
QUrl,
|
|
15
|
+
QEvent,
|
|
16
|
+
QSignalBlocker,
|
|
17
|
+
)
|
|
8
18
|
from PySide6.QtGui import (
|
|
9
19
|
QAction,
|
|
10
20
|
QCursor,
|
|
@@ -12,6 +22,7 @@ from PySide6.QtGui import (
|
|
|
12
22
|
QFont,
|
|
13
23
|
QGuiApplication,
|
|
14
24
|
QTextCharFormat,
|
|
25
|
+
QTextListFormat,
|
|
15
26
|
)
|
|
16
27
|
from PySide6.QtWidgets import (
|
|
17
28
|
QCalendarWidget,
|
|
@@ -94,8 +105,8 @@ class _LockOverlay(QWidget):
|
|
|
94
105
|
|
|
95
106
|
|
|
96
107
|
class MainWindow(QMainWindow):
|
|
97
|
-
def __init__(self):
|
|
98
|
-
super().__init__()
|
|
108
|
+
def __init__(self, *args, **kwargs):
|
|
109
|
+
super().__init__(*args, **kwargs)
|
|
99
110
|
self.setWindowTitle(APP_NAME)
|
|
100
111
|
self.setMinimumSize(1000, 650)
|
|
101
112
|
|
|
@@ -149,6 +160,10 @@ class MainWindow(QMainWindow):
|
|
|
149
160
|
self.toolBar.numbersRequested.connect(self.editor.toggle_numbers)
|
|
150
161
|
self.toolBar.alignRequested.connect(self.editor.setAlignment)
|
|
151
162
|
self.toolBar.historyRequested.connect(self._open_history)
|
|
163
|
+
self.toolBar.insertImageRequested.connect(self._on_insert_image)
|
|
164
|
+
|
|
165
|
+
self.editor.currentCharFormatChanged.connect(lambda _f: self._sync_toolbar())
|
|
166
|
+
self.editor.cursorPositionChanged.connect(self._sync_toolbar)
|
|
152
167
|
|
|
153
168
|
split = QSplitter()
|
|
154
169
|
split.addWidget(left_panel)
|
|
@@ -201,6 +216,10 @@ class MainWindow(QMainWindow):
|
|
|
201
216
|
act_export.setShortcut("Ctrl+E")
|
|
202
217
|
act_export.triggered.connect(self._export)
|
|
203
218
|
file_menu.addAction(act_export)
|
|
219
|
+
act_backup = QAction("&Backup", self)
|
|
220
|
+
act_backup.setShortcut("Ctrl+Shift+B")
|
|
221
|
+
act_backup.triggered.connect(self._backup)
|
|
222
|
+
file_menu.addAction(act_backup)
|
|
204
223
|
file_menu.addSeparator()
|
|
205
224
|
act_quit = QAction("&Quit", self)
|
|
206
225
|
act_quit.setShortcut("Ctrl+Q")
|
|
@@ -210,21 +229,21 @@ class MainWindow(QMainWindow):
|
|
|
210
229
|
# Navigate menu with next/previous/today
|
|
211
230
|
nav_menu = mb.addMenu("&Navigate")
|
|
212
231
|
act_prev = QAction("Previous Day", self)
|
|
213
|
-
act_prev.setShortcut("Ctrl+P")
|
|
232
|
+
act_prev.setShortcut("Ctrl+Shift+P")
|
|
214
233
|
act_prev.setShortcutContext(Qt.ApplicationShortcut)
|
|
215
234
|
act_prev.triggered.connect(lambda: self._adjust_day(-1))
|
|
216
235
|
nav_menu.addAction(act_prev)
|
|
217
236
|
self.addAction(act_prev)
|
|
218
237
|
|
|
219
238
|
act_next = QAction("Next Day", self)
|
|
220
|
-
act_next.setShortcut("Ctrl+N")
|
|
239
|
+
act_next.setShortcut("Ctrl+Shift+N")
|
|
221
240
|
act_next.setShortcutContext(Qt.ApplicationShortcut)
|
|
222
241
|
act_next.triggered.connect(lambda: self._adjust_day(1))
|
|
223
242
|
nav_menu.addAction(act_next)
|
|
224
243
|
self.addAction(act_next)
|
|
225
244
|
|
|
226
245
|
act_today = QAction("Today", self)
|
|
227
|
-
act_today.setShortcut("Ctrl+T")
|
|
246
|
+
act_today.setShortcut("Ctrl+Shift+T")
|
|
228
247
|
act_today.setShortcutContext(Qt.ApplicationShortcut)
|
|
229
248
|
act_today.triggered.connect(self._adjust_today)
|
|
230
249
|
nav_menu.addAction(act_today)
|
|
@@ -315,6 +334,61 @@ class MainWindow(QMainWindow):
|
|
|
315
334
|
pass
|
|
316
335
|
|
|
317
336
|
# --- UI handlers ---------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
def _sync_toolbar(self):
|
|
339
|
+
fmt = self.editor.currentCharFormat()
|
|
340
|
+
c = self.editor.textCursor()
|
|
341
|
+
bf = c.blockFormat()
|
|
342
|
+
|
|
343
|
+
# Block signals so setChecked() doesn't re-trigger actions
|
|
344
|
+
blocker1 = QSignalBlocker(self.toolBar.actBold)
|
|
345
|
+
blocker2 = QSignalBlocker(self.toolBar.actItalic)
|
|
346
|
+
blocker3 = QSignalBlocker(self.toolBar.actUnderline)
|
|
347
|
+
blocker4 = QSignalBlocker(self.toolBar.actStrike)
|
|
348
|
+
|
|
349
|
+
self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold)
|
|
350
|
+
self.toolBar.actItalic.setChecked(fmt.fontItalic())
|
|
351
|
+
self.toolBar.actUnderline.setChecked(fmt.fontUnderline())
|
|
352
|
+
self.toolBar.actStrike.setChecked(fmt.fontStrikeOut())
|
|
353
|
+
|
|
354
|
+
# Headings: decide which to check by current point size
|
|
355
|
+
def _approx(a, b, eps=0.5): # small float tolerance
|
|
356
|
+
return abs(float(a) - float(b)) <= eps
|
|
357
|
+
|
|
358
|
+
cur_size = fmt.fontPointSize() or self.editor.font().pointSizeF()
|
|
359
|
+
|
|
360
|
+
bH1 = _approx(cur_size, 24)
|
|
361
|
+
bH2 = _approx(cur_size, 18)
|
|
362
|
+
bH3 = _approx(cur_size, 14)
|
|
363
|
+
|
|
364
|
+
b1 = QSignalBlocker(self.toolBar.actH1)
|
|
365
|
+
b2 = QSignalBlocker(self.toolBar.actH2)
|
|
366
|
+
b3 = QSignalBlocker(self.toolBar.actH3)
|
|
367
|
+
bN = QSignalBlocker(self.toolBar.actNormal)
|
|
368
|
+
|
|
369
|
+
self.toolBar.actH1.setChecked(bH1)
|
|
370
|
+
self.toolBar.actH2.setChecked(bH2)
|
|
371
|
+
self.toolBar.actH3.setChecked(bH3)
|
|
372
|
+
self.toolBar.actNormal.setChecked(not (bH1 or bH2 or bH3))
|
|
373
|
+
|
|
374
|
+
# Lists
|
|
375
|
+
lst = c.currentList()
|
|
376
|
+
bullets_on = lst and lst.format().style() == QTextListFormat.Style.ListDisc
|
|
377
|
+
numbers_on = lst and lst.format().style() == QTextListFormat.Style.ListDecimal
|
|
378
|
+
QSignalBlocker(self.toolBar.actBullets)
|
|
379
|
+
QSignalBlocker(self.toolBar.actNumbers)
|
|
380
|
+
self.toolBar.actBullets.setChecked(bool(bullets_on))
|
|
381
|
+
self.toolBar.actNumbers.setChecked(bool(numbers_on))
|
|
382
|
+
|
|
383
|
+
# Alignment
|
|
384
|
+
align = bf.alignment() & Qt.AlignHorizontal_Mask
|
|
385
|
+
QSignalBlocker(self.toolBar.actAlignL)
|
|
386
|
+
self.toolBar.actAlignL.setChecked(align == Qt.AlignLeft)
|
|
387
|
+
QSignalBlocker(self.toolBar.actAlignC)
|
|
388
|
+
self.toolBar.actAlignC.setChecked(align == Qt.AlignHCenter)
|
|
389
|
+
QSignalBlocker(self.toolBar.actAlignR)
|
|
390
|
+
self.toolBar.actAlignR.setChecked(align == Qt.AlignRight)
|
|
391
|
+
|
|
318
392
|
def _current_date_iso(self) -> str:
|
|
319
393
|
d = self.calendar.selectedDate()
|
|
320
394
|
return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
|
|
@@ -373,7 +447,7 @@ class MainWindow(QMainWindow):
|
|
|
373
447
|
"""
|
|
374
448
|
if not self._dirty and not explicit:
|
|
375
449
|
return
|
|
376
|
-
text = self.editor.
|
|
450
|
+
text = self.editor.to_html_with_embedded_images()
|
|
377
451
|
try:
|
|
378
452
|
self.db.save_new_version(date_iso, text, note)
|
|
379
453
|
except Exception as e:
|
|
@@ -416,6 +490,18 @@ class MainWindow(QMainWindow):
|
|
|
416
490
|
self._load_selected_date(date_iso)
|
|
417
491
|
self._refresh_calendar_marks()
|
|
418
492
|
|
|
493
|
+
def _on_insert_image(self):
|
|
494
|
+
# Let the user pick one or many images
|
|
495
|
+
paths, _ = QFileDialog.getOpenFileNames(
|
|
496
|
+
self,
|
|
497
|
+
"Insert image(s)",
|
|
498
|
+
"",
|
|
499
|
+
"Images (*.png *.jpg *.jpeg *.bmp *.gif *.webp)",
|
|
500
|
+
)
|
|
501
|
+
if not paths:
|
|
502
|
+
return
|
|
503
|
+
self.editor.insert_images(paths) # call into the editor
|
|
504
|
+
|
|
419
505
|
# ----------- Settings handler ------------#
|
|
420
506
|
def _open_settings(self):
|
|
421
507
|
dlg = SettingsDialog(self.cfg, self.db, self)
|
|
@@ -484,13 +570,31 @@ class MainWindow(QMainWindow):
|
|
|
484
570
|
# ----------------- Export handler ----------------- #
|
|
485
571
|
@Slot()
|
|
486
572
|
def _export(self):
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
573
|
+
warning_title = "Unencrypted export"
|
|
574
|
+
warning_message = """
|
|
575
|
+
Exporting the database will be unencrypted!
|
|
576
|
+
|
|
577
|
+
Are you sure you want to continue?
|
|
578
|
+
|
|
579
|
+
If you want an encrypted backup, choose Backup instead of Export.
|
|
580
|
+
"""
|
|
581
|
+
dlg = QMessageBox()
|
|
582
|
+
dlg.setWindowTitle(warning_title)
|
|
583
|
+
dlg.setText(warning_message)
|
|
584
|
+
dlg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
|
585
|
+
dlg.setIcon(QMessageBox.Warning)
|
|
586
|
+
dlg.show()
|
|
587
|
+
dlg.adjustSize()
|
|
588
|
+
if dlg.exec() != QMessageBox.Yes:
|
|
589
|
+
return False
|
|
491
590
|
|
|
492
|
-
|
|
493
|
-
|
|
591
|
+
filters = (
|
|
592
|
+
"Text (*.txt);;"
|
|
593
|
+
"JSON (*.json);;"
|
|
594
|
+
"CSV (*.csv);;"
|
|
595
|
+
"HTML (*.html);;"
|
|
596
|
+
"SQL (*.sql);;"
|
|
597
|
+
)
|
|
494
598
|
|
|
495
599
|
start_dir = os.path.join(os.path.expanduser("~"), "Documents")
|
|
496
600
|
filename, selected_filter = QFileDialog.getSaveFileName(
|
|
@@ -504,6 +608,7 @@ class MainWindow(QMainWindow):
|
|
|
504
608
|
"JSON (*.json)": ".json",
|
|
505
609
|
"CSV (*.csv)": ".csv",
|
|
506
610
|
"HTML (*.html)": ".html",
|
|
611
|
+
"SQL (*.sql)": ".sql",
|
|
507
612
|
}.get(selected_filter, ".txt")
|
|
508
613
|
|
|
509
614
|
if not Path(filename).suffix:
|
|
@@ -518,14 +623,49 @@ class MainWindow(QMainWindow):
|
|
|
518
623
|
elif selected_filter.startswith("CSV"):
|
|
519
624
|
self.db.export_csv(entries, filename)
|
|
520
625
|
elif selected_filter.startswith("HTML"):
|
|
521
|
-
self.
|
|
626
|
+
self.db.export_html(entries, filename)
|
|
627
|
+
elif selected_filter.startswith("SQL"):
|
|
628
|
+
self.db.export_sql(filename)
|
|
522
629
|
else:
|
|
523
|
-
self.
|
|
630
|
+
self.db.export_by_extension(filename)
|
|
524
631
|
|
|
525
632
|
QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}")
|
|
526
633
|
except Exception as e:
|
|
527
634
|
QMessageBox.critical(self, "Export failed", str(e))
|
|
528
635
|
|
|
636
|
+
# ----------------- Backup handler ----------------- #
|
|
637
|
+
@Slot()
|
|
638
|
+
def _backup(self):
|
|
639
|
+
filters = "SQLCipher (*.db);;"
|
|
640
|
+
|
|
641
|
+
now = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
|
642
|
+
start_dir = os.path.join(
|
|
643
|
+
os.path.expanduser("~"), "Documents", f"bouquin_backup_{now}.db"
|
|
644
|
+
)
|
|
645
|
+
filename, selected_filter = QFileDialog.getSaveFileName(
|
|
646
|
+
self, "Backup encrypted notebook", start_dir, filters
|
|
647
|
+
)
|
|
648
|
+
if not filename:
|
|
649
|
+
return # user cancelled
|
|
650
|
+
|
|
651
|
+
default_ext = {
|
|
652
|
+
"SQLCipher (*.db)": ".db",
|
|
653
|
+
}.get(selected_filter, ".db")
|
|
654
|
+
|
|
655
|
+
if not Path(filename).suffix:
|
|
656
|
+
filename += default_ext
|
|
657
|
+
|
|
658
|
+
try:
|
|
659
|
+
if selected_filter.startswith("SQL"):
|
|
660
|
+
self.db.export_sqlcipher(filename)
|
|
661
|
+
QMessageBox.information(
|
|
662
|
+
self, "Backup complete", f"Saved to:\n{filename}"
|
|
663
|
+
)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
QMessageBox.critical(self, "Backup failed", str(e))
|
|
666
|
+
|
|
667
|
+
# ----------------- Help handlers ----------------- #
|
|
668
|
+
|
|
529
669
|
def _open_docs(self):
|
|
530
670
|
url_str = "https://git.mig5.net/mig5/bouquin/wiki/Help"
|
|
531
671
|
url = QUrl.fromUserInput(url_str)
|
|
@@ -542,7 +682,7 @@ class MainWindow(QMainWindow):
|
|
|
542
682
|
self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}"
|
|
543
683
|
)
|
|
544
684
|
|
|
545
|
-
# Idle handlers
|
|
685
|
+
# ----------------- Idle handlers ----------------- #
|
|
546
686
|
def _apply_idle_minutes(self, minutes: int):
|
|
547
687
|
minutes = max(0, int(minutes))
|
|
548
688
|
if not hasattr(self, "_idle_timer"):
|
|
@@ -600,13 +740,14 @@ class MainWindow(QMainWindow):
|
|
|
600
740
|
tb.setEnabled(True)
|
|
601
741
|
self._idle_timer.start()
|
|
602
742
|
|
|
603
|
-
# Close
|
|
743
|
+
# ----------------- Close handlers ----------------- #
|
|
604
744
|
def closeEvent(self, event):
|
|
605
745
|
try:
|
|
606
746
|
# Save window position
|
|
607
747
|
self.settings.setValue("main/geometry", self.saveGeometry())
|
|
608
748
|
self.settings.setValue("main/windowState", self.saveState())
|
|
609
749
|
self.settings.setValue("main/maximized", self.isMaximized())
|
|
750
|
+
|
|
610
751
|
# Ensure we save any last pending edits to the db
|
|
611
752
|
self._save_current()
|
|
612
753
|
self.db.close()
|
bouquin/settings_dialog.py
CHANGED
|
@@ -57,7 +57,7 @@ class SettingsDialog(QDialog):
|
|
|
57
57
|
form.addRow("Database path", path_row)
|
|
58
58
|
|
|
59
59
|
# Encryption settings
|
|
60
|
-
enc_group = QGroupBox("Encryption
|
|
60
|
+
enc_group = QGroupBox("Encryption")
|
|
61
61
|
enc = QVBoxLayout(enc_group)
|
|
62
62
|
enc.setContentsMargins(12, 8, 12, 12)
|
|
63
63
|
enc.setSpacing(6)
|
|
@@ -68,7 +68,7 @@ class SettingsDialog(QDialog):
|
|
|
68
68
|
self.key = current_settings.key or ""
|
|
69
69
|
self.save_key_btn.setChecked(bool(self.key))
|
|
70
70
|
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
|
71
|
-
self.save_key_btn.toggled.connect(self.
|
|
71
|
+
self.save_key_btn.toggled.connect(self._save_key_btn_clicked)
|
|
72
72
|
enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft)
|
|
73
73
|
|
|
74
74
|
# Explanation for remembering key
|
|
@@ -94,11 +94,20 @@ class SettingsDialog(QDialog):
|
|
|
94
94
|
enc.addWidget(line)
|
|
95
95
|
|
|
96
96
|
# Change key button
|
|
97
|
-
self.rekey_btn = QPushButton("Change key")
|
|
97
|
+
self.rekey_btn = QPushButton("Change encryption key")
|
|
98
98
|
self.rekey_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
99
99
|
self.rekey_btn.clicked.connect(self._change_key)
|
|
100
|
+
|
|
100
101
|
enc.addWidget(self.rekey_btn, 0, Qt.AlignLeft)
|
|
101
102
|
|
|
103
|
+
form.addRow(enc_group)
|
|
104
|
+
|
|
105
|
+
# Privacy settings
|
|
106
|
+
priv_group = QGroupBox("Lock screen when idle")
|
|
107
|
+
priv = QVBoxLayout(priv_group)
|
|
108
|
+
priv.setContentsMargins(12, 8, 12, 12)
|
|
109
|
+
priv.setSpacing(6)
|
|
110
|
+
|
|
102
111
|
self.idle_spin = QSpinBox()
|
|
103
112
|
self.idle_spin.setRange(0, 240)
|
|
104
113
|
self.idle_spin.setSingleStep(1)
|
|
@@ -106,7 +115,7 @@ class SettingsDialog(QDialog):
|
|
|
106
115
|
self.idle_spin.setSuffix(" min")
|
|
107
116
|
self.idle_spin.setSpecialValueText("Never")
|
|
108
117
|
self.idle_spin.setValue(getattr(cfg, "idle_minutes", 15))
|
|
109
|
-
|
|
118
|
+
priv.addWidget(self.idle_spin, 0, Qt.AlignLeft)
|
|
110
119
|
# Explanation for idle option (autolock)
|
|
111
120
|
self.idle_spin_label = QLabel(
|
|
112
121
|
"Bouquin will automatically lock the notepad after this length of time, after which you'll need to re-enter the key to unlock it. "
|
|
@@ -122,10 +131,39 @@ class SettingsDialog(QDialog):
|
|
|
122
131
|
spin_row = QHBoxLayout()
|
|
123
132
|
spin_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the spinbox
|
|
124
133
|
spin_row.addWidget(self.idle_spin_label)
|
|
125
|
-
|
|
134
|
+
priv.addLayout(spin_row)
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
form.addRow(priv_group)
|
|
137
|
+
|
|
138
|
+
# Maintenance settings
|
|
139
|
+
maint_group = QGroupBox("Database maintenance")
|
|
140
|
+
maint = QVBoxLayout(maint_group)
|
|
141
|
+
maint.setContentsMargins(12, 8, 12, 12)
|
|
142
|
+
maint.setSpacing(6)
|
|
143
|
+
|
|
144
|
+
self.compact_btn = QPushButton("Compact database")
|
|
145
|
+
self.compact_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
146
|
+
self.compact_btn.clicked.connect(self._compact_btn_clicked)
|
|
147
|
+
|
|
148
|
+
maint.addWidget(self.compact_btn, 0, Qt.AlignLeft)
|
|
149
|
+
|
|
150
|
+
# Explanation for compating button
|
|
151
|
+
self.compact_label = QLabel(
|
|
152
|
+
"Compacting runs VACUUM on the database. This can help reduce its size."
|
|
153
|
+
)
|
|
154
|
+
self.compact_label.setWordWrap(True)
|
|
155
|
+
self.compact_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
156
|
+
# make it look secondary
|
|
157
|
+
cpal = self.compact_label.palette()
|
|
158
|
+
cpal.setColor(self.compact_label.foregroundRole(), cpal.color(QPalette.Mid))
|
|
159
|
+
self.compact_label.setPalette(cpal)
|
|
160
|
+
|
|
161
|
+
maint_row = QHBoxLayout()
|
|
162
|
+
maint_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the button
|
|
163
|
+
maint_row.addWidget(self.compact_label)
|
|
164
|
+
maint.addLayout(maint_row)
|
|
165
|
+
|
|
166
|
+
form.addRow(maint_group)
|
|
129
167
|
|
|
130
168
|
# Buttons
|
|
131
169
|
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
|
@@ -174,6 +212,7 @@ class SettingsDialog(QDialog):
|
|
|
174
212
|
QMessageBox.warning(self, "Empty key", "Key cannot be empty.")
|
|
175
213
|
return
|
|
176
214
|
try:
|
|
215
|
+
self.key = new_key
|
|
177
216
|
self._db.rekey(new_key)
|
|
178
217
|
QMessageBox.information(
|
|
179
218
|
self, "Key changed", "The notebook was re-encrypted with the new key!"
|
|
@@ -182,7 +221,7 @@ class SettingsDialog(QDialog):
|
|
|
182
221
|
QMessageBox.critical(self, "Error", f"Could not change key:\n{e}")
|
|
183
222
|
|
|
184
223
|
@Slot(bool)
|
|
185
|
-
def
|
|
224
|
+
def _save_key_btn_clicked(self, checked: bool):
|
|
186
225
|
if checked:
|
|
187
226
|
if not self.key:
|
|
188
227
|
p1 = KeyPrompt(
|
|
@@ -193,10 +232,20 @@ class SettingsDialog(QDialog):
|
|
|
193
232
|
self.save_key_btn.setChecked(False)
|
|
194
233
|
self.save_key_btn.blockSignals(False)
|
|
195
234
|
return
|
|
196
|
-
|
|
235
|
+
self.key = p1.key() or ""
|
|
197
236
|
else:
|
|
198
237
|
self.key = ""
|
|
199
238
|
|
|
239
|
+
@Slot(bool)
|
|
240
|
+
def _compact_btn_clicked(self):
|
|
241
|
+
try:
|
|
242
|
+
self._db.compact()
|
|
243
|
+
QMessageBox.information(
|
|
244
|
+
self, "Compact complete", "Database compacted successfully!"
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
QMessageBox.critical(self, "Error", f"Could not compact database:\n{e}")
|
|
248
|
+
|
|
200
249
|
@property
|
|
201
250
|
def config(self) -> DBConfig:
|
|
202
251
|
return self._cfg
|
bouquin/toolbar.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from PySide6.QtCore import Signal, Qt
|
|
4
|
-
from PySide6.QtGui import QAction, QKeySequence, QFont, QFontDatabase
|
|
4
|
+
from PySide6.QtGui import QAction, QKeySequence, QFont, QFontDatabase, QActionGroup
|
|
5
5
|
from PySide6.QtWidgets import QToolBar
|
|
6
6
|
|
|
7
7
|
|
|
@@ -16,6 +16,7 @@ class ToolBar(QToolBar):
|
|
|
16
16
|
numbersRequested = Signal()
|
|
17
17
|
alignRequested = Signal(Qt.AlignmentFlag)
|
|
18
18
|
historyRequested = Signal()
|
|
19
|
+
insertImageRequested = Signal()
|
|
19
20
|
|
|
20
21
|
def __init__(self, parent=None):
|
|
21
22
|
super().__init__("Format", parent)
|
|
@@ -25,54 +26,87 @@ class ToolBar(QToolBar):
|
|
|
25
26
|
self._apply_toolbar_styles()
|
|
26
27
|
|
|
27
28
|
def _build_actions(self):
|
|
28
|
-
self.actBold = QAction("
|
|
29
|
+
self.actBold = QAction("B", self)
|
|
30
|
+
self.actBold.setToolTip("Bold")
|
|
31
|
+
self.actBold.setCheckable(True)
|
|
29
32
|
self.actBold.setShortcut(QKeySequence.Bold)
|
|
30
33
|
self.actBold.triggered.connect(self.boldRequested)
|
|
31
34
|
|
|
32
|
-
self.actItalic = QAction("
|
|
35
|
+
self.actItalic = QAction("I", self)
|
|
36
|
+
self.actItalic.setToolTip("Italic")
|
|
37
|
+
self.actItalic.setCheckable(True)
|
|
33
38
|
self.actItalic.setShortcut(QKeySequence.Italic)
|
|
34
39
|
self.actItalic.triggered.connect(self.italicRequested)
|
|
35
40
|
|
|
36
|
-
self.actUnderline = QAction("
|
|
41
|
+
self.actUnderline = QAction("U", self)
|
|
42
|
+
self.actUnderline.setToolTip("Underline")
|
|
43
|
+
self.actUnderline.setCheckable(True)
|
|
37
44
|
self.actUnderline.setShortcut(QKeySequence.Underline)
|
|
38
45
|
self.actUnderline.triggered.connect(self.underlineRequested)
|
|
39
46
|
|
|
40
|
-
self.actStrike = QAction("
|
|
47
|
+
self.actStrike = QAction("S", self)
|
|
48
|
+
self.actStrike.setToolTip("Strikethrough")
|
|
49
|
+
self.actStrike.setCheckable(True)
|
|
41
50
|
self.actStrike.setShortcut("Ctrl+-")
|
|
42
51
|
self.actStrike.triggered.connect(self.strikeRequested)
|
|
43
52
|
|
|
44
|
-
self.actCode = QAction("
|
|
53
|
+
self.actCode = QAction("</>", self)
|
|
54
|
+
self.actCode.setToolTip("Code block")
|
|
45
55
|
self.actCode.setShortcut("Ctrl+`")
|
|
46
56
|
self.actCode.triggered.connect(self.codeRequested)
|
|
47
57
|
|
|
48
58
|
# Headings
|
|
49
|
-
self.actH1 = QAction("
|
|
50
|
-
self.
|
|
51
|
-
self.
|
|
52
|
-
self.actNormal = QAction("Normal text", self)
|
|
59
|
+
self.actH1 = QAction("H1", self)
|
|
60
|
+
self.actH1.setToolTip("Heading 1")
|
|
61
|
+
self.actH1.setCheckable(True)
|
|
53
62
|
self.actH1.setShortcut("Ctrl+1")
|
|
54
|
-
self.actH2.setShortcut("Ctrl+2")
|
|
55
|
-
self.actH3.setShortcut("Ctrl+3")
|
|
56
|
-
self.actNormal.setShortcut("Ctrl+N")
|
|
57
63
|
self.actH1.triggered.connect(lambda: self.headingRequested.emit(24))
|
|
64
|
+
self.actH2 = QAction("H2", self)
|
|
65
|
+
self.actH2.setToolTip("Heading 2")
|
|
66
|
+
self.actH2.setCheckable(True)
|
|
67
|
+
self.actH2.setShortcut("Ctrl+2")
|
|
58
68
|
self.actH2.triggered.connect(lambda: self.headingRequested.emit(18))
|
|
69
|
+
self.actH3 = QAction("H3", self)
|
|
70
|
+
self.actH3.setToolTip("Heading 3")
|
|
71
|
+
self.actH3.setCheckable(True)
|
|
72
|
+
self.actH3.setShortcut("Ctrl+3")
|
|
59
73
|
self.actH3.triggered.connect(lambda: self.headingRequested.emit(14))
|
|
74
|
+
self.actNormal = QAction("N", self)
|
|
75
|
+
self.actNormal.setToolTip("Normal paragraph text")
|
|
76
|
+
self.actNormal.setCheckable(True)
|
|
77
|
+
self.actNormal.setShortcut("Ctrl+N")
|
|
60
78
|
self.actNormal.triggered.connect(lambda: self.headingRequested.emit(0))
|
|
61
79
|
|
|
62
80
|
# Lists
|
|
63
|
-
self.actBullets = QAction("
|
|
81
|
+
self.actBullets = QAction("•", self)
|
|
82
|
+
self.actBullets.setToolTip("Bulleted list")
|
|
83
|
+
self.actBullets.setCheckable(True)
|
|
64
84
|
self.actBullets.triggered.connect(self.bulletsRequested)
|
|
65
|
-
self.actNumbers = QAction("
|
|
85
|
+
self.actNumbers = QAction("1.", self)
|
|
86
|
+
self.actNumbers.setToolTip("Numbered list")
|
|
87
|
+
self.actNumbers.setCheckable(True)
|
|
66
88
|
self.actNumbers.triggered.connect(self.numbersRequested)
|
|
67
89
|
|
|
90
|
+
# Images
|
|
91
|
+
self.actInsertImg = QAction("Image", self)
|
|
92
|
+
self.actInsertImg.setToolTip("Insert image")
|
|
93
|
+
self.actInsertImg.setShortcut("Ctrl+Shift+I")
|
|
94
|
+
self.actInsertImg.triggered.connect(self.insertImageRequested)
|
|
95
|
+
|
|
68
96
|
# Alignment
|
|
69
|
-
self.actAlignL = QAction("
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
97
|
+
self.actAlignL = QAction("L", self)
|
|
98
|
+
self.actAlignL.setToolTip("Align Left")
|
|
99
|
+
self.actAlignL.setCheckable(True)
|
|
72
100
|
self.actAlignL.triggered.connect(lambda: self.alignRequested.emit(Qt.AlignLeft))
|
|
101
|
+
self.actAlignC = QAction("C", self)
|
|
102
|
+
self.actAlignC.setToolTip("Align Center")
|
|
103
|
+
self.actAlignC.setCheckable(True)
|
|
73
104
|
self.actAlignC.triggered.connect(
|
|
74
105
|
lambda: self.alignRequested.emit(Qt.AlignHCenter)
|
|
75
106
|
)
|
|
107
|
+
self.actAlignR = QAction("R", self)
|
|
108
|
+
self.actAlignR.setToolTip("Align Right")
|
|
109
|
+
self.actAlignR.setCheckable(True)
|
|
76
110
|
self.actAlignR.triggered.connect(
|
|
77
111
|
lambda: self.alignRequested.emit(Qt.AlignRight)
|
|
78
112
|
)
|
|
@@ -81,6 +115,28 @@ class ToolBar(QToolBar):
|
|
|
81
115
|
self.actHistory = QAction("History", self)
|
|
82
116
|
self.actHistory.triggered.connect(self.historyRequested)
|
|
83
117
|
|
|
118
|
+
# Set exclusive buttons in QActionGroups
|
|
119
|
+
self.grpHeadings = QActionGroup(self)
|
|
120
|
+
self.grpHeadings.setExclusive(True)
|
|
121
|
+
for a in (
|
|
122
|
+
self.actBold,
|
|
123
|
+
self.actItalic,
|
|
124
|
+
self.actUnderline,
|
|
125
|
+
self.actStrike,
|
|
126
|
+
self.actH1,
|
|
127
|
+
self.actH2,
|
|
128
|
+
self.actH3,
|
|
129
|
+
self.actNormal,
|
|
130
|
+
):
|
|
131
|
+
a.setCheckable(True)
|
|
132
|
+
a.setActionGroup(self.grpHeadings)
|
|
133
|
+
|
|
134
|
+
self.grpAlign = QActionGroup(self)
|
|
135
|
+
self.grpAlign.setExclusive(True)
|
|
136
|
+
for a in (self.actAlignL, self.actAlignC, self.actAlignR):
|
|
137
|
+
a.setActionGroup(self.grpAlign)
|
|
138
|
+
|
|
139
|
+
# Add actions
|
|
84
140
|
self.addActions(
|
|
85
141
|
[
|
|
86
142
|
self.actBold,
|
|
@@ -94,6 +150,7 @@ class ToolBar(QToolBar):
|
|
|
94
150
|
self.actNormal,
|
|
95
151
|
self.actBullets,
|
|
96
152
|
self.actNumbers,
|
|
153
|
+
self.actInsertImg,
|
|
97
154
|
self.actAlignL,
|
|
98
155
|
self.actAlignC,
|
|
99
156
|
self.actAlignR,
|
|
@@ -106,7 +163,6 @@ class ToolBar(QToolBar):
|
|
|
106
163
|
self._style_letter_button(self.actItalic, "I", italic=True)
|
|
107
164
|
self._style_letter_button(self.actUnderline, "U", underline=True)
|
|
108
165
|
self._style_letter_button(self.actStrike, "S", strike=True)
|
|
109
|
-
|
|
110
166
|
# Monospace look for code; use a fixed font
|
|
111
167
|
code_font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
|
|
112
168
|
self._style_letter_button(self.actCode, "</>", custom_font=code_font)
|
|
@@ -139,11 +195,13 @@ class ToolBar(QToolBar):
|
|
|
139
195
|
underline: bool = False,
|
|
140
196
|
strike: bool = False,
|
|
141
197
|
custom_font: QFont | None = None,
|
|
198
|
+
tooltip: str | None = None,
|
|
142
199
|
):
|
|
143
200
|
btn = self.widgetForAction(action)
|
|
144
201
|
if not btn:
|
|
145
202
|
return
|
|
146
203
|
btn.setText(text)
|
|
204
|
+
|
|
147
205
|
f = custom_font if custom_font is not None else QFont(btn.font())
|
|
148
206
|
if custom_font is None:
|
|
149
207
|
f.setBold(bold)
|
|
@@ -153,5 +211,6 @@ class ToolBar(QToolBar):
|
|
|
153
211
|
btn.setFont(f)
|
|
154
212
|
|
|
155
213
|
# Keep accessibility/tooltip readable
|
|
156
|
-
|
|
157
|
-
|
|
214
|
+
if tooltip:
|
|
215
|
+
btn.setToolTip(tooltip)
|
|
216
|
+
btn.setAccessibleName(tooltip)
|