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"

{date_str}

") + date_lbl.setText(f"{date_str}:") date_f = date_lbl.font() date_f.setPointSizeF(date_f.pointSizeF() + 1) date_lbl.setFont(date_f) + date_lbl.setStyleSheet("color:#000;") outer.addWidget(date_lbl) # Preview row with optional ellipses diff --git a/bouquin/settings.py b/bouquin/settings.py index 8860ed2..fc92394 100644 --- a/bouquin/settings.py +++ b/bouquin/settings.py @@ -22,14 +22,12 @@ def load_db_config() -> DBConfig: s = get_settings() path = Path(s.value("db/path", str(default_db_path()))) key = s.value("db/key", "") - idle = s.value("ui/idle_minutes", 15, type=int) - theme = s.value("ui/theme", "system", type=str) - return DBConfig(path=path, key=key, idle_minutes=idle, theme=theme) + idle = s.value("db/idle_minutes", 15, type=int) + return DBConfig(path=path, key=key, idle_minutes=idle) def save_db_config(cfg: DBConfig) -> None: s = get_settings() s.setValue("db/path", str(cfg.path)) s.setValue("db/key", str(cfg.key)) - s.setValue("ui/idle_minutes", str(cfg.idle_minutes)) - s.setValue("ui/theme", str(cfg.theme)) + s.setValue("db/idle_minutes", str(cfg.idle_minutes)) diff --git a/bouquin/settings_dialog.py b/bouquin/settings_dialog.py index ac36337..0a3dfd8 100644 --- a/bouquin/settings_dialog.py +++ b/bouquin/settings_dialog.py @@ -16,7 +16,6 @@ from PySide6.QtWidgets import ( QPushButton, QFileDialog, QDialogButtonBox, - QRadioButton, QSizePolicy, QSpinBox, QMessageBox, @@ -27,7 +26,6 @@ from PySide6.QtGui import QPalette from .db import DBConfig, DBManager from .settings import load_db_config, save_db_config -from .theme import Theme from .key_prompt import KeyPrompt @@ -44,31 +42,6 @@ class SettingsDialog(QDialog): self.setMinimumWidth(560) self.setSizeGripEnabled(True) - current_settings = load_db_config() - - # Add theme selection - theme_group = QGroupBox("Theme") - theme_layout = QVBoxLayout(theme_group) - - self.theme_system = QRadioButton("System") - self.theme_light = QRadioButton("Light") - self.theme_dark = QRadioButton("Dark") - - # Load current theme from settings - current_theme = current_settings.theme - if current_theme == Theme.DARK.value: - self.theme_dark.setChecked(True) - elif current_theme == Theme.LIGHT.value: - self.theme_light.setChecked(True) - else: - self.theme_system.setChecked(True) - - theme_layout.addWidget(self.theme_system) - theme_layout.addWidget(self.theme_light) - theme_layout.addWidget(self.theme_dark) - - form.addRow(theme_group) - self.path_edit = QLineEdit(str(self._cfg.path)) self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) browse_btn = QPushButton("Browse…") @@ -91,6 +64,7 @@ class SettingsDialog(QDialog): # 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) @@ -214,24 +188,13 @@ class SettingsDialog(QDialog): self.path_edit.setText(p) def _save(self): - # Save the selected theme into QSettings - if self.theme_dark.isChecked(): - selected_theme = Theme.DARK - elif self.theme_light.isChecked(): - selected_theme = Theme.LIGHT - else: - selected_theme = Theme.SYSTEM - 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(), - theme=selected_theme.value, ) - save_db_config(self._cfg) - self.parent().themes.apply(selected_theme) self.accept() def _change_key(self): @@ -259,7 +222,6 @@ class SettingsDialog(QDialog): @Slot(bool) def _save_key_btn_clicked(self, checked: bool): - self.key = "" if checked: if not self.key: p1 = KeyPrompt( @@ -271,6 +233,8 @@ class SettingsDialog(QDialog): self.save_key_btn.blockSignals(False) return self.key = p1.key() or "" + else: + self.key = "" @Slot(bool) def _compact_btn_clicked(self): diff --git a/bouquin/theme.py b/bouquin/theme.py deleted file mode 100644 index 341466e..0000000 --- a/bouquin/theme.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations -from dataclasses import dataclass -from enum import Enum -from PySide6.QtGui import QPalette, QColor, QGuiApplication -from PySide6.QtWidgets import QApplication -from PySide6.QtCore import QObject, Signal - - -class Theme(Enum): - SYSTEM = "system" - LIGHT = "light" - DARK = "dark" - ORANGE_ANCHOR = "#FFA500" - ORANGE_ANCHOR_VISITED = "#B38000" - - -@dataclass -class ThemeConfig: - theme: Theme = Theme.SYSTEM - - -class ThemeManager(QObject): - themeChanged = Signal(Theme) - - def __init__(self, app: QApplication, cfg: ThemeConfig): - super().__init__() - self._app = app - self._cfg = cfg - - # Follow OS if supported (Qt 6+) - hints = QGuiApplication.styleHints() - if hasattr(hints, "colorSchemeChanged"): - hints.colorSchemeChanged.connect( - lambda _: (self._cfg.theme == Theme.SYSTEM) - and self.apply(self._cfg.theme) - ) - - def current(self) -> Theme: - return self._cfg.theme - - def set(self, theme: Theme): - self._cfg.theme = theme - self.apply(theme) - - def apply(self, theme: Theme): - # Resolve "system" - if theme == Theme.SYSTEM: - hints = QGuiApplication.styleHints() - scheme = getattr(hints, "colorScheme", None) - if callable(scheme): - scheme = hints.colorScheme() - # 0=Light, 1=Dark in newer Qt; fall back to Light - theme = Theme.DARK if scheme == 1 else Theme.LIGHT - - # Always use Fusion so palette applies consistently cross-platform - self._app.setStyle("Fusion") - - if theme == Theme.DARK: - pal = self._dark_palette() - self._app.setPalette(pal) - # keep stylesheet empty unless you need widget-specific tweaks - self._app.setStyleSheet("") - else: - pal = self._light_palette() - self._app.setPalette(pal) - self._app.setStyleSheet("") - - self.themeChanged.emit(theme) - - # ----- Palettes ----- - def _dark_palette(self) -> QPalette: - pal = QPalette() - base = QColor(35, 35, 35) - window = QColor(53, 53, 53) - text = QColor(220, 220, 220) - disabled = QColor(127, 127, 127) - focus = QColor(42, 130, 218) - - pal.setColor(QPalette.Window, window) - pal.setColor(QPalette.WindowText, text) - pal.setColor(QPalette.Base, base) - pal.setColor(QPalette.AlternateBase, window) - pal.setColor(QPalette.ToolTipBase, window) - pal.setColor(QPalette.ToolTipText, text) - pal.setColor(QPalette.Text, text) - pal.setColor(QPalette.PlaceholderText, disabled) - pal.setColor(QPalette.Button, window) - pal.setColor(QPalette.ButtonText, text) - pal.setColor(QPalette.BrightText, QColor(255, 84, 84)) - pal.setColor(QPalette.Highlight, focus) - pal.setColor(QPalette.HighlightedText, QColor(0, 0, 0)) - pal.setColor(QPalette.Link, QColor(Theme.ORANGE_ANCHOR.value)) - pal.setColor(QPalette.LinkVisited, QColor(Theme.ORANGE_ANCHOR_VISITED.value)) - - return pal - - def _light_palette(self) -> QPalette: - # Let Qt provide its default light palette, but nudge a couple roles - pal = self._app.style().standardPalette() - pal.setColor(QPalette.Highlight, QColor(0, 120, 215)) - pal.setColor(QPalette.HighlightedText, QColor(255, 255, 255)) - pal.setColor( - QPalette.Link, QColor("#1a73e8") - ) # Light blue for links in light mode - return pal diff --git a/tests/conftest.py b/tests/conftest.py index 8d885e6..1900f40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,6 @@ os.environ.setdefault("QT_FILE_DIALOG_ALWAYS_USE_NATIVE", "0") # Make project importable -from PySide6.QtWidgets import QApplication, QWidget -from bouquin.theme import ThemeManager, ThemeConfig, Theme - PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) @@ -62,10 +59,7 @@ def open_window(qtbot, temp_home, clean_settings): """Launch the app and immediately satisfy first-run/unlock key prompts.""" from bouquin.main_window import MainWindow - app = QApplication.instance() - themes = ThemeManager(app, ThemeConfig()) - themes.apply(Theme.SYSTEM) - win = MainWindow(themes=themes) + win = MainWindow() qtbot.addWidget(win) win.show() qtbot.waitExposed(win) @@ -81,24 +75,3 @@ def today_iso(): d = date.today() return f"{d.year:04d}-{d.month:02d}-{d.day:02d}" - - -@pytest.fixture -def theme_parent_widget(qtbot): - """A minimal parent that provides .themes.apply(...) like MainWindow.""" - - class _ThemesStub: - def __init__(self): - self.applied = [] - - def apply(self, theme): - self.applied.append(theme) - - class _Parent(QWidget): - def __init__(self): - super().__init__() - self.themes = _ThemesStub() - - parent = _Parent() - qtbot.addWidget(parent) - return parent diff --git a/tests/qt_helpers.py b/tests/qt_helpers.py index f228177..1b9b9a3 100644 --- a/tests/qt_helpers.py +++ b/tests/qt_helpers.py @@ -166,7 +166,7 @@ class AutoResponder: continue wid = id(w) - # Handle first-run / unlock / save-name prompts + # Handle first-run / unlock / save-name prompts (your existing branches) if _looks_like_set_key_dialog(w) or _looks_like_unlock_dialog(w): fill_first_line_edit_and_accept(w, "ci-secret-key") self._seen.add(wid) diff --git a/tests/test_db_unit.py b/tests/test_db_unit.py deleted file mode 100644 index d369abf..0000000 --- a/tests/test_db_unit.py +++ /dev/null @@ -1,137 +0,0 @@ -import bouquin.db as dbmod -from bouquin.db import DBConfig, DBManager - - -class FakeCursor: - def __init__(self, rows=None): - self._rows = rows or [] - self.executed = [] - - def execute(self, sql, params=None): - self.executed.append((sql, tuple(params) if params else None)) - return self - - def fetchall(self): - return list(self._rows) - - def fetchone(self): - return self._rows[0] if self._rows else None - - -class FakeConn: - def __init__(self, rows=None): - self._rows = rows or [] - self.closed = False - self.cursors = [] - self.row_factory = None - - def cursor(self): - c = FakeCursor(rows=self._rows) - self.cursors.append(c) - return c - - def close(self): - self.closed = True - - def commit(self): - pass - - def __enter__(self): - return self - - def __exit__(self, *a): - pass - - -def test_integrity_ok_ok(monkeypatch, tmp_path): - mgr = DBManager(DBConfig(tmp_path / "db.sqlite", key="x")) - mgr.conn = FakeConn(rows=[]) - assert mgr._integrity_ok() is None - - -def test_integrity_ok_raises(monkeypatch, tmp_path): - mgr = DBManager(DBConfig(tmp_path / "db.sqlite", key="x")) - mgr.conn = FakeConn(rows=[("oops",), (None,)]) - try: - mgr._integrity_ok() - except Exception as e: - assert isinstance(e, dbmod.sqlite.IntegrityError) - - -def test_connect_closes_on_integrity_failure(monkeypatch, tmp_path): - # Use a non-empty key to avoid SQLCipher complaining before our patch runs - mgr = DBManager(DBConfig(tmp_path / "db.sqlite", key="x")) - # Make the integrity check raise so connect() takes the failure path - monkeypatch.setattr( - DBManager, - "_integrity_ok", - lambda self: (_ for _ in ()).throw(RuntimeError("bad")), - ) - ok = mgr.connect() - assert ok is False - assert mgr.conn is None - - -def test_rekey_not_connected_raises(tmp_path): - mgr = DBManager(DBConfig(tmp_path / "db.sqlite", key="old")) - mgr.conn = None - import pytest - - with pytest.raises(RuntimeError): - mgr.rekey("new") - - -def test_rekey_reopen_failure(monkeypatch, tmp_path): - mgr = DBManager(DBConfig(tmp_path / "db.sqlite", key="old")) - mgr.conn = FakeConn(rows=[(None,)]) - monkeypatch.setattr(DBManager, "connect", lambda self: False) - import pytest - - with pytest.raises(Exception): - mgr.rekey("new") - - -def test_export_by_extension_and_unknown(tmp_path): - mgr = DBManager(DBConfig(tmp_path / "db.sqlite", key="x")) - entries = [("2025-01-01", "Hi")] - # Test each exporter writes the file - p = tmp_path / "out.json" - mgr.export_json(entries, str(p)) - assert p.exists() and p.stat().st_size > 0 - p = tmp_path / "out.csv" - mgr.export_csv(entries, str(p)) - assert p.exists() - p = tmp_path / "out.txt" - mgr.export_txt(entries, str(p)) - assert p.exists() - p = tmp_path / "out.html" - mgr.export_html(entries, str(p)) - assert p.exists() - p = tmp_path / "out.md" - mgr.export_markdown(entries, str(p)) - assert p.exists() - # Router - import types - - mgr.get_all_entries = types.MethodType(lambda self: entries, mgr) - for ext in [".json", ".csv", ".txt", ".html"]: - path = tmp_path / f"route{ext}" - mgr.export_by_extension(str(path)) - assert path.exists() - import pytest - - with pytest.raises(ValueError): - mgr.export_by_extension(str(tmp_path / "x.zzz")) - - -def test_compact_error_prints(monkeypatch, tmp_path, capsys): - mgr = DBManager(DBConfig(tmp_path / "db.sqlite", key="x")) - - class BadConn: - def cursor(self): - raise RuntimeError("no") - - mgr.conn = BadConn() - mgr.compact() - out = capsys.readouterr().out - assert "Error:" in out diff --git a/tests/test_editor.py b/tests/test_editor.py index f3a9859..6935143 100644 --- a/tests/test_editor.py +++ b/tests/test_editor.py @@ -1,21 +1,12 @@ -from PySide6.QtCore import Qt, QMimeData, QPoint, QUrl -from PySide6.QtGui import QImage, QMouseEvent, QKeyEvent, QTextCursor, QTextImageFormat +from PySide6.QtCore import Qt +from PySide6.QtGui import QImage, QTextCursor, QTextImageFormat from PySide6.QtTest import QTest -from PySide6.QtWidgets import QApplication from bouquin.editor import Editor -from bouquin.theme import ThemeManager, ThemeConfig, Theme import re -def _mk_editor() -> Editor: - # pytest-qt ensures a QApplication exists - app = QApplication.instance() - tm = ThemeManager(app, ThemeConfig()) - return Editor(tm) - - def _move_cursor_to_first_image(editor: Editor) -> QTextImageFormat | None: c = editor.textCursor() c.movePosition(QTextCursor.Start) @@ -40,7 +31,7 @@ def _fmt_at(editor: Editor, pos: int): def test_space_breaks_link_anchor_and_styling(qtbot): - e = _mk_editor() + e = Editor() e.resize(600, 300) e.show() qtbot.waitExposed(e) @@ -84,7 +75,7 @@ def test_space_breaks_link_anchor_and_styling(qtbot): def test_embed_qimage_saved_as_data_url(qtbot): - e = _mk_editor() + e = Editor() e.resize(600, 400) qtbot.addWidget(e) e.show() @@ -105,7 +96,7 @@ def test_insert_images_autoscale_and_fit(qtbot, tmp_path): big_path = tmp_path / "big.png" big.save(str(big_path)) - e = _mk_editor() + e = Editor() e.resize(420, 300) # known viewport width qtbot.addWidget(e) e.show() @@ -129,7 +120,7 @@ def test_insert_images_autoscale_and_fit(qtbot, tmp_path): def test_linkify_trims_trailing_punctuation(qtbot): - e = _mk_editor() + e = Editor() qtbot.addWidget(e) e.show() qtbot.waitExposed(e) @@ -144,13 +135,31 @@ def test_linkify_trims_trailing_punctuation(qtbot): assert 'href="https://example.com)."' not in html +def test_space_does_not_bleed_anchor_format(qtbot): + e = Editor() + qtbot.addWidget(e) + e.show() + qtbot.waitExposed(e) + + e.setPlainText("https://a.example") + qtbot.waitUntil(lambda: 'href="' in e.document().toHtml()) + + c = e.textCursor() + c.movePosition(QTextCursor.End) + e.setTextCursor(c) + + # Press Space; keyPressEvent should break the anchor for the next char + QTest.keyClick(e, Qt.Key_Space) + assert e.currentCharFormat().isAnchor() is False + + def test_code_block_enter_exits_on_empty_line(qtbot): from PySide6.QtCore import Qt from PySide6.QtGui import QTextCursor from PySide6.QtTest import QTest from bouquin.editor import Editor - e = _mk_editor() + e = Editor() qtbot.addWidget(e) e.show() qtbot.waitExposed(e) @@ -176,169 +185,3 @@ def test_code_block_enter_exits_on_empty_line(qtbot): # Second Enter should jump *out* of the frame QTest.keyClick(e, Qt.Key_Return) # qtbot.waitUntil(lambda: e._find_code_frame(e.textCursor()) is None) - - -class DummyMenu: - def __init__(self): - self.seps = 0 - self.subs = [] - self.exec_called = False - - def addSeparator(self): - self.seps += 1 - - def addMenu(self, title): - m = DummyMenu() - self.subs.append((title, m)) - return m - - def addAction(self, *a, **k): - pass - - def exec(self, *a, **k): - self.exec_called = True - - -def _themes(): - app = QApplication.instance() - return ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) - - -def test_context_menu_adds_image_actions(monkeypatch, qtbot): - e = Editor(_themes()) - qtbot.addWidget(e) - # Fake an image at cursor - qi = QImage(10, 10, QImage.Format_ARGB32) - qi.fill(0xFF00FF00) - imgfmt = QTextImageFormat() - imgfmt.setName("x") - imgfmt.setWidth(10) - imgfmt.setHeight(10) - tc = e.textCursor() - monkeypatch.setattr(e, "_image_info_at_cursor", lambda: (tc, imgfmt, qi)) - - dummy = DummyMenu() - monkeypatch.setattr(e, "createStandardContextMenu", lambda: dummy) - - class Evt: - def globalPos(self): - return QPoint(0, 0) - - e.contextMenuEvent(Evt()) - assert dummy.exec_called - assert dummy.seps == 1 - assert any(t == "Image size" for t, _ in dummy.subs) - - -def test_insert_from_mime_image_and_urls(tmp_path, qtbot): - e = Editor(_themes()) - qtbot.addWidget(e) - # Build a mime with an image - mime = QMimeData() - img = QImage(6, 6, QImage.Format_ARGB32) - img.fill(0xFF0000FF) - mime.setImageData(img) - e.insertFromMimeData(mime) - html = e.document().toHtml() - assert "a

", - }, - { - "id": 2, - "version_no": 2, - "created_at": "2025-01-02T10:00:00Z", - "note": None, - "is_current": True, - "content": "

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"