diff --git a/CHANGELOG.md b/CHANGELOG.md index 367c243..63af14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,3 @@ -# 0.2.1.4 - - * Increase font size of normal text - * Fix auto-save of a tab if we are moving to another tab and it has not yet saved - * DRY up some code - # 0.2.1.3 * Ensure checkbox only can get checked on/off if it is clicked right on its block position, not any click on the whole line diff --git a/bouquin/history_dialog.py b/bouquin/history_dialog.py index 1127852..212cb37 100644 --- a/bouquin/history_dialog.py +++ b/bouquin/history_dialog.py @@ -109,6 +109,14 @@ class HistoryDialog(QDialog): self._load_versions() # --- Data/UX helpers --- + def _fmt_local(self, iso_utc: str) -> str: + """ + Convert UTC in the database to user's local tz + """ + dt = datetime.fromisoformat(iso_utc.replace("Z", "+00:00")) + local = dt.astimezone() + return local.strftime("%Y-%m-%d %H:%M:%S %Z") + def _load_versions(self): # [{id,version_no,created_at,note,is_current}] self._versions = self._db.list_versions(self._date) @@ -118,11 +126,7 @@ class HistoryDialog(QDialog): ) self.list.clear() for v in self._versions: - created_at = datetime.fromisoformat( - v["created_at"].replace("Z", "+00:00") - ).astimezone() - created_at_local = created_at.strftime("%Y-%m-%d %H:%M:%S %Z") - label = f"v{v['version_no']} — {created_at_local}" + label = f"v{v['version_no']} — {self._fmt_local(v['created_at'])}" if v.get("note"): label += f" · {v['note']}" if v["is_current"]: diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 95ef7f2..87e6d0d 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -106,7 +106,6 @@ class MainWindow(QMainWindow): self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested.connect(self._close_tab) self.tab_widget.currentChanged.connect(self._on_tab_changed) - self._prev_editor = None # Toolbar for controlling styling self.toolBar = ToolBar() @@ -115,7 +114,6 @@ class MainWindow(QMainWindow): # Create the first editor tab self._create_new_tab() - self._prev_editor = self.editor split = QSplitter() split.addWidget(left_panel) @@ -171,7 +169,9 @@ class MainWindow(QMainWindow): self.statusBar().showMessage("Ready", 800) # Add findBar and add it to the statusBar # FindBar will get the current editor dynamically via a callable - self.findBar = FindBar(lambda: self.editor, shortcut_parent=self, parent=self) + self.findBar = FindBar( + lambda: self.current_editor(), shortcut_parent=self, parent=self + ) self.statusBar().addPermanentWidget(self.findBar) # When the findBar closes, put the caret back in the editor self.findBar.closed.connect(self._focus_editor_now) @@ -431,7 +431,7 @@ class MainWindow(QMainWindow): self.tab_widget.setCurrentIndex(index) # Load the date's content - self._load_date_into_editor(date) + self._load_date_into_editor(date, editor) # Store the date with the editor so we can save it later editor.current_date = date @@ -454,7 +454,6 @@ class MainWindow(QMainWindow): if editor: # Save before closing self._save_editor_content(editor) - self._dirty = False self.tab_widget.removeTab(index) @@ -463,19 +462,9 @@ class MainWindow(QMainWindow): if index < 0: return - # If we had pending edits, flush them from the tab we're leaving. - try: - self._save_timer.stop() # avoid a pending autosave targeting the *new* tab - except Exception: - pass - - if getattr(self, "_prev_editor", None) is not None and self._dirty: - self._save_editor_content(self._prev_editor) - self._dirty = False # we just saved the edited tab - - # Update calendar selection to match the tab editor = self.tab_widget.widget(index) if editor and hasattr(editor, "current_date"): + # Update calendar selection to match the tab with QSignalBlocker(self.calendar): self.calendar.setSelectedDate(editor.current_date) @@ -485,19 +474,23 @@ class MainWindow(QMainWindow): # Focus the editor QTimer.singleShot(0, self._focus_editor_now) - # Remember this as the "previous" editor for next switch - self._prev_editor = editor - def _call_editor(self, method_name, *args): """ Call the relevant method of the MarkdownEditor class on bind """ - getattr(self.editor, method_name)(*args) + ed = self.current_editor() + if ed is None: + return + getattr(ed, method_name)(*args) + + def current_editor(self) -> MarkdownEditor | None: + """Get the currently active editor.""" + return self.tab_widget.currentWidget() @property def editor(self) -> MarkdownEditor | None: - """Get the currently active editor.""" - return self.tab_widget.currentWidget() + """Compatibility property to get current editor (for existing code).""" + return self.current_editor() def _date_from_calendar_pos(self, pos) -> QDate | None: """Translate a QCalendarWidget local pos to the QDate under the cursor.""" @@ -808,12 +801,16 @@ class MainWindow(QMainWindow): def _load_selected_date(self, date_iso=False, extra_data=False): """Load a date into the current editor""" + editor = self.current_editor() + if not editor: + return + if not date_iso: date_iso = self._current_date_iso() qd = QDate.fromString(date_iso, "yyyy-MM-dd") - self._load_date_into_editor(qd, extra_data) - self.editor.current_date = qd + self._load_date_into_editor(qd, editor, extra_data) + editor.current_date = qd # Update tab title current_index = self.tab_widget.currentIndex() @@ -823,7 +820,9 @@ class MainWindow(QMainWindow): # Keep tabs sorted by date self._reorder_tabs_by_date() - def _load_date_into_editor(self, date: QDate, extra_data=False): + def _load_date_into_editor( + self, date: QDate, editor: MarkdownEditor, extra_data=False + ): """Load a specific date's content into a given editor.""" date_iso = date.toString("yyyy-MM-dd") try: @@ -834,14 +833,14 @@ class MainWindow(QMainWindow): text += "\n" text += extra_data # Force a save now so we don't lose it. - self._set_editor_markdown_preserve_view(text) + self._set_editor_markdown_preserve_view(text, editor) self._dirty = True self._save_date(date_iso, True) except Exception as e: QMessageBox.critical(self, "Read Error", str(e)) return - self._set_editor_markdown_preserve_view(text) + self._set_editor_markdown_preserve_view(text, editor) self._dirty = False def _save_editor_content(self, editor: MarkdownEditor): @@ -922,6 +921,10 @@ class MainWindow(QMainWindow): if getattr(self, "_showing_context_menu", False): return + editor = self.current_editor() + if not editor: + return + # Stop pending autosave and persist current buffer if needed try: self._save_timer.stop() @@ -929,14 +932,14 @@ class MainWindow(QMainWindow): pass # Save the current editor's content if dirty - if hasattr(self.editor, "current_date") and self._dirty: - prev_date_iso = self.editor.current_date.toString("yyyy-MM-dd") + if hasattr(editor, "current_date") and self._dirty: + prev_date_iso = editor.current_date.toString("yyyy-MM-dd") self._save_date(prev_date_iso, explicit=False) # Now load the newly selected date into the current tab new_date = self.calendar.selectedDate() - self._load_date_into_editor(new_date) - self.editor.current_date = new_date + self._load_date_into_editor(new_date, editor) + editor.current_date = new_date # Update tab title current_index = self.tab_widget.currentIndex() @@ -1000,6 +1003,10 @@ class MainWindow(QMainWindow): except Exception: pass + editor = self.current_editor() + if not editor or not hasattr(editor, "current_date"): + return + if explicit: # Prompt for a note dlg = SaveDialog(self) @@ -1009,7 +1016,7 @@ class MainWindow(QMainWindow): else: note = "autosave" # Save the current editor's date - date_iso = self.editor.current_date.toString("yyyy-MM-dd") + date_iso = editor.current_date.toString("yyyy-MM-dd") self._save_date(date_iso, explicit, note) try: self._save_timer.start() @@ -1307,18 +1314,17 @@ If you want an encrypted backup, choose Backup instead of Export. return if not self.isActiveWindow(): return + editor = self.current_editor() + if not editor: + return # Belt-and-suspenders: do it now and once more on the next tick - self.editor.setFocus(Qt.ActiveWindowFocusReason) - self.editor.ensureCursorVisible() + editor.setFocus(Qt.ActiveWindowFocusReason) + editor.ensureCursorVisible() QTimer.singleShot( 0, lambda: ( - ( - self.editor.setFocus(Qt.ActiveWindowFocusReason) - if self.editor - else None - ), - self.editor.ensureCursorVisible() if self.editor else None, + editor.setFocus(Qt.ActiveWindowFocusReason) if editor else None, + editor.ensureCursorVisible() if editor else None, ), ) @@ -1333,36 +1339,44 @@ If you want an encrypted backup, choose Backup instead of Export. if ev.type() == QEvent.ActivationChange and self.isActiveWindow(): QTimer.singleShot(0, self._focus_editor_now) - def _set_editor_markdown_preserve_view(self, markdown: str): + def _set_editor_markdown_preserve_view( + self, markdown: str, editor: MarkdownEditor | None = None + ): + if editor is None: + editor = self.current_editor() + if not editor: + return + + ed = editor # Save caret/selection and scroll - cur = self.editor.textCursor() + cur = ed.textCursor() old_pos, old_anchor = cur.position(), cur.anchor() - v = self.editor.verticalScrollBar().value() - h = self.editor.horizontalScrollBar().value() + v = ed.verticalScrollBar().value() + h = ed.horizontalScrollBar().value() # Only touch the doc if it actually changed - self.editor.blockSignals(True) - if self.editor.to_markdown() != markdown: - self.editor.from_markdown(markdown) - self.editor.blockSignals(False) + ed.blockSignals(True) + if ed.to_markdown() != markdown: + ed.from_markdown(markdown) + ed.blockSignals(False) # Restore scroll first - self.editor.verticalScrollBar().setValue(v) - self.editor.horizontalScrollBar().setValue(h) + ed.verticalScrollBar().setValue(v) + ed.horizontalScrollBar().setValue(h) # Restore caret/selection (bounded to new doc length) - doc_length = self.editor.document().characterCount() - 1 + doc_length = ed.document().characterCount() - 1 old_pos = min(old_pos, doc_length) old_anchor = min(old_anchor, doc_length) - cur = self.editor.textCursor() + cur = ed.textCursor() cur.setPosition(old_anchor) mode = ( QTextCursor.KeepAnchor if old_anchor != old_pos else QTextCursor.MoveAnchor ) cur.setPosition(old_pos, mode) - self.editor.setTextCursor(cur) + ed.setTextCursor(cur) # Refresh highlights if the theme changed if hasattr(self, "findBar"): diff --git a/bouquin/markdown_editor.py b/bouquin/markdown_editor.py index 9421f23..baea055 100644 --- a/bouquin/markdown_editor.py +++ b/bouquin/markdown_editor.py @@ -44,7 +44,6 @@ class MarkdownHighlighter(QSyntaxHighlighter): def _setup_formats(self): """Setup text formats for different markdown elements.""" - # Bold: **text** or __text__ self.bold_format = QTextCharFormat() self.bold_format.setFontWeight(QFont.Weight.Bold) @@ -262,11 +261,6 @@ class MarkdownEditor(QTextEdit): # We accept plain text, not rich text (markdown is plain text) self.setAcceptRichText(False) - # Normal text - font = QFont() - font.setPointSize(11) - self.setFont(font) - # Install syntax highlighter self.highlighter = MarkdownHighlighter(self.document(), theme_manager) diff --git a/bouquin/settings.py b/bouquin/settings.py index b21835c..2201b09 100644 --- a/bouquin/settings.py +++ b/bouquin/settings.py @@ -9,18 +9,18 @@ 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() - default_db_path = str( - Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) - / "notebook.db" - ) - - path = Path(s.value("db/path", default_db_path)) + 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) diff --git a/pyproject.toml b/pyproject.toml index 117933b..df93682 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.2.1.4" +version = "0.2.1.3" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md" diff --git a/tests/test_settings.py b/tests/test_settings.py index 3f88f6f..254af98 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,5 +1,6 @@ from pathlib import Path from bouquin.settings import ( + default_db_path, get_settings, load_db_config, save_db_config, @@ -7,6 +8,12 @@ from bouquin.settings import ( from bouquin.db import DBConfig +def test_default_db_path_returns_writable_path(app, tmp_path): + p = default_db_path() + assert isinstance(p, Path) + p.parent.mkdir(parents=True, exist_ok=True) + + def test_load_and_save_db_config_roundtrip(app, tmp_path): s = get_settings() for k in ["db/path", "db/key", "ui/idle_minutes", "ui/theme", "ui/move_todos"]: