bouquin 0.1.10__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/__init__.py +0 -0
- bouquin/__main__.py +4 -0
- bouquin/db.py +499 -0
- bouquin/editor.py +897 -0
- bouquin/history_dialog.py +176 -0
- bouquin/key_prompt.py +41 -0
- bouquin/lock_overlay.py +127 -0
- bouquin/main.py +24 -0
- bouquin/main_window.py +904 -0
- bouquin/save_dialog.py +35 -0
- bouquin/search.py +209 -0
- bouquin/settings.py +39 -0
- bouquin/settings_dialog.py +302 -0
- bouquin/theme.py +105 -0
- bouquin/toolbar.py +221 -0
- bouquin-0.1.10.dist-info/LICENSE +674 -0
- bouquin-0.1.10.dist-info/METADATA +83 -0
- bouquin-0.1.10.dist-info/RECORD +20 -0
- bouquin-0.1.10.dist-info/WHEEL +4 -0
- bouquin-0.1.10.dist-info/entry_points.txt +3 -0
bouquin/main_window.py
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from PySide6.QtCore import (
|
|
10
|
+
QDate,
|
|
11
|
+
QTimer,
|
|
12
|
+
Qt,
|
|
13
|
+
QSettings,
|
|
14
|
+
Slot,
|
|
15
|
+
QUrl,
|
|
16
|
+
QEvent,
|
|
17
|
+
QSignalBlocker,
|
|
18
|
+
)
|
|
19
|
+
from PySide6.QtGui import (
|
|
20
|
+
QAction,
|
|
21
|
+
QBrush,
|
|
22
|
+
QColor,
|
|
23
|
+
QCursor,
|
|
24
|
+
QDesktopServices,
|
|
25
|
+
QFont,
|
|
26
|
+
QGuiApplication,
|
|
27
|
+
QPalette,
|
|
28
|
+
QTextCharFormat,
|
|
29
|
+
QTextListFormat,
|
|
30
|
+
)
|
|
31
|
+
from PySide6.QtWidgets import (
|
|
32
|
+
QApplication,
|
|
33
|
+
QCalendarWidget,
|
|
34
|
+
QDialog,
|
|
35
|
+
QFileDialog,
|
|
36
|
+
QMainWindow,
|
|
37
|
+
QMessageBox,
|
|
38
|
+
QSizePolicy,
|
|
39
|
+
QSplitter,
|
|
40
|
+
QVBoxLayout,
|
|
41
|
+
QWidget,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from .db import DBManager
|
|
45
|
+
from .editor import Editor
|
|
46
|
+
from .history_dialog import HistoryDialog
|
|
47
|
+
from .key_prompt import KeyPrompt
|
|
48
|
+
from .lock_overlay import LockOverlay
|
|
49
|
+
from .save_dialog import SaveDialog
|
|
50
|
+
from .search import Search
|
|
51
|
+
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
|
52
|
+
from .settings_dialog import SettingsDialog
|
|
53
|
+
from .toolbar import ToolBar
|
|
54
|
+
from .theme import Theme, ThemeManager
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MainWindow(QMainWindow):
|
|
58
|
+
def __init__(self, themes: ThemeManager, *args, **kwargs):
|
|
59
|
+
super().__init__(*args, **kwargs)
|
|
60
|
+
self.setWindowTitle(APP_NAME)
|
|
61
|
+
self.setMinimumSize(1000, 650)
|
|
62
|
+
|
|
63
|
+
self.themes = themes # Store the themes manager
|
|
64
|
+
|
|
65
|
+
self.cfg = load_db_config()
|
|
66
|
+
if not os.path.exists(self.cfg.path):
|
|
67
|
+
# Fresh database/first time use, so guide the user re: setting a key
|
|
68
|
+
first_time = True
|
|
69
|
+
else:
|
|
70
|
+
first_time = False
|
|
71
|
+
|
|
72
|
+
# Prompt for the key unless it is found in config
|
|
73
|
+
if not self.cfg.key:
|
|
74
|
+
if not self._prompt_for_key_until_valid(first_time):
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
else:
|
|
77
|
+
self._try_connect()
|
|
78
|
+
|
|
79
|
+
# ---- UI: Left fixed panel (calendar) + right editor -----------------
|
|
80
|
+
self.calendar = QCalendarWidget()
|
|
81
|
+
self.calendar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
82
|
+
self.calendar.setGridVisible(True)
|
|
83
|
+
self.calendar.selectionChanged.connect(self._on_date_changed)
|
|
84
|
+
|
|
85
|
+
self.search = Search(self.db)
|
|
86
|
+
self.search.openDateRequested.connect(self._load_selected_date)
|
|
87
|
+
self.search.resultDatesChanged.connect(self._on_search_dates_changed)
|
|
88
|
+
|
|
89
|
+
# Lock the calendar to the left panel at the top to stop it stretching
|
|
90
|
+
# when the main window is resized.
|
|
91
|
+
left_panel = QWidget()
|
|
92
|
+
left_layout = QVBoxLayout(left_panel)
|
|
93
|
+
left_layout.setContentsMargins(8, 8, 8, 8)
|
|
94
|
+
left_layout.addWidget(self.calendar)
|
|
95
|
+
left_layout.addWidget(self.search)
|
|
96
|
+
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
|
|
97
|
+
|
|
98
|
+
# This is the note-taking editor
|
|
99
|
+
self.editor = Editor(self.themes)
|
|
100
|
+
|
|
101
|
+
# Toolbar for controlling styling
|
|
102
|
+
self.toolBar = ToolBar()
|
|
103
|
+
self.addToolBar(self.toolBar)
|
|
104
|
+
# Wire toolbar intents to editor methods
|
|
105
|
+
self.toolBar.boldRequested.connect(self.editor.apply_weight)
|
|
106
|
+
self.toolBar.italicRequested.connect(self.editor.apply_italic)
|
|
107
|
+
self.toolBar.underlineRequested.connect(self.editor.apply_underline)
|
|
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)
|
|
120
|
+
|
|
121
|
+
split = QSplitter()
|
|
122
|
+
split.addWidget(left_panel)
|
|
123
|
+
split.addWidget(self.editor)
|
|
124
|
+
split.setStretchFactor(1, 1) # editor grows
|
|
125
|
+
|
|
126
|
+
container = QWidget()
|
|
127
|
+
lay = QVBoxLayout(container)
|
|
128
|
+
lay.addWidget(split)
|
|
129
|
+
self.setCentralWidget(container)
|
|
130
|
+
|
|
131
|
+
# Idle lock setup
|
|
132
|
+
self._idle_timer = QTimer(self)
|
|
133
|
+
self._idle_timer.setSingleShot(True)
|
|
134
|
+
self._idle_timer.timeout.connect(self._enter_lock)
|
|
135
|
+
self._apply_idle_minutes(getattr(self.cfg, "idle_minutes", 15))
|
|
136
|
+
self._idle_timer.start()
|
|
137
|
+
|
|
138
|
+
# full-window overlay that sits on top of the central widget
|
|
139
|
+
self._lock_overlay = LockOverlay(self.centralWidget(), self._on_unlock_clicked)
|
|
140
|
+
self.centralWidget().installEventFilter(self._lock_overlay)
|
|
141
|
+
|
|
142
|
+
self._locked = False
|
|
143
|
+
|
|
144
|
+
# reset idle timer on any key press anywhere in the app
|
|
145
|
+
from PySide6.QtWidgets import QApplication
|
|
146
|
+
|
|
147
|
+
QApplication.instance().installEventFilter(self)
|
|
148
|
+
|
|
149
|
+
# Status bar for feedback
|
|
150
|
+
self.statusBar().showMessage("Ready", 800)
|
|
151
|
+
|
|
152
|
+
# Menu bar (File)
|
|
153
|
+
mb = self.menuBar()
|
|
154
|
+
file_menu = mb.addMenu("&File")
|
|
155
|
+
act_save = QAction("&Save a version", self)
|
|
156
|
+
act_save.setShortcut("Ctrl+S")
|
|
157
|
+
act_save.triggered.connect(lambda: self._save_current(explicit=True))
|
|
158
|
+
file_menu.addAction(act_save)
|
|
159
|
+
act_history = QAction("History", self)
|
|
160
|
+
act_history.setShortcut("Ctrl+H")
|
|
161
|
+
act_history.setShortcutContext(Qt.ApplicationShortcut)
|
|
162
|
+
act_history.triggered.connect(self._open_history)
|
|
163
|
+
file_menu.addAction(act_history)
|
|
164
|
+
act_settings = QAction("Settin&gs", self)
|
|
165
|
+
act_settings.setShortcut("Ctrl+G")
|
|
166
|
+
act_settings.triggered.connect(self._open_settings)
|
|
167
|
+
file_menu.addAction(act_settings)
|
|
168
|
+
act_export = QAction("&Export", self)
|
|
169
|
+
act_export.setShortcut("Ctrl+E")
|
|
170
|
+
act_export.triggered.connect(self._export)
|
|
171
|
+
file_menu.addAction(act_export)
|
|
172
|
+
act_backup = QAction("&Backup", self)
|
|
173
|
+
act_backup.setShortcut("Ctrl+Shift+B")
|
|
174
|
+
act_backup.triggered.connect(self._backup)
|
|
175
|
+
file_menu.addAction(act_backup)
|
|
176
|
+
file_menu.addSeparator()
|
|
177
|
+
act_quit = QAction("&Quit", self)
|
|
178
|
+
act_quit.setShortcut("Ctrl+Q")
|
|
179
|
+
act_quit.triggered.connect(self.close)
|
|
180
|
+
file_menu.addAction(act_quit)
|
|
181
|
+
|
|
182
|
+
# Navigate menu with next/previous/today
|
|
183
|
+
nav_menu = mb.addMenu("&Navigate")
|
|
184
|
+
act_prev = QAction("Previous Day", self)
|
|
185
|
+
act_prev.setShortcut("Ctrl+Shift+P")
|
|
186
|
+
act_prev.setShortcutContext(Qt.ApplicationShortcut)
|
|
187
|
+
act_prev.triggered.connect(lambda: self._adjust_day(-1))
|
|
188
|
+
nav_menu.addAction(act_prev)
|
|
189
|
+
self.addAction(act_prev)
|
|
190
|
+
|
|
191
|
+
act_next = QAction("Next Day", self)
|
|
192
|
+
act_next.setShortcut("Ctrl+Shift+N")
|
|
193
|
+
act_next.setShortcutContext(Qt.ApplicationShortcut)
|
|
194
|
+
act_next.triggered.connect(lambda: self._adjust_day(1))
|
|
195
|
+
nav_menu.addAction(act_next)
|
|
196
|
+
self.addAction(act_next)
|
|
197
|
+
|
|
198
|
+
act_today = QAction("Today", self)
|
|
199
|
+
act_today.setShortcut("Ctrl+Shift+T")
|
|
200
|
+
act_today.setShortcutContext(Qt.ApplicationShortcut)
|
|
201
|
+
act_today.triggered.connect(self._adjust_today)
|
|
202
|
+
nav_menu.addAction(act_today)
|
|
203
|
+
self.addAction(act_today)
|
|
204
|
+
|
|
205
|
+
# Help menu with drop-down
|
|
206
|
+
help_menu = mb.addMenu("&Help")
|
|
207
|
+
act_docs = QAction("Documentation", self)
|
|
208
|
+
act_docs.setShortcut("Ctrl+D")
|
|
209
|
+
act_docs.setShortcutContext(Qt.ApplicationShortcut)
|
|
210
|
+
act_docs.triggered.connect(self._open_docs)
|
|
211
|
+
help_menu.addAction(act_docs)
|
|
212
|
+
self.addAction(act_docs)
|
|
213
|
+
act_bugs = QAction("Report a bug", self)
|
|
214
|
+
act_bugs.setShortcut("Ctrl+R")
|
|
215
|
+
act_bugs.setShortcutContext(Qt.ApplicationShortcut)
|
|
216
|
+
act_bugs.triggered.connect(self._open_bugs)
|
|
217
|
+
help_menu.addAction(act_bugs)
|
|
218
|
+
self.addAction(act_bugs)
|
|
219
|
+
|
|
220
|
+
# Autosave
|
|
221
|
+
self._dirty = False
|
|
222
|
+
self._save_timer = QTimer(self)
|
|
223
|
+
self._save_timer.setSingleShot(True)
|
|
224
|
+
self._save_timer.timeout.connect(self._save_current)
|
|
225
|
+
self.editor.textChanged.connect(self._on_text_changed)
|
|
226
|
+
|
|
227
|
+
# First load + mark dates in calendar with content
|
|
228
|
+
if not self._load_yesterday_todos():
|
|
229
|
+
self._load_selected_date()
|
|
230
|
+
self._refresh_calendar_marks()
|
|
231
|
+
|
|
232
|
+
# Restore window position from settings
|
|
233
|
+
self.settings = QSettings(APP_ORG, APP_NAME)
|
|
234
|
+
self._restore_window_position()
|
|
235
|
+
|
|
236
|
+
self._apply_link_css() # Apply link color on startup
|
|
237
|
+
# re-apply all runtime color tweaks when theme changes
|
|
238
|
+
self.themes.themeChanged.connect(lambda _t: self._retheme_overrides())
|
|
239
|
+
self.themes.themeChanged.connect(self._apply_calendar_theme)
|
|
240
|
+
self._apply_calendar_text_colors()
|
|
241
|
+
self._apply_calendar_theme(self.themes.current())
|
|
242
|
+
|
|
243
|
+
# apply once on startup so links / calendar colors are set immediately
|
|
244
|
+
self._retheme_overrides()
|
|
245
|
+
|
|
246
|
+
def _try_connect(self) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Try to connect to the database.
|
|
249
|
+
"""
|
|
250
|
+
try:
|
|
251
|
+
self.db = DBManager(self.cfg)
|
|
252
|
+
ok = self.db.connect()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
if str(e) == "file is not a database":
|
|
255
|
+
error = "The key is probably incorrect."
|
|
256
|
+
else:
|
|
257
|
+
error = str(e)
|
|
258
|
+
QMessageBox.critical(self, "Database Error", error)
|
|
259
|
+
return False
|
|
260
|
+
return ok
|
|
261
|
+
|
|
262
|
+
def _prompt_for_key_until_valid(self, first_time: bool) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Prompt for the SQLCipher key.
|
|
265
|
+
"""
|
|
266
|
+
if first_time:
|
|
267
|
+
title = "Set an encryption key"
|
|
268
|
+
message = "Bouquin encrypts your data.\n\nPlease create a strong passphrase to encrypt the notebook.\n\nYou can always change it later!"
|
|
269
|
+
else:
|
|
270
|
+
title = "Unlock encrypted notebook"
|
|
271
|
+
message = "Enter your key to unlock the notebook"
|
|
272
|
+
while True:
|
|
273
|
+
dlg = KeyPrompt(self, title, message)
|
|
274
|
+
if dlg.exec() != QDialog.Accepted:
|
|
275
|
+
return False
|
|
276
|
+
self.cfg.key = dlg.key()
|
|
277
|
+
if self._try_connect():
|
|
278
|
+
return True
|
|
279
|
+
|
|
280
|
+
def _retheme_overrides(self):
|
|
281
|
+
if hasattr(self, "_lock_overlay"):
|
|
282
|
+
self._lock_overlay._apply_overlay_style()
|
|
283
|
+
self._apply_calendar_text_colors()
|
|
284
|
+
self._apply_link_css() # Reapply link styles based on the current theme
|
|
285
|
+
self._apply_search_highlights(getattr(self, "_search_highlighted_dates", set()))
|
|
286
|
+
self.calendar.update()
|
|
287
|
+
self.editor.viewport().update()
|
|
288
|
+
|
|
289
|
+
def _apply_link_css(self):
|
|
290
|
+
if self.themes and self.themes.current() == Theme.DARK:
|
|
291
|
+
anchor = Theme.ORANGE_ANCHOR.value
|
|
292
|
+
visited = Theme.ORANGE_ANCHOR_VISITED.value
|
|
293
|
+
css = f"""
|
|
294
|
+
a {{ color: {anchor}; text-decoration: underline; }}
|
|
295
|
+
a:visited {{ color: {visited}; }}
|
|
296
|
+
"""
|
|
297
|
+
else:
|
|
298
|
+
css = "" # Default to no custom styling for links (system or light theme)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
# Apply to the editor (QTextEdit or any other relevant widgets)
|
|
302
|
+
self.editor.document().setDefaultStyleSheet(css)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
self.search.document().setDefaultStyleSheet(css)
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
def _apply_calendar_theme(self, theme: Theme):
|
|
312
|
+
"""Use orange accents on the calendar in dark mode only."""
|
|
313
|
+
app_pal = QApplication.instance().palette()
|
|
314
|
+
|
|
315
|
+
if theme == Theme.DARK:
|
|
316
|
+
highlight = QColor(Theme.ORANGE_ANCHOR.value)
|
|
317
|
+
black = QColor(0, 0, 0)
|
|
318
|
+
|
|
319
|
+
highlight_css = Theme.ORANGE_ANCHOR.value
|
|
320
|
+
|
|
321
|
+
# Per-widget palette: selection color inside the date grid
|
|
322
|
+
pal = self.calendar.palette()
|
|
323
|
+
pal.setColor(QPalette.Highlight, highlight)
|
|
324
|
+
pal.setColor(QPalette.HighlightedText, black)
|
|
325
|
+
self.calendar.setPalette(pal)
|
|
326
|
+
|
|
327
|
+
# Stylesheet: nav bar + selected-day background
|
|
328
|
+
self.calendar.setStyleSheet(
|
|
329
|
+
f"""
|
|
330
|
+
QWidget#qt_calendar_navigationbar {{ background-color: {highlight_css}; }}
|
|
331
|
+
QCalendarWidget QToolButton {{ color: black; }}
|
|
332
|
+
QCalendarWidget QToolButton:hover {{ background-color: rgba(255,165,0,0.20); }}
|
|
333
|
+
/* Selected day color in the table view */
|
|
334
|
+
QCalendarWidget QTableView:enabled {{
|
|
335
|
+
selection-background-color: {highlight_css};
|
|
336
|
+
selection-color: black;
|
|
337
|
+
}}
|
|
338
|
+
/* Optional: keep weekday header readable */
|
|
339
|
+
QCalendarWidget QTableView QHeaderView::section {{
|
|
340
|
+
background: transparent;
|
|
341
|
+
color: palette(windowText);
|
|
342
|
+
}}
|
|
343
|
+
"""
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
# Back to app defaults in light/system
|
|
347
|
+
self.calendar.setPalette(app_pal)
|
|
348
|
+
self.calendar.setStyleSheet("")
|
|
349
|
+
|
|
350
|
+
# Keep weekend text color in sync with the current palette
|
|
351
|
+
self._apply_calendar_text_colors()
|
|
352
|
+
self.calendar.update()
|
|
353
|
+
|
|
354
|
+
def _apply_calendar_text_colors(self):
|
|
355
|
+
pal = self.palette()
|
|
356
|
+
txt = pal.windowText().color()
|
|
357
|
+
fmt = QTextCharFormat()
|
|
358
|
+
fmt.setForeground(txt)
|
|
359
|
+
# Use normal text color for weekends
|
|
360
|
+
self.calendar.setWeekdayTextFormat(Qt.Saturday, fmt)
|
|
361
|
+
self.calendar.setWeekdayTextFormat(Qt.Sunday, fmt)
|
|
362
|
+
|
|
363
|
+
def _on_search_dates_changed(self, date_strs: list[str]):
|
|
364
|
+
dates = set()
|
|
365
|
+
for ds in date_strs or []:
|
|
366
|
+
qd = QDate.fromString(ds, "yyyy-MM-dd")
|
|
367
|
+
if qd.isValid():
|
|
368
|
+
dates.add(qd)
|
|
369
|
+
self._apply_search_highlights(dates)
|
|
370
|
+
|
|
371
|
+
def _apply_search_highlights(self, dates: set):
|
|
372
|
+
pal = self.palette()
|
|
373
|
+
base = pal.base().color()
|
|
374
|
+
hi = pal.highlight().color()
|
|
375
|
+
# Blend highlight with base so it looks soft in both modes
|
|
376
|
+
blend = QColor(
|
|
377
|
+
(2 * hi.red() + base.red()) // 3,
|
|
378
|
+
(2 * hi.green() + base.green()) // 3,
|
|
379
|
+
(2 * hi.blue() + base.blue()) // 3,
|
|
380
|
+
)
|
|
381
|
+
yellow = QBrush(blend)
|
|
382
|
+
old = getattr(self, "_search_highlighted_dates", set())
|
|
383
|
+
|
|
384
|
+
for d in old - dates: # clear removed
|
|
385
|
+
fmt = self.calendar.dateTextFormat(d)
|
|
386
|
+
fmt.setBackground(Qt.transparent)
|
|
387
|
+
self.calendar.setDateTextFormat(d, fmt)
|
|
388
|
+
|
|
389
|
+
for d in dates: # apply new/current
|
|
390
|
+
fmt = self.calendar.dateTextFormat(d)
|
|
391
|
+
fmt.setBackground(yellow)
|
|
392
|
+
self.calendar.setDateTextFormat(d, fmt)
|
|
393
|
+
|
|
394
|
+
self._search_highlighted_dates = dates
|
|
395
|
+
|
|
396
|
+
def _refresh_calendar_marks(self):
|
|
397
|
+
"""Make days with entries bold, but keep any search highlight backgrounds."""
|
|
398
|
+
for d in getattr(self, "_marked_dates", set()):
|
|
399
|
+
fmt = self.calendar.dateTextFormat(d)
|
|
400
|
+
fmt.setFontWeight(QFont.Weight.Normal) # remove bold only
|
|
401
|
+
self.calendar.setDateTextFormat(d, fmt)
|
|
402
|
+
self._marked_dates = set()
|
|
403
|
+
try:
|
|
404
|
+
for date_iso in self.db.dates_with_content():
|
|
405
|
+
qd = QDate.fromString(date_iso, "yyyy-MM-dd")
|
|
406
|
+
if qd.isValid():
|
|
407
|
+
fmt = self.calendar.dateTextFormat(qd)
|
|
408
|
+
fmt.setFontWeight(QFont.Weight.Bold) # add bold only
|
|
409
|
+
self.calendar.setDateTextFormat(qd, fmt)
|
|
410
|
+
self._marked_dates.add(qd)
|
|
411
|
+
except Exception:
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
# --- UI handlers ---------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
def _sync_toolbar(self):
|
|
417
|
+
fmt = self.editor.currentCharFormat()
|
|
418
|
+
c = self.editor.textCursor()
|
|
419
|
+
bf = c.blockFormat()
|
|
420
|
+
|
|
421
|
+
# Block signals so setChecked() doesn't re-trigger actions
|
|
422
|
+
QSignalBlocker(self.toolBar.actBold)
|
|
423
|
+
QSignalBlocker(self.toolBar.actItalic)
|
|
424
|
+
QSignalBlocker(self.toolBar.actUnderline)
|
|
425
|
+
QSignalBlocker(self.toolBar.actStrike)
|
|
426
|
+
|
|
427
|
+
self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold)
|
|
428
|
+
self.toolBar.actItalic.setChecked(fmt.fontItalic())
|
|
429
|
+
self.toolBar.actUnderline.setChecked(fmt.fontUnderline())
|
|
430
|
+
self.toolBar.actStrike.setChecked(fmt.fontStrikeOut())
|
|
431
|
+
|
|
432
|
+
# Headings: decide which to check by current point size
|
|
433
|
+
def _approx(a, b, eps=0.5): # small float tolerance
|
|
434
|
+
return abs(float(a) - float(b)) <= eps
|
|
435
|
+
|
|
436
|
+
cur_size = fmt.fontPointSize() or self.editor.font().pointSizeF()
|
|
437
|
+
|
|
438
|
+
bH1 = _approx(cur_size, 24)
|
|
439
|
+
bH2 = _approx(cur_size, 18)
|
|
440
|
+
bH3 = _approx(cur_size, 14)
|
|
441
|
+
|
|
442
|
+
QSignalBlocker(self.toolBar.actH1)
|
|
443
|
+
QSignalBlocker(self.toolBar.actH2)
|
|
444
|
+
QSignalBlocker(self.toolBar.actH3)
|
|
445
|
+
QSignalBlocker(self.toolBar.actNormal)
|
|
446
|
+
|
|
447
|
+
self.toolBar.actH1.setChecked(bH1)
|
|
448
|
+
self.toolBar.actH2.setChecked(bH2)
|
|
449
|
+
self.toolBar.actH3.setChecked(bH3)
|
|
450
|
+
self.toolBar.actNormal.setChecked(not (bH1 or bH2 or bH3))
|
|
451
|
+
|
|
452
|
+
# Lists
|
|
453
|
+
lst = c.currentList()
|
|
454
|
+
bullets_on = lst and lst.format().style() == QTextListFormat.Style.ListDisc
|
|
455
|
+
numbers_on = lst and lst.format().style() == QTextListFormat.Style.ListDecimal
|
|
456
|
+
QSignalBlocker(self.toolBar.actBullets)
|
|
457
|
+
QSignalBlocker(self.toolBar.actNumbers)
|
|
458
|
+
self.toolBar.actBullets.setChecked(bool(bullets_on))
|
|
459
|
+
self.toolBar.actNumbers.setChecked(bool(numbers_on))
|
|
460
|
+
|
|
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
|
+
def _load_selected_date(self, date_iso=False, extra_data=False):
|
|
475
|
+
if not date_iso:
|
|
476
|
+
date_iso = self._current_date_iso()
|
|
477
|
+
try:
|
|
478
|
+
text = self.db.get_entry(date_iso)
|
|
479
|
+
if extra_data:
|
|
480
|
+
# Wrap extra_data in a <p> tag for HTML rendering
|
|
481
|
+
extra_data_html = f"<p>{extra_data}</p>"
|
|
482
|
+
|
|
483
|
+
# Inject the extra_data before the closing </body></html>
|
|
484
|
+
modified = re.sub(r"(<\/body><\/html>)", extra_data_html + r"\1", text)
|
|
485
|
+
text = modified
|
|
486
|
+
self.editor.setHtml(text)
|
|
487
|
+
self._dirty = True
|
|
488
|
+
self._save_date(date_iso, True)
|
|
489
|
+
|
|
490
|
+
print("end")
|
|
491
|
+
except Exception as e:
|
|
492
|
+
QMessageBox.critical(self, "Read Error", str(e))
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
self.editor.blockSignals(True)
|
|
496
|
+
self.editor.setHtml(text)
|
|
497
|
+
self.editor.blockSignals(False)
|
|
498
|
+
|
|
499
|
+
self._dirty = False
|
|
500
|
+
# track which date the editor currently represents
|
|
501
|
+
self._active_date_iso = date_iso
|
|
502
|
+
qd = QDate.fromString(date_iso, "yyyy-MM-dd")
|
|
503
|
+
self.calendar.setSelectedDate(qd)
|
|
504
|
+
|
|
505
|
+
def _on_text_changed(self):
|
|
506
|
+
self._dirty = True
|
|
507
|
+
self._save_timer.start(5000) # autosave after idle
|
|
508
|
+
|
|
509
|
+
def _adjust_day(self, delta: int):
|
|
510
|
+
"""Move selection by delta days (negative for previous)."""
|
|
511
|
+
d = self.calendar.selectedDate().addDays(delta)
|
|
512
|
+
self.calendar.setSelectedDate(d)
|
|
513
|
+
|
|
514
|
+
def _adjust_today(self):
|
|
515
|
+
"""Jump to today."""
|
|
516
|
+
today = QDate.currentDate()
|
|
517
|
+
self.calendar.setSelectedDate(today)
|
|
518
|
+
|
|
519
|
+
def _load_yesterday_todos(self):
|
|
520
|
+
try:
|
|
521
|
+
if not self.cfg.move_todos:
|
|
522
|
+
return
|
|
523
|
+
yesterday_str = QDate.currentDate().addDays(-1).toString("yyyy-MM-dd")
|
|
524
|
+
text = self.db.get_entry(yesterday_str)
|
|
525
|
+
unchecked_items = []
|
|
526
|
+
|
|
527
|
+
# Regex to match the unchecked checkboxes and their associated text
|
|
528
|
+
checkbox_pattern = re.compile(
|
|
529
|
+
r"<span[^>]*>(☐)</span>\s*(.*?)</p>", re.DOTALL
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Find unchecked items and store them
|
|
533
|
+
for match in checkbox_pattern.finditer(text):
|
|
534
|
+
checkbox = match.group(1) # Either ☐ or ☑
|
|
535
|
+
item_text = match.group(2).strip() # The text after the checkbox
|
|
536
|
+
if checkbox == "☐": # If it's an unchecked checkbox (☐)
|
|
537
|
+
unchecked_items.append("☐ " + item_text) # Store the unchecked item
|
|
538
|
+
|
|
539
|
+
# Remove the unchecked items from yesterday's HTML content
|
|
540
|
+
if unchecked_items:
|
|
541
|
+
# This regex will find the entire checkbox line and remove it from the HTML content
|
|
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
|
|
550
|
+
self.db.save_new_version(
|
|
551
|
+
yesterday_str,
|
|
552
|
+
modified_text,
|
|
553
|
+
"Unchecked checkbox items moved to next day",
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Join unchecked items into a formatted string
|
|
557
|
+
unchecked_str = "\n".join(
|
|
558
|
+
[f"<p>{item}</p>" for item in unchecked_items]
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# Load the unchecked items into the current editor
|
|
562
|
+
self._load_selected_date(False, unchecked_str)
|
|
563
|
+
else:
|
|
564
|
+
return False
|
|
565
|
+
|
|
566
|
+
except Exception as e:
|
|
567
|
+
raise SystemError(e)
|
|
568
|
+
|
|
569
|
+
def _on_date_changed(self):
|
|
570
|
+
"""
|
|
571
|
+
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.
|
|
573
|
+
"""
|
|
574
|
+
# Stop pending autosave and persist current buffer if needed
|
|
575
|
+
try:
|
|
576
|
+
self._save_timer.stop()
|
|
577
|
+
except Exception:
|
|
578
|
+
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
|
+
|
|
585
|
+
def _save_date(self, date_iso: str, explicit: bool = False, note: str = "autosave"):
|
|
586
|
+
"""
|
|
587
|
+
Save editor contents into the given date. Shows status on success.
|
|
588
|
+
explicit=True means user invoked Save: show feedback even if nothing changed.
|
|
589
|
+
"""
|
|
590
|
+
if not self._dirty and not explicit:
|
|
591
|
+
return
|
|
592
|
+
text = self.editor.to_html_with_embedded_images()
|
|
593
|
+
try:
|
|
594
|
+
self.db.save_new_version(date_iso, text, note)
|
|
595
|
+
except Exception as e:
|
|
596
|
+
QMessageBox.critical(self, "Save Error", str(e))
|
|
597
|
+
return
|
|
598
|
+
self._dirty = False
|
|
599
|
+
self._refresh_calendar_marks()
|
|
600
|
+
# Feedback in the status bar
|
|
601
|
+
from datetime import datetime as _dt
|
|
602
|
+
|
|
603
|
+
self.statusBar().showMessage(
|
|
604
|
+
f"Saved {date_iso} at {_dt.now().strftime('%H:%M:%S')}", 2000
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
def _save_current(self, explicit: bool = False):
|
|
608
|
+
try:
|
|
609
|
+
self._save_timer.stop()
|
|
610
|
+
except Exception:
|
|
611
|
+
pass
|
|
612
|
+
if explicit:
|
|
613
|
+
# Prompt for a note
|
|
614
|
+
dlg = SaveDialog(self)
|
|
615
|
+
if dlg.exec() != QDialog.Accepted:
|
|
616
|
+
return
|
|
617
|
+
note = dlg.note_text()
|
|
618
|
+
else:
|
|
619
|
+
note = "autosave"
|
|
620
|
+
# Delegate to _save_date for the currently selected date
|
|
621
|
+
self._save_date(self._current_date_iso(), explicit, note)
|
|
622
|
+
try:
|
|
623
|
+
self._save_timer.start()
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
|
|
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
|
+
# ----------- Settings handler ------------#
|
|
648
|
+
def _open_settings(self):
|
|
649
|
+
dlg = SettingsDialog(self.cfg, self.db, self)
|
|
650
|
+
if dlg.exec() != QDialog.Accepted:
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
new_cfg = dlg.config
|
|
654
|
+
old_path = self.cfg.path
|
|
655
|
+
|
|
656
|
+
# Update in-memory config from the dialog
|
|
657
|
+
self.cfg.path = new_cfg.path
|
|
658
|
+
self.cfg.key = new_cfg.key
|
|
659
|
+
self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes)
|
|
660
|
+
self.cfg.theme = getattr(new_cfg, "theme", self.cfg.theme)
|
|
661
|
+
self.cfg.move_todos = getattr(new_cfg, "move_todos", self.cfg.move_todos)
|
|
662
|
+
|
|
663
|
+
# Persist once
|
|
664
|
+
save_db_config(self.cfg)
|
|
665
|
+
|
|
666
|
+
# Apply idle setting immediately (restart the timer with new interval if it changed)
|
|
667
|
+
self._apply_idle_minutes(self.cfg.idle_minutes)
|
|
668
|
+
|
|
669
|
+
# If the DB path changed, reconnect
|
|
670
|
+
if self.cfg.path != old_path:
|
|
671
|
+
self.db.close()
|
|
672
|
+
if not self._prompt_for_key_until_valid(first_time=False):
|
|
673
|
+
QMessageBox.warning(
|
|
674
|
+
self, "Reopen failed", "Could not unlock database at new path."
|
|
675
|
+
)
|
|
676
|
+
return
|
|
677
|
+
self._load_selected_date()
|
|
678
|
+
self._refresh_calendar_marks()
|
|
679
|
+
|
|
680
|
+
# ------------ Window positioning --------------- #
|
|
681
|
+
def _restore_window_position(self):
|
|
682
|
+
geom = self.settings.value("main/geometry", None)
|
|
683
|
+
state = self.settings.value("main/windowState", None)
|
|
684
|
+
was_max = self.settings.value("main/maximized", False, type=bool)
|
|
685
|
+
|
|
686
|
+
if geom is not None:
|
|
687
|
+
self.restoreGeometry(geom)
|
|
688
|
+
if state is not None:
|
|
689
|
+
self.restoreState(state)
|
|
690
|
+
if not self._rect_on_any_screen(self.frameGeometry()):
|
|
691
|
+
self._move_to_cursor_screen_center()
|
|
692
|
+
else:
|
|
693
|
+
# First run: place window on the screen where the mouse cursor is.
|
|
694
|
+
self._move_to_cursor_screen_center()
|
|
695
|
+
|
|
696
|
+
# If it was maximized, do that AFTER the window exists in the event loop.
|
|
697
|
+
if was_max:
|
|
698
|
+
QTimer.singleShot(0, self.showMaximized)
|
|
699
|
+
|
|
700
|
+
def _rect_on_any_screen(self, rect):
|
|
701
|
+
for sc in QGuiApplication.screens():
|
|
702
|
+
if sc.availableGeometry().intersects(rect):
|
|
703
|
+
return True
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
def _move_to_cursor_screen_center(self):
|
|
707
|
+
screen = (
|
|
708
|
+
QGuiApplication.screenAt(QCursor.pos()) or QGuiApplication.primaryScreen()
|
|
709
|
+
)
|
|
710
|
+
r = screen.availableGeometry()
|
|
711
|
+
# Center the window in that screen’s available area
|
|
712
|
+
self.move(r.center() - self.rect().center())
|
|
713
|
+
|
|
714
|
+
# ----------------- Export handler ----------------- #
|
|
715
|
+
@Slot()
|
|
716
|
+
def _export(self):
|
|
717
|
+
warning_title = "Unencrypted export"
|
|
718
|
+
warning_message = """
|
|
719
|
+
Exporting the database will be unencrypted!
|
|
720
|
+
|
|
721
|
+
Are you sure you want to continue?
|
|
722
|
+
|
|
723
|
+
If you want an encrypted backup, choose Backup instead of Export.
|
|
724
|
+
"""
|
|
725
|
+
dlg = QMessageBox()
|
|
726
|
+
dlg.setWindowTitle(warning_title)
|
|
727
|
+
dlg.setText(warning_message)
|
|
728
|
+
dlg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
|
729
|
+
dlg.setIcon(QMessageBox.Warning)
|
|
730
|
+
dlg.show()
|
|
731
|
+
dlg.adjustSize()
|
|
732
|
+
if dlg.exec() != QMessageBox.Yes:
|
|
733
|
+
return False
|
|
734
|
+
|
|
735
|
+
filters = (
|
|
736
|
+
"Text (*.txt);;"
|
|
737
|
+
"JSON (*.json);;"
|
|
738
|
+
"CSV (*.csv);;"
|
|
739
|
+
"HTML (*.html);;"
|
|
740
|
+
"Markdown (*.md);;"
|
|
741
|
+
"SQL (*.sql);;"
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
start_dir = os.path.join(os.path.expanduser("~"), "Documents")
|
|
745
|
+
filename, selected_filter = QFileDialog.getSaveFileName(
|
|
746
|
+
self, "Export entries", start_dir, filters
|
|
747
|
+
)
|
|
748
|
+
if not filename:
|
|
749
|
+
return # user cancelled
|
|
750
|
+
|
|
751
|
+
default_ext = {
|
|
752
|
+
"Text (*.txt)": ".txt",
|
|
753
|
+
"JSON (*.json)": ".json",
|
|
754
|
+
"CSV (*.csv)": ".csv",
|
|
755
|
+
"HTML (*.html)": ".html",
|
|
756
|
+
"Markdown (*.md)": ".md",
|
|
757
|
+
"SQL (*.sql)": ".sql",
|
|
758
|
+
}.get(selected_filter, ".txt")
|
|
759
|
+
|
|
760
|
+
if not Path(filename).suffix:
|
|
761
|
+
filename += default_ext
|
|
762
|
+
|
|
763
|
+
try:
|
|
764
|
+
entries = self.db.get_all_entries()
|
|
765
|
+
if selected_filter.startswith("Text"):
|
|
766
|
+
self.db.export_txt(entries, filename)
|
|
767
|
+
elif selected_filter.startswith("JSON"):
|
|
768
|
+
self.db.export_json(entries, filename)
|
|
769
|
+
elif selected_filter.startswith("CSV"):
|
|
770
|
+
self.db.export_csv(entries, filename)
|
|
771
|
+
elif selected_filter.startswith("HTML"):
|
|
772
|
+
self.db.export_html(entries, filename)
|
|
773
|
+
elif selected_filter.startswith("Markdown"):
|
|
774
|
+
self.db.export_markdown(entries, filename)
|
|
775
|
+
elif selected_filter.startswith("SQL"):
|
|
776
|
+
self.db.export_sql(filename)
|
|
777
|
+
else:
|
|
778
|
+
self.db.export_by_extension(filename)
|
|
779
|
+
|
|
780
|
+
QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}")
|
|
781
|
+
except Exception as e:
|
|
782
|
+
QMessageBox.critical(self, "Export failed", str(e))
|
|
783
|
+
|
|
784
|
+
# ----------------- Backup handler ----------------- #
|
|
785
|
+
@Slot()
|
|
786
|
+
def _backup(self):
|
|
787
|
+
filters = "SQLCipher (*.db);;"
|
|
788
|
+
|
|
789
|
+
now = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
|
790
|
+
start_dir = os.path.join(
|
|
791
|
+
os.path.expanduser("~"), "Documents", f"bouquin_backup_{now}.db"
|
|
792
|
+
)
|
|
793
|
+
filename, selected_filter = QFileDialog.getSaveFileName(
|
|
794
|
+
self, "Backup encrypted notebook", start_dir, filters
|
|
795
|
+
)
|
|
796
|
+
if not filename:
|
|
797
|
+
return # user cancelled
|
|
798
|
+
|
|
799
|
+
default_ext = {
|
|
800
|
+
"SQLCipher (*.db)": ".db",
|
|
801
|
+
}.get(selected_filter, ".db")
|
|
802
|
+
|
|
803
|
+
if not Path(filename).suffix:
|
|
804
|
+
filename += default_ext
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
if selected_filter.startswith("SQL"):
|
|
808
|
+
self.db.export_sqlcipher(filename)
|
|
809
|
+
QMessageBox.information(
|
|
810
|
+
self, "Backup complete", f"Saved to:\n{filename}"
|
|
811
|
+
)
|
|
812
|
+
except Exception as e:
|
|
813
|
+
QMessageBox.critical(self, "Backup failed", str(e))
|
|
814
|
+
|
|
815
|
+
# ----------------- Help handlers ----------------- #
|
|
816
|
+
|
|
817
|
+
def _open_docs(self):
|
|
818
|
+
url_str = "https://git.mig5.net/mig5/bouquin/wiki/Help"
|
|
819
|
+
url = QUrl.fromUserInput(url_str)
|
|
820
|
+
if not QDesktopServices.openUrl(url):
|
|
821
|
+
QMessageBox.warning(
|
|
822
|
+
self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}"
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
def _open_bugs(self):
|
|
826
|
+
url_str = "https://nr.mig5.net/forms/mig5/contact"
|
|
827
|
+
url = QUrl.fromUserInput(url_str)
|
|
828
|
+
if not QDesktopServices.openUrl(url):
|
|
829
|
+
QMessageBox.warning(
|
|
830
|
+
self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}"
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# ----------------- Idle handlers ----------------- #
|
|
834
|
+
def _apply_idle_minutes(self, minutes: int):
|
|
835
|
+
minutes = max(0, int(minutes))
|
|
836
|
+
if not hasattr(self, "_idle_timer"):
|
|
837
|
+
return
|
|
838
|
+
if minutes == 0:
|
|
839
|
+
self._idle_timer.stop()
|
|
840
|
+
# If you’re currently locked, unlock when user disables the timer:
|
|
841
|
+
if getattr(self, "_locked", False):
|
|
842
|
+
try:
|
|
843
|
+
self._locked = False
|
|
844
|
+
if hasattr(self, "_lock_overlay"):
|
|
845
|
+
self._lock_overlay.hide()
|
|
846
|
+
except Exception:
|
|
847
|
+
pass
|
|
848
|
+
else:
|
|
849
|
+
self._idle_timer.setInterval(minutes * 60 * 1000)
|
|
850
|
+
if not getattr(self, "_locked", False):
|
|
851
|
+
self._idle_timer.start()
|
|
852
|
+
|
|
853
|
+
def eventFilter(self, obj, event):
|
|
854
|
+
if event.type() == QEvent.KeyPress and not self._locked:
|
|
855
|
+
self._idle_timer.start()
|
|
856
|
+
return super().eventFilter(obj, event)
|
|
857
|
+
|
|
858
|
+
def _enter_lock(self):
|
|
859
|
+
if self._locked:
|
|
860
|
+
return
|
|
861
|
+
self._locked = True
|
|
862
|
+
if self.menuBar():
|
|
863
|
+
self.menuBar().setEnabled(False)
|
|
864
|
+
if self.statusBar():
|
|
865
|
+
self.statusBar().setEnabled(False)
|
|
866
|
+
tb = getattr(self, "toolBar", None)
|
|
867
|
+
if tb:
|
|
868
|
+
tb.setEnabled(False)
|
|
869
|
+
self._lock_overlay.show()
|
|
870
|
+
self._lock_overlay.raise_()
|
|
871
|
+
|
|
872
|
+
@Slot()
|
|
873
|
+
def _on_unlock_clicked(self):
|
|
874
|
+
try:
|
|
875
|
+
ok = self._prompt_for_key_until_valid(first_time=False)
|
|
876
|
+
except Exception as e:
|
|
877
|
+
QMessageBox.critical(self, "Unlock failed", str(e))
|
|
878
|
+
return
|
|
879
|
+
if ok:
|
|
880
|
+
self._locked = False
|
|
881
|
+
self._lock_overlay.hide()
|
|
882
|
+
if self.menuBar():
|
|
883
|
+
self.menuBar().setEnabled(True)
|
|
884
|
+
if self.statusBar():
|
|
885
|
+
self.statusBar().setEnabled(True)
|
|
886
|
+
tb = getattr(self, "toolBar", None)
|
|
887
|
+
if tb:
|
|
888
|
+
tb.setEnabled(True)
|
|
889
|
+
self._idle_timer.start()
|
|
890
|
+
|
|
891
|
+
# ----------------- Close handlers ----------------- #
|
|
892
|
+
def closeEvent(self, event):
|
|
893
|
+
try:
|
|
894
|
+
# Save window position
|
|
895
|
+
self.settings.setValue("main/geometry", self.saveGeometry())
|
|
896
|
+
self.settings.setValue("main/windowState", self.saveState())
|
|
897
|
+
self.settings.setValue("main/maximized", self.isMaximized())
|
|
898
|
+
|
|
899
|
+
# Ensure we save any last pending edits to the db
|
|
900
|
+
self._save_current()
|
|
901
|
+
self.db.close()
|
|
902
|
+
except Exception:
|
|
903
|
+
pass
|
|
904
|
+
super().closeEvent(event)
|