Fix focusing on editor after leaving the app and returning. More code coverage and removing obsolete bits of code

This commit is contained in:
Miguel Jacq 2025-11-07 13:53:27 +11:00
parent 74177f2cd3
commit aad1ba5d7d
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
16 changed files with 264 additions and 100 deletions

View file

@ -257,67 +257,31 @@ class DBManager:
).fetchall()
return [dict(r) for r in rows]
def get_version(
self,
*,
date_iso: str | None = None,
version_no: int | None = None,
version_id: int | None = None,
) -> dict | None:
def get_version(self, *, version_id: int) -> dict | None:
"""
Fetch a specific version by (date, version_no) OR by version_id.
Fetch a specific version by version_id.
Returns a dict with keys: id, date, version_no, created_at, note, content.
"""
cur = self.conn.cursor()
if version_id is not None:
row = cur.execute(
"SELECT id, date, version_no, created_at, note, content "
"FROM versions WHERE id=?;",
(version_id,),
).fetchone()
else:
if date_iso is None or version_no is None:
raise ValueError(
"Provide either version_id OR (date_iso and version_no)"
)
row = cur.execute(
"SELECT id, date, version_no, created_at, note, content "
"FROM versions WHERE date=? AND version_no=?;",
(date_iso, version_no),
).fetchone()
row = cur.execute(
"SELECT id, date, version_no, created_at, note, content "
"FROM versions WHERE id=?;",
(version_id,),
).fetchone()
return dict(row) if row else None
def revert_to_version(
self,
date_iso: str,
*,
version_no: int | None = None,
version_id: int | None = None,
) -> None:
def revert_to_version(self, date_iso: str, version_id: int) -> None:
"""
Point the page head (pages.current_version_id) to an existing version.
"""
if self.conn is None:
raise RuntimeError("Database is not connected")
cur = self.conn.cursor()
if version_id is None:
if version_no is None:
raise ValueError("Provide version_no or version_id")
row = cur.execute(
"SELECT id FROM versions WHERE date=? AND version_no=?;",
(date_iso, version_no),
).fetchone()
if row is None:
raise ValueError("Version not found for this date")
version_id = int(row["id"])
else:
# Ensure that version_id belongs to the given date
row = cur.execute(
"SELECT date FROM versions WHERE id=?;", (version_id,)
).fetchone()
if row is None or row["date"] != date_iso:
raise ValueError("version_id does not belong to the given date")
# Ensure that version_id belongs to the given date
row = cur.execute(
"SELECT date FROM versions WHERE id=?;", (version_id,)
).fetchone()
if row is None or row["date"] != date_iso:
raise ValueError("version_id does not belong to the given date")
with self.conn:
cur.execute(
@ -341,18 +305,13 @@ class DBManager:
).fetchall()
return [(r[0], r[1]) for r in rows]
def export_json(
self, entries: Sequence[Entry], file_path: str, pretty: bool = True
) -> None:
def export_json(self, entries: Sequence[Entry], file_path: str) -> None:
"""
Export to json.
"""
data = [{"date": d, "content": c} for d, c in entries]
with open(file_path, "w", encoding="utf-8") as f:
if pretty:
json.dump(data, f, ensure_ascii=False, indent=2)
else:
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
json.dump(data, f, ensure_ascii=False, indent=2)
def export_csv(self, entries: Sequence[Entry], file_path: str) -> None:
"""
@ -500,7 +459,7 @@ class DBManager:
elif ext in {".sql", ".sqlite"}:
self.export_sql(file_path)
elif ext == ".md":
self.export_markdown(file_path)
self.export_markdown(entries, file_path)
else:
raise ValueError(f"Unsupported extension: {ext}")

View file

@ -140,10 +140,8 @@ class Editor(QTextEdit):
bc.setPosition(b.position() + b.length())
return blocks > 0 and (codeish / blocks) >= 0.6
def _nearest_code_frame(self, cursor=None, tolerant: bool = False):
def _nearest_code_frame(self, cursor, tolerant: bool = False):
"""Walk up parents from the cursor and return the first code frame."""
if cursor is None:
cursor = self.textCursor()
f = cursor.currentFrame()
while f:
if self._is_code_frame(f, tolerant=tolerant):

View file

@ -26,6 +26,7 @@ from PySide6.QtGui import (
QGuiApplication,
QPalette,
QTextCharFormat,
QTextCursor,
QTextListFormat,
)
from PySide6.QtWidgets import (
@ -146,6 +147,16 @@ class MainWindow(QMainWindow):
QApplication.instance().installEventFilter(self)
# Focus on the editor
self.setFocusPolicy(Qt.StrongFocus)
self.editor.setFocusPolicy(Qt.StrongFocus)
self.toolBar.setFocusPolicy(Qt.NoFocus)
for w in self.toolBar.findChildren(QWidget):
w.setFocusPolicy(Qt.NoFocus)
QGuiApplication.instance().applicationStateChanged.connect(
self._on_app_state_changed
)
# Status bar for feedback
self.statusBar().showMessage("Ready", 800)
@ -481,7 +492,8 @@ class MainWindow(QMainWindow):
# Inject the extra_data before the closing </body></html>
modified = re.sub(r"(<\/body><\/html>)", extra_data_html + r"\1", text)
text = modified
self.editor.setHtml(text)
# Force a save now so we don't lose it.
self._set_editor_html_preserve_view(text)
self._dirty = True
self._save_date(date_iso, True)
@ -489,9 +501,7 @@ class MainWindow(QMainWindow):
QMessageBox.critical(self, "Read Error", str(e))
return
self.editor.blockSignals(True)
self.editor.setHtml(text)
self.editor.blockSignals(False)
self._set_editor_html_preserve_view(text)
self._dirty = False
# track which date the editor currently represents
@ -850,6 +860,8 @@ If you want an encrypted backup, choose Backup instead of Export.
def eventFilter(self, obj, event):
if event.type() == QEvent.KeyPress and not self._locked:
self._idle_timer.start()
if event.type() in (QEvent.ApplicationActivate, QEvent.WindowActivate):
QTimer.singleShot(0, self._focus_editor_now)
return super().eventFilter(obj, event)
def _enter_lock(self):
@ -891,6 +903,7 @@ If you want an encrypted backup, choose Backup instead of Export.
if tb:
tb.setEnabled(True)
self._idle_timer.start()
QTimer.singleShot(0, self._focus_editor_now)
# ----------------- Close handlers ----------------- #
def closeEvent(self, event):
@ -906,3 +919,61 @@ If you want an encrypted backup, choose Backup instead of Export.
except Exception:
pass
super().closeEvent(event)
# ----------------- Below logic helps focus the editor ----------------- #
def _focus_editor_now(self):
"""Give focus to the editor and ensure the caret is visible."""
if getattr(self, "_locked", False):
return
if not self.isActiveWindow():
return
# Belt-and-suspenders: do it now and once more on the next tick
self.editor.setFocus(Qt.ActiveWindowFocusReason)
self.editor.ensureCursorVisible()
QTimer.singleShot(
0,
lambda: (
self.editor.setFocus(Qt.ActiveWindowFocusReason),
self.editor.ensureCursorVisible(),
),
)
def _on_app_state_changed(self, state):
# Called on macOS/Wayland/Windows when the whole app re-activates
if state == Qt.ApplicationActive and self.isActiveWindow():
QTimer.singleShot(0, self._focus_editor_now)
def changeEvent(self, ev):
# Called on some platforms when the window's activation state flips
super().changeEvent(ev)
if ev.type() == QEvent.ActivationChange and self.isActiveWindow():
QTimer.singleShot(0, self._focus_editor_now)
def _set_editor_html_preserve_view(self, html: str):
ed = self.editor
# Save caret/selection and scroll
cur = ed.textCursor()
old_pos, old_anchor = cur.position(), cur.anchor()
v = ed.verticalScrollBar().value()
h = ed.horizontalScrollBar().value()
# Only touch the doc if it actually changed
ed.blockSignals(True)
if ed.toHtml() != html:
ed.setHtml(html)
ed.blockSignals(False)
# Restore scroll first
ed.verticalScrollBar().setValue(v)
ed.horizontalScrollBar().setValue(h)
# Restore caret/selection
cur = ed.textCursor()
cur.setPosition(old_anchor)
mode = (
QTextCursor.KeepAnchor if old_anchor != old_pos else QTextCursor.MoveAnchor
)
cur.setPosition(old_pos, mode)
ed.setTextCursor(cur)