bouquin 0.1.3__tar.gz → 0.1.4__tar.gz
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-0.1.3 → bouquin-0.1.4}/PKG-INFO +1 -1
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/db.py +1 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/main_window.py +190 -29
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/settings.py +3 -1
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/settings_dialog.py +47 -14
- {bouquin-0.1.3 → bouquin-0.1.4}/pyproject.toml +1 -1
- {bouquin-0.1.3 → bouquin-0.1.4}/LICENSE +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/README.md +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/__init__.py +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/__main__.py +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/editor.py +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/key_prompt.py +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/main.py +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/search.py +0 -0
- {bouquin-0.1.3 → bouquin-0.1.4}/bouquin/toolbar.py +0 -0
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from PySide6.QtCore import QDate, QTimer, Qt, QSettings, Slot, QUrl
|
|
7
|
+
from PySide6.QtCore import QDate, QTimer, Qt, QSettings, Slot, QUrl, QEvent
|
|
8
8
|
from PySide6.QtGui import (
|
|
9
9
|
QAction,
|
|
10
10
|
QCursor,
|
|
@@ -17,8 +17,10 @@ from PySide6.QtWidgets import (
|
|
|
17
17
|
QCalendarWidget,
|
|
18
18
|
QDialog,
|
|
19
19
|
QFileDialog,
|
|
20
|
+
QLabel,
|
|
20
21
|
QMainWindow,
|
|
21
22
|
QMessageBox,
|
|
23
|
+
QPushButton,
|
|
22
24
|
QSizePolicy,
|
|
23
25
|
QSplitter,
|
|
24
26
|
QVBoxLayout,
|
|
@@ -34,6 +36,61 @@ from .settings_dialog import SettingsDialog
|
|
|
34
36
|
from .toolbar import ToolBar
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
class _LockOverlay(QWidget):
|
|
40
|
+
def __init__(self, parent: QWidget, on_unlock: callable):
|
|
41
|
+
super().__init__(parent)
|
|
42
|
+
self.setObjectName("LockOverlay")
|
|
43
|
+
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
44
|
+
self.setFocusPolicy(Qt.StrongFocus)
|
|
45
|
+
self.setGeometry(parent.rect())
|
|
46
|
+
|
|
47
|
+
self.setStyleSheet(
|
|
48
|
+
"""
|
|
49
|
+
#LockOverlay { background-color: #ccc; }
|
|
50
|
+
#LockOverlay QLabel { color: #fff; font-size: 18px; }
|
|
51
|
+
#LockOverlay QPushButton {
|
|
52
|
+
background-color: #f2f2f2;
|
|
53
|
+
color: #000;
|
|
54
|
+
padding: 6px 14px;
|
|
55
|
+
border: 1px solid #808080;
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
}
|
|
59
|
+
#LockOverlay QPushButton:hover { background-color: #ffffff; }
|
|
60
|
+
#LockOverlay QPushButton:pressed { background-color: #e6e6e6; }
|
|
61
|
+
"""
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
lay = QVBoxLayout(self)
|
|
65
|
+
lay.addStretch(1)
|
|
66
|
+
|
|
67
|
+
msg = QLabel("Locked due to inactivity")
|
|
68
|
+
msg.setAlignment(Qt.AlignCenter)
|
|
69
|
+
|
|
70
|
+
self._btn = QPushButton("Unlock")
|
|
71
|
+
self._btn.setFixedWidth(200)
|
|
72
|
+
self._btn.setCursor(Qt.PointingHandCursor)
|
|
73
|
+
self._btn.setAutoDefault(True)
|
|
74
|
+
self._btn.setDefault(True)
|
|
75
|
+
self._btn.clicked.connect(on_unlock)
|
|
76
|
+
|
|
77
|
+
lay.addWidget(msg, 0, Qt.AlignCenter)
|
|
78
|
+
lay.addWidget(self._btn, 0, Qt.AlignCenter)
|
|
79
|
+
lay.addStretch(1)
|
|
80
|
+
|
|
81
|
+
self.hide() # start hidden
|
|
82
|
+
|
|
83
|
+
# keep overlay sized with its parent
|
|
84
|
+
def eventFilter(self, obj, event):
|
|
85
|
+
if obj is self.parent() and event.type() in (QEvent.Resize, QEvent.Show):
|
|
86
|
+
self.setGeometry(obj.rect())
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def showEvent(self, e):
|
|
90
|
+
super().showEvent(e)
|
|
91
|
+
self._btn.setFocus()
|
|
92
|
+
|
|
93
|
+
|
|
37
94
|
class MainWindow(QMainWindow):
|
|
38
95
|
def __init__(self):
|
|
39
96
|
super().__init__()
|
|
@@ -77,18 +134,18 @@ class MainWindow(QMainWindow):
|
|
|
77
134
|
self.editor = Editor()
|
|
78
135
|
|
|
79
136
|
# Toolbar for controlling styling
|
|
80
|
-
|
|
81
|
-
self.addToolBar(
|
|
137
|
+
self.toolBar = ToolBar()
|
|
138
|
+
self.addToolBar(self.toolBar)
|
|
82
139
|
# Wire toolbar intents to editor methods
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
140
|
+
self.toolBar.boldRequested.connect(self.editor.apply_weight)
|
|
141
|
+
self.toolBar.italicRequested.connect(self.editor.apply_italic)
|
|
142
|
+
self.toolBar.underlineRequested.connect(self.editor.apply_underline)
|
|
143
|
+
self.toolBar.strikeRequested.connect(self.editor.apply_strikethrough)
|
|
144
|
+
self.toolBar.codeRequested.connect(self.editor.apply_code)
|
|
145
|
+
self.toolBar.headingRequested.connect(self.editor.apply_heading)
|
|
146
|
+
self.toolBar.bulletsRequested.connect(self.editor.toggle_bullets)
|
|
147
|
+
self.toolBar.numbersRequested.connect(self.editor.toggle_numbers)
|
|
148
|
+
self.toolBar.alignRequested.connect(self.editor.setAlignment)
|
|
92
149
|
|
|
93
150
|
split = QSplitter()
|
|
94
151
|
split.addWidget(left_panel)
|
|
@@ -100,6 +157,24 @@ class MainWindow(QMainWindow):
|
|
|
100
157
|
lay.addWidget(split)
|
|
101
158
|
self.setCentralWidget(container)
|
|
102
159
|
|
|
160
|
+
# Idle lock setup
|
|
161
|
+
self._idle_timer = QTimer(self)
|
|
162
|
+
self._idle_timer.setSingleShot(True)
|
|
163
|
+
self._idle_timer.timeout.connect(self._enter_lock)
|
|
164
|
+
self._apply_idle_minutes(getattr(self.cfg, "idle_minutes", 15))
|
|
165
|
+
self._idle_timer.start()
|
|
166
|
+
|
|
167
|
+
# full-window overlay that sits on top of the central widget
|
|
168
|
+
self._lock_overlay = _LockOverlay(self.centralWidget(), self._on_unlock_clicked)
|
|
169
|
+
self.centralWidget().installEventFilter(self._lock_overlay)
|
|
170
|
+
|
|
171
|
+
self._locked = False
|
|
172
|
+
|
|
173
|
+
# reset idle timer on any key press anywhere in the app
|
|
174
|
+
from PySide6.QtWidgets import QApplication
|
|
175
|
+
|
|
176
|
+
QApplication.instance().installEventFilter(self)
|
|
177
|
+
|
|
103
178
|
# Status bar for feedback
|
|
104
179
|
self.statusBar().showMessage("Ready", 800)
|
|
105
180
|
|
|
@@ -155,6 +230,12 @@ class MainWindow(QMainWindow):
|
|
|
155
230
|
act_docs.triggered.connect(self._open_docs)
|
|
156
231
|
help_menu.addAction(act_docs)
|
|
157
232
|
self.addAction(act_docs)
|
|
233
|
+
act_bugs = QAction("Report a bug", self)
|
|
234
|
+
act_bugs.setShortcut("Ctrl+R")
|
|
235
|
+
act_bugs.setShortcutContext(Qt.ApplicationShortcut)
|
|
236
|
+
act_bugs.triggered.connect(self._open_bugs)
|
|
237
|
+
help_menu.addAction(act_bugs)
|
|
238
|
+
self.addAction(act_bugs)
|
|
158
239
|
|
|
159
240
|
# Autosave
|
|
160
241
|
self._dirty = False
|
|
@@ -305,21 +386,33 @@ class MainWindow(QMainWindow):
|
|
|
305
386
|
|
|
306
387
|
def _open_settings(self):
|
|
307
388
|
dlg = SettingsDialog(self.cfg, self.db, self)
|
|
308
|
-
if dlg.exec()
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
389
|
+
if dlg.exec() != QDialog.Accepted:
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
new_cfg = dlg.config
|
|
393
|
+
old_path = self.cfg.path
|
|
394
|
+
|
|
395
|
+
# Update in-memory config from the dialog
|
|
396
|
+
self.cfg.path = new_cfg.path
|
|
397
|
+
self.cfg.key = new_cfg.key
|
|
398
|
+
self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes)
|
|
399
|
+
|
|
400
|
+
# Persist once
|
|
401
|
+
save_db_config(self.cfg)
|
|
402
|
+
|
|
403
|
+
# Apply idle setting immediately (restart the timer with new interval if it changed)
|
|
404
|
+
self._apply_idle_minutes(self.cfg.idle_minutes)
|
|
405
|
+
|
|
406
|
+
# If the DB path changed, reconnect
|
|
407
|
+
if self.cfg.path != old_path:
|
|
408
|
+
self.db.close()
|
|
409
|
+
if not self._prompt_for_key_until_valid(first_time=False):
|
|
410
|
+
QMessageBox.warning(
|
|
411
|
+
self, "Reopen failed", "Could not unlock database at new path."
|
|
412
|
+
)
|
|
413
|
+
return
|
|
414
|
+
self._load_selected_date()
|
|
415
|
+
self._refresh_calendar_marks()
|
|
323
416
|
|
|
324
417
|
def _restore_window_position(self):
|
|
325
418
|
geom = self.settings.value("main/geometry", None)
|
|
@@ -402,9 +495,77 @@ class MainWindow(QMainWindow):
|
|
|
402
495
|
url_str = "https://git.mig5.net/mig5/bouquin/wiki/Help"
|
|
403
496
|
url = QUrl.fromUserInput(url_str)
|
|
404
497
|
if not QDesktopServices.openUrl(url):
|
|
405
|
-
QMessageBox.warning(
|
|
406
|
-
|
|
498
|
+
QMessageBox.warning(
|
|
499
|
+
self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}"
|
|
500
|
+
)
|
|
407
501
|
|
|
502
|
+
def _open_bugs(self):
|
|
503
|
+
url_str = "https://nr.mig5.net/forms/mig5/contact"
|
|
504
|
+
url = QUrl.fromUserInput(url_str)
|
|
505
|
+
if not QDesktopServices.openUrl(url):
|
|
506
|
+
QMessageBox.warning(
|
|
507
|
+
self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Idle handlers
|
|
511
|
+
def _apply_idle_minutes(self, minutes: int):
|
|
512
|
+
minutes = max(0, int(minutes))
|
|
513
|
+
if not hasattr(self, "_idle_timer"):
|
|
514
|
+
return
|
|
515
|
+
if minutes == 0:
|
|
516
|
+
self._idle_timer.stop()
|
|
517
|
+
# If you’re currently locked, unlock when user disables the timer:
|
|
518
|
+
if getattr(self, "_locked", False):
|
|
519
|
+
try:
|
|
520
|
+
self._locked = False
|
|
521
|
+
if hasattr(self, "_lock_overlay"):
|
|
522
|
+
self._lock_overlay.hide()
|
|
523
|
+
except Exception:
|
|
524
|
+
pass
|
|
525
|
+
else:
|
|
526
|
+
self._idle_timer.setInterval(minutes * 60 * 1000)
|
|
527
|
+
if not getattr(self, "_locked", False):
|
|
528
|
+
self._idle_timer.start()
|
|
529
|
+
|
|
530
|
+
def eventFilter(self, obj, event):
|
|
531
|
+
if event.type() == QEvent.KeyPress and not self._locked:
|
|
532
|
+
self._idle_timer.start()
|
|
533
|
+
return super().eventFilter(obj, event)
|
|
534
|
+
|
|
535
|
+
def _enter_lock(self):
|
|
536
|
+
if self._locked:
|
|
537
|
+
return
|
|
538
|
+
self._locked = True
|
|
539
|
+
if self.menuBar():
|
|
540
|
+
self.menuBar().setEnabled(False)
|
|
541
|
+
if self.statusBar():
|
|
542
|
+
self.statusBar().setEnabled(False)
|
|
543
|
+
tb = getattr(self, "toolBar", None)
|
|
544
|
+
if tb:
|
|
545
|
+
tb.setEnabled(False)
|
|
546
|
+
self._lock_overlay.show()
|
|
547
|
+
self._lock_overlay.raise_()
|
|
548
|
+
|
|
549
|
+
@Slot()
|
|
550
|
+
def _on_unlock_clicked(self):
|
|
551
|
+
try:
|
|
552
|
+
ok = self._prompt_for_key_until_valid(first_time=False)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
QMessageBox.critical(self, "Unlock failed", str(e))
|
|
555
|
+
return
|
|
556
|
+
if ok:
|
|
557
|
+
self._locked = False
|
|
558
|
+
self._lock_overlay.hide()
|
|
559
|
+
if self.menuBar():
|
|
560
|
+
self.menuBar().setEnabled(True)
|
|
561
|
+
if self.statusBar():
|
|
562
|
+
self.statusBar().setEnabled(True)
|
|
563
|
+
tb = getattr(self, "toolBar", None)
|
|
564
|
+
if tb:
|
|
565
|
+
tb.setEnabled(True)
|
|
566
|
+
self._idle_timer.start()
|
|
567
|
+
|
|
568
|
+
# Close app handler - save window position and database
|
|
408
569
|
def closeEvent(self, event):
|
|
409
570
|
try:
|
|
410
571
|
# Save window position
|
|
@@ -22,10 +22,12 @@ def load_db_config() -> DBConfig:
|
|
|
22
22
|
s = get_settings()
|
|
23
23
|
path = Path(s.value("db/path", str(default_db_path())))
|
|
24
24
|
key = s.value("db/key", "")
|
|
25
|
-
|
|
25
|
+
idle = s.value("db/idle_minutes", 15, type=int)
|
|
26
|
+
return DBConfig(path=path, key=key, idle_minutes=idle)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def save_db_config(cfg: DBConfig) -> None:
|
|
29
30
|
s = get_settings()
|
|
30
31
|
s.setValue("db/path", str(cfg.path))
|
|
31
32
|
s.setValue("db/key", str(cfg.key))
|
|
33
|
+
s.setValue("db/idle_minutes", str(cfg.idle_minutes))
|
|
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import (
|
|
|
17
17
|
QFileDialog,
|
|
18
18
|
QDialogButtonBox,
|
|
19
19
|
QSizePolicy,
|
|
20
|
+
QSpinBox,
|
|
20
21
|
QMessageBox,
|
|
21
22
|
)
|
|
22
23
|
from PySide6.QtCore import Qt, Slot
|
|
@@ -56,7 +57,7 @@ class SettingsDialog(QDialog):
|
|
|
56
57
|
form.addRow("Database path", path_row)
|
|
57
58
|
|
|
58
59
|
# Encryption settings
|
|
59
|
-
enc_group = QGroupBox("Encryption")
|
|
60
|
+
enc_group = QGroupBox("Encryption and Privacy")
|
|
60
61
|
enc = QVBoxLayout(enc_group)
|
|
61
62
|
enc.setContentsMargins(12, 8, 12, 12)
|
|
62
63
|
enc.setSpacing(6)
|
|
@@ -64,10 +65,8 @@ class SettingsDialog(QDialog):
|
|
|
64
65
|
# Checkbox to remember key
|
|
65
66
|
self.save_key_btn = QCheckBox("Remember key")
|
|
66
67
|
current_settings = load_db_config()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
else:
|
|
70
|
-
self.save_key_btn.setChecked(False)
|
|
68
|
+
self.key = current_settings.key or ""
|
|
69
|
+
self.save_key_btn.setChecked(bool(self.key))
|
|
71
70
|
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
|
72
71
|
self.save_key_btn.toggled.connect(self.save_key_btn_clicked)
|
|
73
72
|
enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft)
|
|
@@ -100,6 +99,31 @@ class SettingsDialog(QDialog):
|
|
|
100
99
|
self.rekey_btn.clicked.connect(self._change_key)
|
|
101
100
|
enc.addWidget(self.rekey_btn, 0, Qt.AlignLeft)
|
|
102
101
|
|
|
102
|
+
self.idle_spin = QSpinBox()
|
|
103
|
+
self.idle_spin.setRange(0, 240)
|
|
104
|
+
self.idle_spin.setSingleStep(1)
|
|
105
|
+
self.idle_spin.setAccelerated(True)
|
|
106
|
+
self.idle_spin.setSuffix(" min")
|
|
107
|
+
self.idle_spin.setSpecialValueText("Never")
|
|
108
|
+
self.idle_spin.setValue(getattr(cfg, "idle_minutes", 15))
|
|
109
|
+
enc.addWidget(self.idle_spin, 0, Qt.AlignLeft)
|
|
110
|
+
# Explanation for idle option (autolock)
|
|
111
|
+
self.idle_spin_label = QLabel(
|
|
112
|
+
"Bouquin will automatically lock the notepad after this length of time, after which you'll need to re-enter the key to unlock it. "
|
|
113
|
+
"Set to 0 (never) to never lock."
|
|
114
|
+
)
|
|
115
|
+
self.idle_spin_label.setWordWrap(True)
|
|
116
|
+
self.idle_spin_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
117
|
+
# make it look secondary
|
|
118
|
+
spal = self.idle_spin_label.palette()
|
|
119
|
+
spal.setColor(self.idle_spin_label.foregroundRole(), spal.color(QPalette.Mid))
|
|
120
|
+
self.idle_spin_label.setPalette(spal)
|
|
121
|
+
|
|
122
|
+
spin_row = QHBoxLayout()
|
|
123
|
+
spin_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the spinbox
|
|
124
|
+
spin_row.addWidget(self.idle_spin_label)
|
|
125
|
+
enc.addLayout(spin_row)
|
|
126
|
+
|
|
103
127
|
# Put the group into the form so it spans the full width nicely
|
|
104
128
|
form.addRow(enc_group)
|
|
105
129
|
|
|
@@ -126,7 +150,12 @@ class SettingsDialog(QDialog):
|
|
|
126
150
|
self.path_edit.setText(p)
|
|
127
151
|
|
|
128
152
|
def _save(self):
|
|
129
|
-
self.
|
|
153
|
+
key_to_save = self.key if self.save_key_btn.isChecked() else ""
|
|
154
|
+
self._cfg = DBConfig(
|
|
155
|
+
path=Path(self.path_edit.text()),
|
|
156
|
+
key=key_to_save,
|
|
157
|
+
idle_minutes=self.idle_spin.value(),
|
|
158
|
+
)
|
|
130
159
|
save_db_config(self._cfg)
|
|
131
160
|
self.accept()
|
|
132
161
|
|
|
@@ -155,14 +184,18 @@ class SettingsDialog(QDialog):
|
|
|
155
184
|
@Slot(bool)
|
|
156
185
|
def save_key_btn_clicked(self, checked: bool):
|
|
157
186
|
if checked:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
if not self.key:
|
|
188
|
+
p1 = KeyPrompt(
|
|
189
|
+
self, title="Enter your key", message="Enter the encryption key"
|
|
190
|
+
)
|
|
191
|
+
if p1.exec() != QDialog.Accepted:
|
|
192
|
+
self.save_key_btn.blockSignals(True)
|
|
193
|
+
self.save_key_btn.setChecked(False)
|
|
194
|
+
self.save_key_btn.blockSignals(False)
|
|
195
|
+
return
|
|
196
|
+
self.key = p1.key() or ""
|
|
197
|
+
else:
|
|
198
|
+
self.key = ""
|
|
166
199
|
|
|
167
200
|
@property
|
|
168
201
|
def config(self) -> DBConfig:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|