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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bouquin
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
5
5
  Home-page: https://git.mig5.net/mig5/bouquin
6
6
  License: GPL-3.0-or-later
@@ -17,6 +17,7 @@ Entry = Tuple[str, str]
17
17
  class DBConfig:
18
18
  path: Path
19
19
  key: str
20
+ idle_minutes: int = 15 # 0 = never lock
20
21
 
21
22
 
22
23
  class DBManager:
@@ -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
- tb = ToolBar()
81
- self.addToolBar(tb)
137
+ self.toolBar = ToolBar()
138
+ self.addToolBar(self.toolBar)
82
139
  # Wire toolbar intents to editor methods
83
- tb.boldRequested.connect(self.editor.apply_weight)
84
- tb.italicRequested.connect(self.editor.apply_italic)
85
- tb.underlineRequested.connect(self.editor.apply_underline)
86
- tb.strikeRequested.connect(self.editor.apply_strikethrough)
87
- tb.codeRequested.connect(self.editor.apply_code)
88
- tb.headingRequested.connect(self.editor.apply_heading)
89
- tb.bulletsRequested.connect(self.editor.toggle_bullets)
90
- tb.numbersRequested.connect(self.editor.toggle_numbers)
91
- tb.alignRequested.connect(self.editor.setAlignment)
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() == QDialog.Accepted:
309
- new_cfg = dlg.config
310
- if new_cfg.path != self.cfg.path:
311
- # Save the new path to the notebook
312
- self.cfg.path = new_cfg.path
313
- save_db_config(self.cfg)
314
- self.db.close()
315
- # Prompt again for the key for the new path
316
- if not self._prompt_for_key_until_valid():
317
- QMessageBox.warning(
318
- self, "Reopen failed", "Could not unlock database at new path."
319
- )
320
- return
321
- self._load_selected_date()
322
- self._refresh_calendar_marks()
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(self, "Open Documentation",
406
- f"Couldn't open:\n{url.toDisplayString()}")
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
- return DBConfig(path=path, key=key)
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
- if current_settings.key:
68
- self.save_key_btn.setChecked(True)
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._cfg = DBConfig(path=Path(self.path_edit.text()), key=self.key)
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
- p1 = KeyPrompt(
159
- self, title="Enter your key", message="Enter the encryption key"
160
- )
161
- if p1.exec() != QDialog.Accepted:
162
- return
163
- self.key = p1.key()
164
- self._cfg = DBConfig(path=Path(self.path_edit.text()), key=self.key)
165
- save_db_config(self._cfg)
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:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bouquin"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
5
5
  authors = ["Miguel Jacq <mig@mig5.net>"]
6
6
  readme = "README.md"
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