Initial commit
This commit is contained in:
commit
3e6a08231c
17 changed files with 2054 additions and 0 deletions
1
bouquin/__init__.py
Normal file
1
bouquin/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .main import main
|
||||
4
bouquin/__main__.py
Normal file
4
bouquin/__main__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
92
bouquin/db.py
Normal file
92
bouquin/db.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from sqlcipher3 import dbapi2 as sqlite
|
||||
|
||||
|
||||
@dataclass
|
||||
class DBConfig:
|
||||
path: Path
|
||||
key: str
|
||||
|
||||
|
||||
class DBManager:
|
||||
def __init__(self, cfg: DBConfig):
|
||||
self.cfg = cfg
|
||||
self.conn: sqlite.Connection | None = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
# Ensure parent dir exists
|
||||
self.cfg.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.conn = sqlite.connect(str(self.cfg.path))
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(f"PRAGMA key = '{self.cfg.key}';")
|
||||
cur.execute("PRAGMA cipher_compatibility = 4;")
|
||||
cur.execute("PRAGMA journal_mode = WAL;")
|
||||
self.conn.commit()
|
||||
try:
|
||||
self._integrity_ok()
|
||||
except Exception:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
return False
|
||||
self._ensure_schema()
|
||||
return True
|
||||
|
||||
def _integrity_ok(self) -> bool:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("PRAGMA cipher_integrity_check;")
|
||||
rows = cur.fetchall()
|
||||
|
||||
# OK
|
||||
if not rows:
|
||||
return
|
||||
|
||||
# Not OK
|
||||
details = "; ".join(str(r[0]) for r in rows if r and r[0] is not None)
|
||||
raise sqlite.IntegrityError(
|
||||
"SQLCipher integrity check failed"
|
||||
+ (f": {details}" if details else f" ({len(rows)} issue(s) reported)")
|
||||
)
|
||||
|
||||
def _ensure_schema(self) -> None:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
date TEXT PRIMARY KEY, -- ISO yyyy-MM-dd
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
cur.execute("PRAGMA user_version = 1;")
|
||||
self.conn.commit()
|
||||
|
||||
def get_entry(self, date_iso: str) -> str:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,))
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else ""
|
||||
|
||||
def upsert_entry(self, date_iso: str, content: str) -> None:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO entries(date, content) VALUES(?, ?)
|
||||
ON CONFLICT(date) DO UPDATE SET content = excluded.content;
|
||||
""",
|
||||
(date_iso, content),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def dates_with_content(self) -> list[str]:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT date FROM entries WHERE TRIM(content) <> '';")
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
def close(self) -> None:
|
||||
if self.conn is not None:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
112
bouquin/highlighter.py
Normal file
112
bouquin/highlighter.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from PySide6.QtGui import QFont, QTextCharFormat, QSyntaxHighlighter, QColor
|
||||
|
||||
|
||||
class MarkdownHighlighter(QSyntaxHighlighter):
|
||||
ST_NORMAL = 0
|
||||
ST_CODE = 1
|
||||
|
||||
FENCE = re.compile(r"^```")
|
||||
|
||||
def __init__(self, document):
|
||||
super().__init__(document)
|
||||
|
||||
base_size = document.defaultFont().pointSizeF() or 12.0
|
||||
|
||||
# Monospace for code
|
||||
self.mono = QFont("Monospace")
|
||||
self.mono.setStyleHint(QFont.TypeWriter)
|
||||
|
||||
# Light, high-contrast scheme for code
|
||||
self.col_bg = QColor("#eef2f6") # light code bg
|
||||
self.col_fg = QColor("#1f2328") # dark text
|
||||
|
||||
# Formats
|
||||
self.fmt_h = [QTextCharFormat() for _ in range(6)]
|
||||
for i, f in enumerate(self.fmt_h, start=1):
|
||||
f.setFontWeight(QFont.Weight.Bold)
|
||||
f.setFontPointSize(base_size + (7 - i))
|
||||
self.fmt_bold = QTextCharFormat()
|
||||
self.fmt_bold.setFontWeight(QFont.Weight.Bold)
|
||||
self.fmt_italic = QTextCharFormat()
|
||||
self.fmt_italic.setFontItalic(True)
|
||||
self.fmt_quote = QTextCharFormat()
|
||||
self.fmt_quote.setForeground(QColor("#6a737d"))
|
||||
self.fmt_link = QTextCharFormat()
|
||||
self.fmt_link.setFontUnderline(True)
|
||||
self.fmt_list = QTextCharFormat()
|
||||
self.fmt_list.setFontWeight(QFont.Weight.DemiBold)
|
||||
self.fmt_strike = QTextCharFormat()
|
||||
self.fmt_strike.setFontStrikeOut(True)
|
||||
|
||||
# Uniform code style
|
||||
self.fmt_code = QTextCharFormat()
|
||||
self.fmt_code.setFont(self.mono)
|
||||
self.fmt_code.setFontPointSize(max(6.0, base_size - 1))
|
||||
self.fmt_code.setBackground(self.col_bg)
|
||||
self.fmt_code.setForeground(self.col_fg)
|
||||
|
||||
# Simple patterns
|
||||
self.re_heading = re.compile(r"^(#{1,6}) +.*$")
|
||||
self.re_bold = re.compile(r"\*\*(.+?)\*\*|__(.+?)__")
|
||||
self.re_italic = re.compile(r"\*(?!\*)(.+?)\*|_(?!_)(.+?)_")
|
||||
self.re_strike = re.compile(r"~~(.+?)~~")
|
||||
self.re_inline_code = re.compile(r"`([^`]+)`")
|
||||
self.re_link = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
||||
self.re_list = re.compile(r"^ *(?:[-*+] +|[0-9]+[.)] +)")
|
||||
self.re_quote = re.compile(r"^> ?.*$")
|
||||
|
||||
def highlightBlock(self, text: str) -> None:
|
||||
prev = self.previousBlockState()
|
||||
in_code = prev == self.ST_CODE
|
||||
|
||||
if in_code:
|
||||
# Entire line is code
|
||||
self.setFormat(0, len(text), self.fmt_code)
|
||||
if self.FENCE.match(text):
|
||||
self.setCurrentBlockState(self.ST_NORMAL)
|
||||
else:
|
||||
self.setCurrentBlockState(self.ST_CODE)
|
||||
return
|
||||
|
||||
# Starting/ending a fenced block?
|
||||
if self.FENCE.match(text):
|
||||
self.setFormat(0, len(text), self.fmt_code)
|
||||
self.setCurrentBlockState(self.ST_CODE)
|
||||
return
|
||||
|
||||
# --- Normal markdown styling ---
|
||||
m = self.re_heading.match(text)
|
||||
if m:
|
||||
level = min(len(m.group(1)), 6)
|
||||
self.setFormat(0, len(text), self.fmt_h[level - 1])
|
||||
self.setCurrentBlockState(self.ST_NORMAL)
|
||||
return
|
||||
|
||||
m = self.re_list.match(text)
|
||||
if m:
|
||||
self.setFormat(m.start(), m.end() - m.start(), self.fmt_list)
|
||||
|
||||
if self.re_quote.match(text):
|
||||
self.setFormat(0, len(text), self.fmt_quote)
|
||||
|
||||
for m in self.re_inline_code.finditer(text):
|
||||
self.setFormat(m.start(), m.end() - m.start(), self.fmt_code)
|
||||
|
||||
for m in self.re_bold.finditer(text):
|
||||
self.setFormat(m.start(), m.end() - m.start(), self.fmt_bold)
|
||||
|
||||
for m in self.re_italic.finditer(text):
|
||||
self.setFormat(m.start(), m.end() - m.start(), self.fmt_italic)
|
||||
|
||||
for m in self.re_strike.finditer(text):
|
||||
self.setFormat(m.start(), m.end() - m.start(), self.fmt_strike)
|
||||
|
||||
for m in self.re_link.finditer(text):
|
||||
start = m.start(1) - 1
|
||||
length = len(m.group(1)) + 2
|
||||
self.setFormat(start, length, self.fmt_link)
|
||||
|
||||
self.setCurrentBlockState(self.ST_NORMAL)
|
||||
41
bouquin/key_prompt.py
Normal file
41
bouquin/key_prompt.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QDialogButtonBox,
|
||||
)
|
||||
|
||||
|
||||
class KeyPrompt(QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
title: str = "Unlock database",
|
||||
message: str = "Enter SQLCipher key",
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(title)
|
||||
v = QVBoxLayout(self)
|
||||
v.addWidget(QLabel(message))
|
||||
self.edit = QLineEdit()
|
||||
self.edit.setEchoMode(QLineEdit.Password)
|
||||
v.addWidget(self.edit)
|
||||
toggle = QPushButton("Show")
|
||||
toggle.setCheckable(True)
|
||||
toggle.toggled.connect(
|
||||
lambda c: self.edit.setEchoMode(
|
||||
QLineEdit.Normal if c else QLineEdit.Password
|
||||
)
|
||||
)
|
||||
v.addWidget(toggle)
|
||||
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
v.addWidget(bb)
|
||||
|
||||
def key(self) -> str:
|
||||
return self.edit.text()
|
||||
15
bouquin/main.py
Normal file
15
bouquin/main.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from .settings import APP_NAME, APP_ORG
|
||||
from .main_window import MainWindow
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName(APP_NAME)
|
||||
app.setOrganizationName(APP_ORG)
|
||||
win = MainWindow(); win.show()
|
||||
sys.exit(app.exec())
|
||||
245
bouquin/main_window.py
Normal file
245
bouquin/main_window.py
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import QDate, QTimer, Qt
|
||||
from PySide6.QtGui import QAction, QFont, QTextCharFormat
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QCalendarWidget,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QSizePolicy,
|
||||
)
|
||||
|
||||
from .db import DBManager
|
||||
from .settings import APP_NAME, load_db_config, save_db_config
|
||||
from .key_prompt import KeyPrompt
|
||||
from .highlighter import MarkdownHighlighter
|
||||
from .settings_dialog import SettingsDialog
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle(APP_NAME)
|
||||
self.setMinimumSize(1000, 650)
|
||||
|
||||
self.cfg = load_db_config()
|
||||
# Always prompt for the key (we never store it)
|
||||
if not self._prompt_for_key_until_valid():
|
||||
sys.exit(1)
|
||||
|
||||
# ---- UI: Left fixed panel (calendar) + right editor -----------------
|
||||
self.calendar = QCalendarWidget()
|
||||
self.calendar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.calendar.setGridVisible(True)
|
||||
self.calendar.selectionChanged.connect(self._on_date_changed)
|
||||
|
||||
left_panel = QWidget()
|
||||
left_layout = QVBoxLayout(left_panel)
|
||||
left_layout.setContentsMargins(8, 8, 8, 8)
|
||||
left_layout.addWidget(self.calendar, alignment=Qt.AlignTop)
|
||||
left_layout.addStretch(1)
|
||||
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
|
||||
|
||||
self.editor = QPlainTextEdit()
|
||||
tab_w = 4 * self.editor.fontMetrics().horizontalAdvance(" ")
|
||||
self.editor.setTabStopDistance(tab_w)
|
||||
self.highlighter = MarkdownHighlighter(self.editor.document())
|
||||
|
||||
split = QSplitter()
|
||||
split.addWidget(left_panel)
|
||||
split.addWidget(self.editor)
|
||||
split.setStretchFactor(1, 1) # editor grows
|
||||
|
||||
container = QWidget()
|
||||
lay = QVBoxLayout(container)
|
||||
lay.addWidget(split)
|
||||
self.setCentralWidget(container)
|
||||
|
||||
# Status bar for feedback
|
||||
self.statusBar().showMessage("Ready", 800)
|
||||
|
||||
# Menu bar (File)
|
||||
mb = self.menuBar()
|
||||
file_menu = mb.addMenu("&File")
|
||||
act_save = QAction("&Save", self)
|
||||
act_save.setShortcut("Ctrl+S")
|
||||
act_save.triggered.connect(lambda: self._save_current(explicit=True))
|
||||
file_menu.addAction(act_save)
|
||||
act_settings = QAction("&Settings", self)
|
||||
act_settings.triggered.connect(self._open_settings)
|
||||
file_menu.addAction(act_settings)
|
||||
file_menu.addSeparator()
|
||||
act_quit = QAction("&Quit", self)
|
||||
act_quit.setShortcut("Ctrl+Q")
|
||||
act_quit.triggered.connect(self.close)
|
||||
file_menu.addAction(act_quit)
|
||||
|
||||
# Navigate menu with next/previous day
|
||||
nav_menu = mb.addMenu("&Navigate")
|
||||
act_prev = QAction("Previous Day", self)
|
||||
act_prev.setShortcut("Ctrl+P")
|
||||
act_prev.setShortcutContext(Qt.ApplicationShortcut)
|
||||
act_prev.triggered.connect(lambda: self._adjust_day(-1))
|
||||
nav_menu.addAction(act_prev)
|
||||
self.addAction(act_prev)
|
||||
|
||||
act_next = QAction("Next Day", self)
|
||||
act_next.setShortcut("Ctrl+N")
|
||||
act_next.setShortcutContext(Qt.ApplicationShortcut)
|
||||
act_next.triggered.connect(lambda: self._adjust_day(1))
|
||||
nav_menu.addAction(act_next)
|
||||
self.addAction(act_next)
|
||||
|
||||
# Autosave
|
||||
self._dirty = False
|
||||
self._save_timer = QTimer(self)
|
||||
self._save_timer.setSingleShot(True)
|
||||
self._save_timer.timeout.connect(self._save_current)
|
||||
self.editor.textChanged.connect(self._on_text_changed)
|
||||
|
||||
# First load + mark dates with content
|
||||
self._load_selected_date()
|
||||
self._refresh_calendar_marks()
|
||||
|
||||
# --- DB lifecycle
|
||||
def _try_connect(self) -> bool:
|
||||
try:
|
||||
self.db = DBManager(self.cfg)
|
||||
ok = self.db.connect()
|
||||
except Exception as e:
|
||||
if str(e) == "file is not a database":
|
||||
error = "The key is probably incorrect."
|
||||
else:
|
||||
error = str(e)
|
||||
QMessageBox.critical(self, "Database Error", error)
|
||||
return False
|
||||
return ok
|
||||
|
||||
def _prompt_for_key_until_valid(self) -> bool:
|
||||
while True:
|
||||
dlg = KeyPrompt(self, message="Enter a key to unlock the notebook")
|
||||
if dlg.exec() != QDialog.Accepted:
|
||||
return False
|
||||
self.cfg.key = dlg.key()
|
||||
if self._try_connect():
|
||||
return True
|
||||
|
||||
# --- Calendar marks to indicate text exists for htat day -----------------
|
||||
def _refresh_calendar_marks(self):
|
||||
fmt_bold = QTextCharFormat()
|
||||
fmt_bold.setFontWeight(QFont.Weight.Bold)
|
||||
# Clear previous marks
|
||||
for d in getattr(self, "_marked_dates", set()):
|
||||
self.calendar.setDateTextFormat(d, QTextCharFormat())
|
||||
self._marked_dates = set()
|
||||
try:
|
||||
for date_iso in self.db.dates_with_content():
|
||||
qd = QDate.fromString(date_iso, "yyyy-MM-dd")
|
||||
if qd.isValid():
|
||||
self.calendar.setDateTextFormat(qd, fmt_bold)
|
||||
self._marked_dates.add(qd)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- UI handlers ---------------------------------------------------------
|
||||
def _current_date_iso(self) -> str:
|
||||
d = self.calendar.selectedDate()
|
||||
return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
|
||||
|
||||
def _load_selected_date(self):
|
||||
date_iso = self._current_date_iso()
|
||||
try:
|
||||
text = self.db.get_entry(date_iso)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Read Error", str(e))
|
||||
return
|
||||
self.editor.blockSignals(True)
|
||||
self.editor.setPlainText(text)
|
||||
self.editor.blockSignals(False)
|
||||
self._dirty = False
|
||||
# track which date the editor currently represents
|
||||
self._active_date_iso = date_iso
|
||||
|
||||
def _on_text_changed(self):
|
||||
self._dirty = True
|
||||
self._save_timer.start(1200) # autosave after idle
|
||||
|
||||
def _adjust_day(self, delta: int):
|
||||
"""Move selection by delta days (negative for previous)."""
|
||||
d = self.calendar.selectedDate().addDays(delta)
|
||||
self.calendar.setSelectedDate(d)
|
||||
|
||||
def _on_date_changed(self):
|
||||
"""
|
||||
When the calendar selection changes, save the previous day's note if dirty,
|
||||
so we don't lose that text, then load the newly selected day.
|
||||
"""
|
||||
# Stop pending autosave and persist current buffer if needed
|
||||
try:
|
||||
self._save_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
prev = getattr(self, "_active_date_iso", None)
|
||||
if prev and self._dirty:
|
||||
self._save_date(prev, explicit=False)
|
||||
# Now load the newly selected date
|
||||
self._load_selected_date()
|
||||
|
||||
def _save_date(self, date_iso: str, explicit: bool = False):
|
||||
"""
|
||||
Save editor contents into the given date. Shows status on success.
|
||||
explicit=True means user invoked Save: show feedback even if nothing changed.
|
||||
"""
|
||||
if not self._dirty and not explicit:
|
||||
return
|
||||
text = self.editor.toPlainText()
|
||||
try:
|
||||
self.db.upsert_entry(date_iso, text)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Save Error", str(e))
|
||||
return
|
||||
self._dirty = False
|
||||
self._refresh_calendar_marks()
|
||||
# Feedback in the status bar
|
||||
from datetime import datetime as _dt
|
||||
|
||||
self.statusBar().showMessage(
|
||||
f"Saved {date_iso} at {_dt.now().strftime('%H:%M:%S')}", 2000
|
||||
)
|
||||
|
||||
def _save_current(self, explicit: bool = False):
|
||||
# Delegate to _save_date for the currently selected date
|
||||
self._save_date(self._current_date_iso(), explicit)
|
||||
|
||||
def _open_settings(self):
|
||||
dlg = SettingsDialog(self.cfg, self)
|
||||
if dlg.exec() == QDialog.Accepted:
|
||||
new_cfg = dlg.config
|
||||
if new_cfg.path != self.cfg.path:
|
||||
# Save the new path to the notebook
|
||||
self.cfg.path = new_cfg.path
|
||||
save_db_config(self.cfg)
|
||||
self.db.close()
|
||||
# Prompt again for the key for the new path
|
||||
if not self._prompt_for_key_until_valid():
|
||||
QMessageBox.warning(
|
||||
self, "Reopen failed", "Could not unlock database at new path."
|
||||
)
|
||||
return
|
||||
self._load_selected_date()
|
||||
self._refresh_calendar_marks()
|
||||
|
||||
def closeEvent(self, event): # noqa: N802
|
||||
try:
|
||||
self._save_current()
|
||||
self.db.close()
|
||||
except Exception:
|
||||
pass
|
||||
super().closeEvent(event)
|
||||
29
bouquin/settings.py
Normal file
29
bouquin/settings.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import QSettings, QStandardPaths
|
||||
|
||||
from .db import DBConfig
|
||||
|
||||
APP_ORG = "Bouquin"
|
||||
APP_NAME = "Bouquin"
|
||||
|
||||
|
||||
def default_db_path() -> Path:
|
||||
base = Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation))
|
||||
return base / "notebook.db"
|
||||
|
||||
|
||||
def get_settings() -> QSettings:
|
||||
return QSettings(APP_ORG, APP_NAME)
|
||||
|
||||
|
||||
def load_db_config() -> DBConfig:
|
||||
s = get_settings()
|
||||
path = Path(s.value("db/path", str(default_db_path())))
|
||||
return DBConfig(path=path, key="")
|
||||
|
||||
|
||||
def save_db_config(cfg: DBConfig) -> None:
|
||||
s = get_settings()
|
||||
s.setValue("db/path", str(cfg.path))
|
||||
72
bouquin/settings_dialog.py
Normal file
72
bouquin/settings_dialog.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QFileDialog,
|
||||
QDialogButtonBox,
|
||||
QSizePolicy,
|
||||
)
|
||||
|
||||
from .db import DBConfig
|
||||
from .settings import save_db_config
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
def __init__(self, cfg: DBConfig, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Settings")
|
||||
self._cfg = DBConfig(path=cfg.path, key="")
|
||||
|
||||
form = QFormLayout()
|
||||
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
||||
self.setMinimumWidth(520)
|
||||
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)
|
||||
|
||||
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
bb.accepted.connect(self._save)
|
||||
bb.rejected.connect(self.reject)
|
||||
|
||||
v = QVBoxLayout(self)
|
||||
v.addLayout(form)
|
||||
v.addWidget(bb)
|
||||
|
||||
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):
|
||||
self._cfg = DBConfig(path=Path(self.path_edit.text()), key="")
|
||||
save_db_config(self._cfg)
|
||||
self.accept()
|
||||
|
||||
@property
|
||||
def config(self) -> DBConfig:
|
||||
return self._cfg
|
||||
Loading…
Add table
Add a link
Reference in a new issue