Fix focusing on editor after leaving the app and returning. More code coverage and removing obsolete bits of code
This commit is contained in:
parent
74177f2cd3
commit
aad1ba5d7d
16 changed files with 264 additions and 100 deletions
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue