diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0763e..425b5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ * Fix styling issue with text that comes after a URL, so it doesn't appear as part of the URL. * Add ability to export to Markdown (and fix heading styles) * Represent in the History diff pane when an image was the thing that changed - * Support theme choice in settings (light/dark/system) # 0.1.9 diff --git a/bouquin/__init__.py b/bouquin/__init__.py index e69de29..c28a133 100644 --- a/bouquin/__init__.py +++ b/bouquin/__init__.py @@ -0,0 +1 @@ +from .main import main diff --git a/bouquin/db.py b/bouquin/db.py index e8c4903..68f956d 100644 --- a/bouquin/db.py +++ b/bouquin/db.py @@ -19,7 +19,6 @@ class DBConfig: path: Path key: str idle_minutes: int = 15 # 0 = never lock - theme: str = "system" class DBManager: @@ -161,6 +160,13 @@ class DBManager: ).fetchone() return row[0] if row else "" + def upsert_entry(self, date_iso: str, content: str) -> None: + """ + Insert or update an entry. + """ + # Make a new version and set it as current + self.save_new_version(date_iso, content, note=None, set_current=True) + def search_entries(self, text: str) -> list[str]: """ Search for entries by term. This only works against the latest diff --git a/bouquin/editor.py b/bouquin/editor.py index f68d3c1..296ca34 100644 --- a/bouquin/editor.py +++ b/bouquin/editor.py @@ -10,7 +10,6 @@ from PySide6.QtGui import ( QFontDatabase, QImage, QImageReader, - QPalette, QPixmap, QTextCharFormat, QTextCursor, @@ -29,11 +28,8 @@ from PySide6.QtCore import ( QBuffer, QByteArray, QIODevice, - QTimer, ) -from PySide6.QtWidgets import QTextEdit, QApplication - -from .theme import Theme, ThemeManager +from PySide6.QtWidgets import QTextEdit class Editor(QTextEdit): @@ -46,7 +42,7 @@ class Editor(QTextEdit): _IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp") _DATA_IMG_RX = re.compile(r'src=["\']data:image/[^;]+;base64,([^"\']+)["\']', re.I) - def __init__(self, theme_manager: ThemeManager, *args, **kwargs): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) tab_w = 4 * self.fontMetrics().horizontalAdvance(" ") self.setTabStopDistance(tab_w) @@ -59,13 +55,7 @@ class Editor(QTextEdit): self.setAcceptRichText(True) - # If older docs have a baked-in color, normalize once: - self._retint_anchors_to_palette() - - self._themes = theme_manager - # Refresh on theme change - self._themes.themeChanged.connect(self._on_theme_changed) - + # Turn raw URLs into anchors self._linkifying = False self.textChanged.connect(self._linkify_document) self.viewport().setMouseTracking(True) @@ -97,6 +87,15 @@ class Editor(QTextEdit): f = f.parentFrame() return None + def _is_code_block(self, block) -> bool: + if not block.isValid(): + return False + bf = block.blockFormat() + return bool( + bf.nonBreakableLines() + and bf.background().color().rgb() == self._CODE_BG.rgb() + ) + def _trim_url_end(self, url: str) -> str: # strip common trailing punctuation not part of the URL trimmed = url.rstrip(".,;:!?\"'") @@ -142,7 +141,7 @@ class Editor(QTextEdit): fmt.setAnchor(True) fmt.setAnchorHref(href) # always refresh to the latest full URL fmt.setFontUnderline(True) - fmt.setForeground(self.palette().brush(QPalette.Link)) + fmt.setForeground(Qt.blue) cur.mergeCharFormat(fmt) # merge so we don’t clobber other styling @@ -482,6 +481,11 @@ class Editor(QTextEdit): # otherwise default handling return super().keyPressEvent(e) + def _clear_insertion_char_format(self): + """Reset inline typing format (keeps lists, alignment, margins, etc.).""" + nf = QTextCharFormat() + self.setCurrentCharFormat(nf) + def _break_anchor_for_next_char(self): """ Ensure the *next* typed character is not part of a hyperlink. @@ -665,41 +669,3 @@ class Editor(QTextEdit): fmt = QTextListFormat() fmt.setStyle(QTextListFormat.Style.ListDecimal) c.createList(fmt) - - @Slot(Theme) - def _on_theme_changed(self, _theme: Theme): - # Defer one event-loop tick so widgets have the new palette - QTimer.singleShot(0, self._retint_anchors_to_palette) - - @Slot() - def _retint_anchors_to_palette(self, *_): - # Always read from the *application* palette to avoid stale widget palette - app = QApplication.instance() - link_brush = app.palette().brush(QPalette.Link) - doc = self.document() - cur = QTextCursor(doc) - cur.beginEditBlock() - block = doc.firstBlock() - while block.isValid(): - it = block.begin() - while not it.atEnd(): - frag = it.fragment() - if frag.isValid(): - fmt = frag.charFormat() - if fmt.isAnchor(): - new_fmt = QTextCharFormat(fmt) - new_fmt.setForeground(link_brush) # force palette link color - cur.setPosition(frag.position()) - cur.setPosition( - frag.position() + frag.length(), QTextCursor.KeepAnchor - ) - cur.setCharFormat(new_fmt) - it += 1 - block = block.next() - cur.endEditBlock() - self.viewport().update() - - def setHtml(self, html: str) -> None: - super().setHtml(html) - # Ensure anchors adopt the palette color on startup - self._retint_anchors_to_palette() diff --git a/bouquin/main.py b/bouquin/main.py index a481480..3e5f90b 100644 --- a/bouquin/main.py +++ b/bouquin/main.py @@ -3,22 +3,14 @@ from __future__ import annotations import sys from PySide6.QtWidgets import QApplication -from .settings import APP_NAME, APP_ORG, get_settings +from .settings import APP_NAME, APP_ORG from .main_window import MainWindow -from .theme import Theme, ThemeConfig, ThemeManager def main(): app = QApplication(sys.argv) app.setApplicationName(APP_NAME) app.setOrganizationName(APP_ORG) - - s = get_settings() - theme_str = s.value("ui/theme", "system") - cfg = ThemeConfig(theme=Theme(theme_str)) - themes = ThemeManager(app, cfg) - themes.apply(cfg.theme) - - win = MainWindow(themes=themes) + win = MainWindow() win.show() sys.exit(app.exec()) diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 7b29bbc..cc01f6d 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -23,12 +23,9 @@ from PySide6.QtGui import ( QDesktopServices, QFont, QGuiApplication, - QPalette, - QTextCharFormat, QTextListFormat, ) from PySide6.QtWidgets import ( - QApplication, QCalendarWidget, QDialog, QFileDialog, @@ -51,7 +48,6 @@ from .search import Search from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config from .settings_dialog import SettingsDialog from .toolbar import ToolBar -from .theme import Theme, ThemeManager class _LockOverlay(QWidget): @@ -62,6 +58,23 @@ class _LockOverlay(QWidget): self.setFocusPolicy(Qt.StrongFocus) self.setGeometry(parent.rect()) + self.setStyleSheet( + """ +#LockOverlay { background-color: #ccc; } +#LockOverlay QLabel { color: #fff; font-size: 18px; } +#LockOverlay QPushButton { + background-color: #f2f2f2; + color: #000; + padding: 6px 14px; + border: 1px solid #808080; + border-radius: 6px; + font-size: 14px; +} +#LockOverlay QPushButton:hover { background-color: #ffffff; } +#LockOverlay QPushButton:pressed { background-color: #e6e6e6; } +""" + ) + lay = QVBoxLayout(self) lay.addStretch(1) @@ -79,42 +92,8 @@ class _LockOverlay(QWidget): lay.addWidget(self._btn, 0, Qt.AlignCenter) lay.addStretch(1) - self._apply_overlay_style() - self.hide() # start hidden - def _apply_overlay_style(self): - pal = self.palette() - bg = ( - pal.window().color().darker(180) - if pal.color(QPalette.Window).value() < 128 - else pal.window().color().lighter(110) - ) - text = pal.windowText().color() - btn_bg = pal.button().color() - btn_fg = pal.buttonText().color() - border = pal.mid().color() - - hover_bg = btn_bg.lighter(106) # +6% - press_bg = btn_bg.darker(106) # -6% - - self.setStyleSheet( - f""" - #LockOverlay {{ background-color: {bg.name()}; }} - #LockOverlay QLabel {{ color: {text.name()}; font-size: 18px; }} - #LockOverlay QPushButton {{ - background-color: {btn_bg.name()}; - color: {btn_fg.name()}; - padding: 6px 14px; - border: 1px solid {border.name()}; - border-radius: 6px; - font-size: 14px; - }} - #LockOverlay QPushButton:hover {{ background-color: {hover_bg.name()}; }} - #LockOverlay QPushButton:pressed {{ background-color: {press_bg.name()}; }} - """ - ) - # keep overlay sized with its parent def eventFilter(self, obj, event): if obj is self.parent() and event.type() in (QEvent.Resize, QEvent.Show): @@ -127,13 +106,11 @@ class _LockOverlay(QWidget): class MainWindow(QMainWindow): - def __init__(self, themes: ThemeManager, *args, **kwargs): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle(APP_NAME) self.setMinimumSize(1000, 650) - self.themes = themes # Store the themes manager - self.cfg = load_db_config() if not os.path.exists(self.cfg.path): # Fresh database/first time use, so guide the user re: setting a key @@ -168,7 +145,7 @@ class MainWindow(QMainWindow): left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16) # This is the note-taking editor - self.editor = Editor(self.themes) + self.editor = Editor() # Toolbar for controlling styling self.toolBar = ToolBar() @@ -208,7 +185,6 @@ class MainWindow(QMainWindow): # full-window overlay that sits on top of the central widget self._lock_overlay = _LockOverlay(self.centralWidget(), self._on_unlock_clicked) - self._lock_overlay._apply_overlay_style() self.centralWidget().installEventFilter(self._lock_overlay) self._locked = False @@ -304,16 +280,6 @@ class MainWindow(QMainWindow): self.settings = QSettings(APP_ORG, APP_NAME) self._restore_window_position() - self._apply_link_css() # Apply link color on startup - # re-apply all runtime color tweaks when theme changes - self.themes.themeChanged.connect(lambda _t: self._retheme_overrides()) - self.themes.themeChanged.connect(self._apply_calendar_theme) - self._apply_calendar_text_colors() - self._apply_calendar_theme(self.themes.current()) - - # apply once on startup so links / calendar colors are set immediately - self._retheme_overrides() - def _try_connect(self) -> bool: """ Try to connect to the database. @@ -348,90 +314,6 @@ class MainWindow(QMainWindow): if self._try_connect(): return True - def _retheme_overrides(self): - if hasattr(self, "_lock_overlay"): - self._lock_overlay._apply_overlay_style() - self._apply_calendar_text_colors() - self._apply_link_css() # Reapply link styles based on the current theme - self._apply_search_highlights(getattr(self, "_search_highlighted_dates", set())) - self.calendar.update() - self.editor.viewport().update() - - def _apply_link_css(self): - if self.themes and self.themes.current() == Theme.DARK: - anchor = Theme.ORANGE_ANCHOR.value - visited = Theme.ORANGE_ANCHOR_VISITED.value - css = f""" - a {{ color: {anchor}; text-decoration: underline; }} - a:visited {{ color: {visited}; }} - """ - else: - css = "" # Default to no custom styling for links (system or light theme) - - try: - # Apply to the editor (QTextEdit or any other relevant widgets) - self.editor.document().setDefaultStyleSheet(css) - except Exception: - pass - - try: - # Apply to the search widget (if it's also a rich-text widget) - self.search.document().setDefaultStyleSheet(css) - except Exception: - pass - - def _apply_calendar_theme(self, theme: Theme): - """Use orange accents on the calendar in dark mode only.""" - app_pal = QApplication.instance().palette() - - if theme == Theme.DARK: - highlight = QColor(Theme.ORANGE_ANCHOR.value) - black = QColor(0, 0, 0) - - highlight_css = Theme.ORANGE_ANCHOR.value - - # Per-widget palette: selection color inside the date grid - pal = self.calendar.palette() - pal.setColor(QPalette.Highlight, highlight) - pal.setColor(QPalette.HighlightedText, black) - self.calendar.setPalette(pal) - - # Stylesheet: nav bar + selected-day background - self.calendar.setStyleSheet( - f""" - QWidget#qt_calendar_navigationbar {{ background-color: {highlight_css}; }} - QCalendarWidget QToolButton {{ color: black; }} - QCalendarWidget QToolButton:hover {{ background-color: rgba(255,165,0,0.20); }} - /* Selected day color in the table view */ - QCalendarWidget QTableView:enabled {{ - selection-background-color: {highlight_css}; - selection-color: black; - }} - /* Optional: keep weekday header readable */ - QCalendarWidget QTableView QHeaderView::section {{ - background: transparent; - color: palette(windowText); - }} - """ - ) - else: - # Back to app defaults in light/system - self.calendar.setPalette(app_pal) - self.calendar.setStyleSheet("") - - # Keep weekend text color in sync with the current palette - self._apply_calendar_text_colors() - self.calendar.update() - - def _apply_calendar_text_colors(self): - pal = self.palette() - txt = pal.windowText().color() - fmt = QTextCharFormat() - fmt.setForeground(txt) - # Use normal text color for weekends - self.calendar.setWeekdayTextFormat(Qt.Saturday, fmt) - self.calendar.setWeekdayTextFormat(Qt.Sunday, fmt) - def _on_search_dates_changed(self, date_strs: list[str]): dates = set() for ds in date_strs or []: @@ -441,16 +323,7 @@ class MainWindow(QMainWindow): self._apply_search_highlights(dates) def _apply_search_highlights(self, dates: set): - pal = self.palette() - base = pal.base().color() - hi = pal.highlight().color() - # Blend highlight with base so it looks soft in both modes - blend = QColor( - (2 * hi.red() + base.red()) // 3, - (2 * hi.green() + base.green()) // 3, - (2 * hi.blue() + base.blue()) // 3, - ) - yellow = QBrush(blend) + yellow = QBrush(QColor("#fff9c4")) old = getattr(self, "_search_highlighted_dates", set()) for d in old - dates: # clear removed @@ -491,10 +364,10 @@ class MainWindow(QMainWindow): bf = c.blockFormat() # Block signals so setChecked() doesn't re-trigger actions - QSignalBlocker(self.toolBar.actBold) - QSignalBlocker(self.toolBar.actItalic) - QSignalBlocker(self.toolBar.actUnderline) - QSignalBlocker(self.toolBar.actStrike) + blocker1 = QSignalBlocker(self.toolBar.actBold) + blocker2 = QSignalBlocker(self.toolBar.actItalic) + blocker3 = QSignalBlocker(self.toolBar.actUnderline) + blocker4 = QSignalBlocker(self.toolBar.actStrike) self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold) self.toolBar.actItalic.setChecked(fmt.fontItalic()) @@ -511,10 +384,10 @@ class MainWindow(QMainWindow): bH2 = _approx(cur_size, 18) bH3 = _approx(cur_size, 14) - QSignalBlocker(self.toolBar.actH1) - QSignalBlocker(self.toolBar.actH2) - QSignalBlocker(self.toolBar.actH3) - QSignalBlocker(self.toolBar.actNormal) + b1 = QSignalBlocker(self.toolBar.actH1) + b2 = QSignalBlocker(self.toolBar.actH2) + b3 = QSignalBlocker(self.toolBar.actH3) + bN = QSignalBlocker(self.toolBar.actNormal) self.toolBar.actH1.setChecked(bH1) self.toolBar.actH2.setChecked(bH2) @@ -665,7 +538,6 @@ class MainWindow(QMainWindow): self.cfg.path = new_cfg.path self.cfg.key = new_cfg.key self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes) - self.cfg.theme = getattr(new_cfg, "theme", self.cfg.theme) # Persist once save_db_config(self.cfg) diff --git a/bouquin/search.py b/bouquin/search.py index 2805e4c..27c7e17 100644 --- a/bouquin/search.py +++ b/bouquin/search.py @@ -17,6 +17,7 @@ from PySide6.QtWidgets import ( QWidget, ) +# type: rows are (date_iso, content) Row = Tuple[str, str] @@ -101,10 +102,11 @@ class Search(QWidget): # Date label (plain text) date_lbl = QLabel() date_lbl.setTextFormat(Qt.TextFormat.RichText) - date_lbl.setText(f"
b
", - }, - ] - - def get_version(self, version_id): - if version_id == 2: - return {"content": "b
"} - return {"content": "a
"} - - def revert_to_version(self, date, version_id=None, version_no=None): - if self.fail_revert: - raise RuntimeError("boom") - - -def test_on_select_no_item(qtbot): - dlg = HistoryDialog(FakeDB(), "2025-01-01") - qtbot.addWidget(dlg) - dlg.list.clear() - dlg._on_select() - - -def test_revert_failure_shows_critical(qtbot, monkeypatch): - from PySide6.QtWidgets import QMessageBox - - fake = FakeDB() - fake.fail_revert = True - dlg = HistoryDialog(fake, "2025-01-01") - qtbot.addWidget(dlg) - item = QListWidgetItem("v1") - item.setData(Qt.UserRole, 1) # different from current 2 - dlg.list.addItem(item) - dlg.list.setCurrentItem(item) - msgs = {} - - def fake_crit(parent, title, text): - msgs["t"] = (title, text) - - monkeypatch.setattr(QMessageBox, "critical", staticmethod(fake_crit)) - dlg._revert() - assert "Revert failed" in msgs["t"][0] diff --git a/tests/test_misc.py b/tests/test_misc.py deleted file mode 100644 index 20a3b1c..0000000 --- a/tests/test_misc.py +++ /dev/null @@ -1,113 +0,0 @@ -from PySide6.QtWidgets import QApplication, QMessageBox -from bouquin.main_window import MainWindow -from bouquin.theme import ThemeManager, ThemeConfig, Theme -from bouquin.db import DBConfig - - -def _themes_light(): - app = QApplication.instance() - return ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) - - -def _themes_dark(): - app = QApplication.instance() - return ThemeManager(app, ThemeConfig(theme=Theme.DARK)) - - -class FakeDBErr: - def __init__(self, cfg): - pass - - def connect(self): - raise Exception("file is not a database") - - -class FakeDBOk: - def __init__(self, cfg): - pass - - def connect(self): - return True - - def save_new_version(self, date, text, note): - raise RuntimeError("nope") - - def get_entry(self, date): - return "hi
" - - def get_entries_days(self): - return [] - - -def test_try_connect_sqlcipher_error(monkeypatch, qtbot, tmp_path): - # Config with a key so __init__ calls _try_connect immediately - cfg = DBConfig(tmp_path / "db.sqlite", key="x") - (tmp_path / "db.sqlite").write_text("", encoding="utf-8") - monkeypatch.setattr("bouquin.main_window.load_db_config", lambda: cfg) - monkeypatch.setattr("bouquin.main_window.DBManager", FakeDBErr) - msgs = {} - monkeypatch.setattr( - QMessageBox, "critical", staticmethod(lambda p, t, m: msgs.setdefault("m", m)) - ) - w = MainWindow(_themes_light()) # auto-calls _try_connect - qtbot.addWidget(w) - assert "incorrect" in msgs.get("m", "").lower() - - -def test_apply_link_css_dark(qtbot, monkeypatch, tmp_path): - cfg = DBConfig(tmp_path / "db.sqlite", key="x") - (tmp_path / "db.sqlite").write_text("", encoding="utf-8") - monkeypatch.setattr("bouquin.main_window.load_db_config", lambda: cfg) - monkeypatch.setattr("bouquin.main_window.DBManager", FakeDBOk) - w = MainWindow(_themes_dark()) - qtbot.addWidget(w) - w._apply_link_css() - css = w.editor.document().defaultStyleSheet() - assert "a {" in css - - -def test_restore_window_position_first_run(qtbot, monkeypatch, tmp_path): - cfg = DBConfig(tmp_path / "db.sqlite", key="x") - (tmp_path / "db.sqlite").write_text("", encoding="utf-8") - monkeypatch.setattr("bouquin.main_window.load_db_config", lambda: cfg) - monkeypatch.setattr("bouquin.main_window.DBManager", FakeDBOk) - w = MainWindow(_themes_light()) - qtbot.addWidget(w) - called = {} - - class FakeSettings: - def value(self, key, default=None, type=None): - if key == "main/geometry": - return None - if key == "main/windowState": - return None - if key == "main/maximized": - return False - return default - - w.settings = FakeSettings() - monkeypatch.setattr( - w, "_move_to_cursor_screen_center", lambda: called.setdefault("x", True) - ) - w._restore_window_position() - assert called.get("x") is True - - -def test_on_insert_image_calls_editor(qtbot, monkeypatch, tmp_path): - cfg = DBConfig(tmp_path / "db.sqlite", key="x") - (tmp_path / "db.sqlite").write_text("", encoding="utf-8") - monkeypatch.setattr("bouquin.main_window.load_db_config", lambda: cfg) - monkeypatch.setattr("bouquin.main_window.DBManager", FakeDBOk) - w = MainWindow(_themes_light()) - qtbot.addWidget(w) - captured = {} - monkeypatch.setattr( - w.editor, "insert_images", lambda paths: captured.setdefault("p", paths) - ) - # Simulate file dialog returning paths - monkeypatch.setattr( - "bouquin.main_window.QFileDialog.getOpenFileNames", - staticmethod(lambda *a, **k: (["/tmp/a.png", "/tmp/b.jpg"], "Images")), - ) - w._on_insert_image() - assert captured.get("p") == ["/tmp/a.png", "/tmp/b.jpg"] diff --git a/tests/test_search_unit.py b/tests/test_search_unit.py deleted file mode 100644 index 13c1ef9..0000000 --- a/tests/test_search_unit.py +++ /dev/null @@ -1,57 +0,0 @@ -from PySide6.QtCore import Qt -from PySide6.QtWidgets import QListWidgetItem - -# The widget class is named `Search` in bouquin.search -from bouquin.search import Search as SearchWidget - - -class FakeDB: - def __init__(self, rows): - self.rows = rows - - def search_entries(self, q): - return list(self.rows) - - -def test_search_empty_clears_and_hides(qtbot): - w = SearchWidget(db=FakeDB([])) - qtbot.addWidget(w) - w.show() - qtbot.waitExposed(w) - dates = [] - w.resultDatesChanged.connect(lambda ds: dates.extend(ds)) - w._search(" ") - assert w.results.isHidden() - assert dates == [] - - -def test_populate_empty_hides(qtbot): - w = SearchWidget(db=FakeDB([])) - qtbot.addWidget(w) - w._populate_results("x", []) - assert w.results.isHidden() - - -def test_open_selected_emits_when_present(qtbot): - w = SearchWidget(db=FakeDB([])) - qtbot.addWidget(w) - got = {} - w.openDateRequested.connect(lambda d: got.setdefault("d", d)) - it = QListWidgetItem("x") - it.setData(Qt.ItemDataRole.UserRole, "") - w._open_selected(it) - assert "d" not in got - it.setData(Qt.ItemDataRole.UserRole, "2025-01-02") - w._open_selected(it) - assert got["d"] == "2025-01-02" - - -def test_make_html_snippet_edge_cases(qtbot): - w = SearchWidget(db=FakeDB([])) - qtbot.addWidget(w) - # Empty HTML -> empty fragment, no ellipses - frag, l, r = w._make_html_snippet("", "hello") - assert frag == "" and not l and not r - # Small doc around token -> should not show ellipses - frag, l, r = w._make_html_snippet("Hello world
", "world") - assert "world" in frag or "world" in frag diff --git a/tests/test_settings_dialog.py b/tests/test_settings_dialog.py index 906ec2c..f300c6f 100644 --- a/tests/test_settings_dialog.py +++ b/tests/test_settings_dialog.py @@ -1,24 +1,9 @@ from pathlib import Path -from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox, QWidget +from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox from bouquin.db import DBConfig from bouquin.settings_dialog import SettingsDialog -from bouquin.theme import Theme - - -class _ThemeSpy: - def __init__(self): - self.calls = [] - - def apply(self, t): - self.calls.append(t) - - -class _Parent(QWidget): - def __init__(self): - super().__init__() - self.themes = _ThemeSpy() class FakeDB: @@ -73,22 +58,7 @@ def test_save_persists_all_fields(monkeypatch, qtbot, tmp_path): p = AcceptingPrompt().set_key("sekrit") monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: p) - # Provide a lightweight parent that mimics MainWindow’s `themes` API - class _ThemeSpy: - def __init__(self): - self.calls = [] - - def apply(self, theme): - self.calls.append(theme) - - class _Parent(QWidget): - def __init__(self): - super().__init__() - self.themes = _ThemeSpy() - - parent = _Parent() - qtbot.addWidget(parent) - dlg = SettingsDialog(cfg, db, parent=parent) + dlg = SettingsDialog(cfg, db) qtbot.addWidget(dlg) dlg.show() qtbot.waitExposed(dlg) @@ -107,7 +77,6 @@ def test_save_persists_all_fields(monkeypatch, qtbot, tmp_path): assert out.path == new_path assert out.idle_minutes == 0 assert out.key == "sekrit" - assert parent.themes.calls and parent.themes.calls[-1] == Theme.SYSTEM def test_save_key_checkbox_requires_key_and_reverts_if_cancelled(monkeypatch, qtbot): @@ -281,16 +250,3 @@ def test_save_key_checkbox_preexisting_key_does_not_crash(monkeypatch, qtbot): dlg.save_key_btn.setChecked(True) # We should reach here with the original key preserved. assert dlg.key == "already" - - -def test_save_unchecked_clears_key_and_applies_theme(qtbot, tmp_path): - parent = _Parent() - qtbot.addWidget(parent) - cfg = DBConfig(tmp_path / "db.sqlite", key="sekrit", idle_minutes=5) - dlg = SettingsDialog(cfg, FakeDB(), parent=parent) - qtbot.addWidget(dlg) - dlg.save_key_btn.setChecked(False) - # Trigger save - dlg._save() - assert dlg.config.key == "" # cleared - assert parent.themes.calls # applied some theme diff --git a/tests/test_settings_module.py b/tests/test_settings_module.py deleted file mode 100644 index 24a9aac..0000000 --- a/tests/test_settings_module.py +++ /dev/null @@ -1,28 +0,0 @@ -from bouquin.db import DBConfig -import bouquin.settings as settings - - -class FakeSettings: - def __init__(self): - self.store = {} - - def value(self, key, default=None, type=None): - return self.store.get(key, default) - - def setValue(self, key, value): - self.store[key] = value - - -def test_save_and_load_db_config_roundtrip(monkeypatch, tmp_path): - fake = FakeSettings() - monkeypatch.setattr(settings, "get_settings", lambda: fake) - - cfg = DBConfig(path=tmp_path / "db.sqlite", key="k", idle_minutes=7, theme="dark") - settings.save_db_config(cfg) - - # Now read back into a new DBConfig - cfg2 = settings.load_db_config() - assert cfg2.path == cfg.path - assert cfg2.key == "k" - assert cfg2.idle_minutes == "7" - assert cfg2.theme == "dark" diff --git a/tests/test_theme_integration.py b/tests/test_theme_integration.py deleted file mode 100644 index f1949c3..0000000 --- a/tests/test_theme_integration.py +++ /dev/null @@ -1,19 +0,0 @@ -from bouquin.theme import Theme - - -def test_apply_link_css_dark_theme(open_window, qtbot): - win = open_window - # Switch to dark and apply link CSS - win.themes.set(Theme.DARK) - win._apply_link_css() - css = win.editor.document().defaultStyleSheet() - assert "#FFA500" in css and "a:visited" in css - - -def test_apply_link_css_light_theme(open_window, qtbot): - win = open_window - # Switch to light and apply link CSS - win.themes.set(Theme.LIGHT) - win._apply_link_css() - css = win.editor.document().defaultStyleSheet() - assert css == "" or "a {" not in css diff --git a/tests/test_theme_manager.py b/tests/test_theme_manager.py deleted file mode 100644 index 39121ea..0000000 --- a/tests/test_theme_manager.py +++ /dev/null @@ -1,19 +0,0 @@ -from PySide6.QtWidgets import QApplication -from PySide6.QtGui import QPalette, QColor - -from bouquin.theme import ThemeManager, ThemeConfig, Theme - - -def test_theme_manager_applies_palettes(qtbot): - app = QApplication.instance() - tm = ThemeManager(app, ThemeConfig()) - - # Light palette should set Link to the light blue - tm.apply(Theme.LIGHT) - pal = app.palette() - assert pal.color(QPalette.Link) == QColor("#1a73e8") - - # Dark palette should set Link to lavender-ish - tm.apply(Theme.DARK) - pal = app.palette() - assert pal.color(QPalette.Link) == QColor("#FFA500") diff --git a/tests/test_toolbar_private.py b/tests/test_toolbar_private.py deleted file mode 100644 index 834f4c2..0000000 --- a/tests/test_toolbar_private.py +++ /dev/null @@ -1,23 +0,0 @@ -from bouquin.toolbar import ToolBar - - -def test_style_letter_button_handles_missing_widget(qtbot): - tb = ToolBar() - qtbot.addWidget(tb) - # Create a dummy action detached from toolbar to force widgetForAction->None - from PySide6.QtGui import QAction - - act = QAction("X", tb) - # No crash and early return - tb._style_letter_button(act, "X") - - -def test_style_letter_button_sets_tooltip_and_accessible(qtbot): - tb = ToolBar() - qtbot.addWidget(tb) - # Use an existing action so widgetForAction returns a button - act = tb.actBold - tb._style_letter_button(act, "B", bold=True, tooltip="Bold") - btn = tb.widgetForAction(act) - assert btn.toolTip() == "Bold" - assert btn.accessibleName() == "Bold"