251 lines
8.8 KiB
Python
251 lines
8.8 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtWidgets import (
|
|
QCheckBox,
|
|
QDialog,
|
|
QFormLayout,
|
|
QFrame,
|
|
QGroupBox,
|
|
QLabel,
|
|
QHBoxLayout,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
QLineEdit,
|
|
QPushButton,
|
|
QFileDialog,
|
|
QDialogButtonBox,
|
|
QSizePolicy,
|
|
QSpinBox,
|
|
QMessageBox,
|
|
)
|
|
from PySide6.QtCore import Qt, Slot
|
|
from PySide6.QtGui import QPalette
|
|
|
|
|
|
from .db import DBConfig, DBManager
|
|
from .settings import load_db_config, save_db_config
|
|
from .key_prompt import KeyPrompt
|
|
|
|
|
|
class SettingsDialog(QDialog):
|
|
def __init__(self, cfg: DBConfig, db: DBManager, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Settings")
|
|
self._cfg = DBConfig(path=cfg.path, key="")
|
|
self._db = db
|
|
self.key = ""
|
|
|
|
form = QFormLayout()
|
|
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
|
self.setMinimumWidth(560)
|
|
self.setSizeGripEnabled(True)
|
|
|
|
self.path_edit = QLineEdit(str(self._cfg.path))
|
|
self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
browse_btn = QPushButton("Browse…")
|
|
browse_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
browse_btn.clicked.connect(self._browse)
|
|
path_row = QWidget()
|
|
h = QHBoxLayout(path_row)
|
|
h.setContentsMargins(0, 0, 0, 0)
|
|
h.addWidget(self.path_edit, 1)
|
|
h.addWidget(browse_btn, 0)
|
|
h.setStretch(0, 1)
|
|
h.setStretch(1, 0)
|
|
form.addRow("Database path", path_row)
|
|
|
|
# Encryption settings
|
|
enc_group = QGroupBox("Encryption")
|
|
enc = QVBoxLayout(enc_group)
|
|
enc.setContentsMargins(12, 8, 12, 12)
|
|
enc.setSpacing(6)
|
|
|
|
# Checkbox to remember key
|
|
self.save_key_btn = QCheckBox("Remember key")
|
|
current_settings = load_db_config()
|
|
self.key = current_settings.key or ""
|
|
self.save_key_btn.setChecked(bool(self.key))
|
|
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
|
self.save_key_btn.toggled.connect(self._save_key_btn_clicked)
|
|
enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft)
|
|
|
|
# Explanation for remembering key
|
|
self.save_key_label = QLabel(
|
|
"If you don't want to be prompted for your encryption key, check this to remember it. "
|
|
"WARNING: the key is saved to disk and could be recoverable if your disk is compromised."
|
|
)
|
|
self.save_key_label.setWordWrap(True)
|
|
self.save_key_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
# make it look secondary
|
|
pal = self.save_key_label.palette()
|
|
pal.setColor(self.save_key_label.foregroundRole(), pal.color(QPalette.Mid))
|
|
self.save_key_label.setPalette(pal)
|
|
|
|
exp_row = QHBoxLayout()
|
|
exp_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the checkbox
|
|
exp_row.addWidget(self.save_key_label)
|
|
enc.addLayout(exp_row)
|
|
|
|
line = QFrame()
|
|
line.setFrameShape(QFrame.HLine)
|
|
line.setFrameShadow(QFrame.Sunken)
|
|
enc.addWidget(line)
|
|
|
|
# Change key button
|
|
self.rekey_btn = QPushButton("Change encryption key")
|
|
self.rekey_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
self.rekey_btn.clicked.connect(self._change_key)
|
|
|
|
enc.addWidget(self.rekey_btn, 0, Qt.AlignLeft)
|
|
|
|
form.addRow(enc_group)
|
|
|
|
# Privacy settings
|
|
priv_group = QGroupBox("Lock screen when idle")
|
|
priv = QVBoxLayout(priv_group)
|
|
priv.setContentsMargins(12, 8, 12, 12)
|
|
priv.setSpacing(6)
|
|
|
|
self.idle_spin = QSpinBox()
|
|
self.idle_spin.setRange(0, 240)
|
|
self.idle_spin.setSingleStep(1)
|
|
self.idle_spin.setAccelerated(True)
|
|
self.idle_spin.setSuffix(" min")
|
|
self.idle_spin.setSpecialValueText("Never")
|
|
self.idle_spin.setValue(getattr(cfg, "idle_minutes", 15))
|
|
priv.addWidget(self.idle_spin, 0, Qt.AlignLeft)
|
|
# Explanation for idle option (autolock)
|
|
self.idle_spin_label = QLabel(
|
|
"Bouquin will automatically lock the notepad after this length of time, after which you'll need to re-enter the key to unlock it. "
|
|
"Set to 0 (never) to never lock."
|
|
)
|
|
self.idle_spin_label.setWordWrap(True)
|
|
self.idle_spin_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
# make it look secondary
|
|
spal = self.idle_spin_label.palette()
|
|
spal.setColor(self.idle_spin_label.foregroundRole(), spal.color(QPalette.Mid))
|
|
self.idle_spin_label.setPalette(spal)
|
|
|
|
spin_row = QHBoxLayout()
|
|
spin_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the spinbox
|
|
spin_row.addWidget(self.idle_spin_label)
|
|
priv.addLayout(spin_row)
|
|
|
|
form.addRow(priv_group)
|
|
|
|
# Maintenance settings
|
|
maint_group = QGroupBox("Database maintenance")
|
|
maint = QVBoxLayout(maint_group)
|
|
maint.setContentsMargins(12, 8, 12, 12)
|
|
maint.setSpacing(6)
|
|
|
|
self.compact_btn = QPushButton("Compact database")
|
|
self.compact_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
self.compact_btn.clicked.connect(self._compact_btn_clicked)
|
|
|
|
maint.addWidget(self.compact_btn, 0, Qt.AlignLeft)
|
|
|
|
# Explanation for compating button
|
|
self.compact_label = QLabel(
|
|
"Compacting runs VACUUM on the database. This can help reduce its size."
|
|
)
|
|
self.compact_label.setWordWrap(True)
|
|
self.compact_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
# make it look secondary
|
|
cpal = self.compact_label.palette()
|
|
cpal.setColor(self.compact_label.foregroundRole(), cpal.color(QPalette.Mid))
|
|
self.compact_label.setPalette(cpal)
|
|
|
|
maint_row = QHBoxLayout()
|
|
maint_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the button
|
|
maint_row.addWidget(self.compact_label)
|
|
maint.addLayout(maint_row)
|
|
|
|
form.addRow(maint_group)
|
|
|
|
# Buttons
|
|
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
|
bb.accepted.connect(self._save)
|
|
bb.rejected.connect(self.reject)
|
|
|
|
# Root layout (adjust margins/spacing a bit)
|
|
v = QVBoxLayout(self)
|
|
v.setContentsMargins(12, 12, 12, 12)
|
|
v.setSpacing(10)
|
|
v.addLayout(form)
|
|
v.addWidget(bb, 0, Qt.AlignRight)
|
|
|
|
def _browse(self):
|
|
p, _ = QFileDialog.getSaveFileName(
|
|
self,
|
|
"Choose database file",
|
|
self.path_edit.text(),
|
|
"DB Files (*.db);;All Files (*)",
|
|
)
|
|
if p:
|
|
self.path_edit.setText(p)
|
|
|
|
def _save(self):
|
|
key_to_save = self.key if self.save_key_btn.isChecked() else ""
|
|
self._cfg = DBConfig(
|
|
path=Path(self.path_edit.text()),
|
|
key=key_to_save,
|
|
idle_minutes=self.idle_spin.value(),
|
|
)
|
|
save_db_config(self._cfg)
|
|
self.accept()
|
|
|
|
def _change_key(self):
|
|
p1 = KeyPrompt(self, title="Change key", message="Enter a new encryption key")
|
|
if p1.exec() != QDialog.Accepted:
|
|
return
|
|
new_key = p1.key()
|
|
p2 = KeyPrompt(self, title="Change key", message="Re-enter the new key")
|
|
if p2.exec() != QDialog.Accepted:
|
|
return
|
|
if new_key != p2.key():
|
|
QMessageBox.warning(self, "Key mismatch", "The two entries did not match.")
|
|
return
|
|
if not new_key:
|
|
QMessageBox.warning(self, "Empty key", "Key cannot be empty.")
|
|
return
|
|
try:
|
|
self.key = new_key
|
|
self._db.rekey(new_key)
|
|
QMessageBox.information(
|
|
self, "Key changed", "The notebook was re-encrypted with the new key!"
|
|
)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Could not change key:\n{e}")
|
|
|
|
@Slot(bool)
|
|
def _save_key_btn_clicked(self, checked: bool):
|
|
if checked:
|
|
if not self.key:
|
|
p1 = KeyPrompt(
|
|
self, title="Enter your key", message="Enter the encryption key"
|
|
)
|
|
if p1.exec() != QDialog.Accepted:
|
|
self.save_key_btn.blockSignals(True)
|
|
self.save_key_btn.setChecked(False)
|
|
self.save_key_btn.blockSignals(False)
|
|
return
|
|
self.key = p1.key() or ""
|
|
else:
|
|
self.key = ""
|
|
|
|
@Slot(bool)
|
|
def _compact_btn_clicked(self):
|
|
try:
|
|
self._db.compact()
|
|
QMessageBox.information(
|
|
self, "Compact complete", "Database compacted successfully!"
|
|
)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Could not compact database:\n{e}")
|
|
|
|
@property
|
|
def config(self) -> DBConfig:
|
|
return self._cfg
|