diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a40747..6add14eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ First 0.8.1 beta. Rolls up everything through 0.8.0 (below) plus the features, f - **Smaller installer: the offline speech engine downloads on demand.** The private, on-device speech engine (whisper.cpp) is no longer bundled in the installer. The first time you use offline dictation or transcription, QUILL **offers to download it right there** — about 8 MB, checksum-verified, with a cancelable progress bar (disabled in Safe Mode) — and you can also get it any time from **Tools > Speech > Download Offline Speech Engine...**. **Upgrading from a release that bundled the engine? Your existing copy is kept and keeps working** — nothing to re-download. (First step of the AI footprint/optimization plan: a pinned, SHA-256-verified release-asset acquisition path.) - **Smaller installer: Kokoro neural voices now download on demand.** The ~120 MB Kokoro voice models are no longer bundled in the installer (it is that much smaller to download and install). The first time you choose a Kokoro voice, QUILL downloads it for you — checksum-verified, with a cancelable progress bar — and other read-aloud voices (DECtalk, eSpeak NG, Piper, SAPI) work in the meantime. **If you are upgrading from a release that bundled Kokoro, your existing copy is kept and keeps working** — nothing to re-download. (Proof-of-concept for the AI footprint/optimization plan's "host redistributable components as verified release assets.") +- **Proofread Mastodon posts before sending (per account).** In **Tools → Share → Mastodon Accounts...**, select an account and tick **Spell-check posts before sending** to have QUILL open the Spelling Review (F7) on the post text when you press Post, so you can fix misspellings before it goes out. Off by default and per account, so existing accounts are unaffected until you turn it on. +- **Spell check a document before saving.** A new **Settings → Editing → Spell check a document before saving** option (off by default) opens the Spelling Review (F7) automatically when you Save or Save As, so you can correct the document before the file is written, then the save proceeds. - **Hear how deep your indentation is.** Press **Tab** or **Shift+Tab** and QUILL can now speak the new indentation depth — "4 spaces", "8 spaces", "1 tab" — instead of just "Indented lines", so you always know where the line sits. It honours your tabs-vs-spaces and width choices (**Insert tab characters instead of spaces** and **Number of spaces per indent level** in Settings). Prefer the terse message? Turn off **Announce indentation depth on Tab** (Settings, Accessibility). - **Quieter dialogs, your choice.** A new **Announce entering and leaving dialogs** setting (Settings, Accessibility) turns off the spoken "Entered / Exited *name* dialog" cues for people whose screen reader already announces dialogs. On by default. - **Jump straight to an open document.** With several documents open, press **Alt+1** through **Alt+9** (and **Alt+0** for the tenth) to go directly to that document by its position, instead of cycling with Ctrl+Tab. If nothing is open at that position, QUILL tells you and stays put. The bindings are remappable in the Keymap Editor. diff --git a/docs/release notes/release0.8.1-beta1.epub b/docs/release notes/release0.8.1-beta1.epub index dde2584a..4b1afae0 100644 Binary files a/docs/release notes/release0.8.1-beta1.epub and b/docs/release notes/release0.8.1-beta1.epub differ diff --git a/docs/release notes/release0.8.1-beta1.html b/docs/release notes/release0.8.1-beta1.html index 37dc94b0..cf1442a0 100644 --- a/docs/release notes/release0.8.1-beta1.html +++ b/docs/release notes/release0.8.1-beta1.html @@ -429,6 +429,18 @@

Enhancements

is kept and keeps working — nothing to re-download. This is the proof-of-concept for the footprint plan's "host redistributable components as verified release assets." +
  • Proofread Mastodon posts before sending (per +account). In Tools → Share → Mastodon +Accounts..., select an account and tick Spell-check +posts before sending: pressing Post for that account then opens +the Spelling Review (F7) on the post text so you can fix misspellings +before it goes out. Off by default and per account, so existing accounts +are unaffected until you turn it on.
  • +
  • Spell check a document before saving. A new +Settings → Editing → Spell check a document before +saving option (off by default) opens the Spelling Review (F7) +automatically when you Save or Save As, so you can correct the document +before the file is written; the save then proceeds.
  • Hear how deep your indentation is. Tab / Shift+Tab can now speak the new indentation depth — "4 spaces", "8 spaces", "1 tab" — instead of "Indented lines", honouring your tabs-vs-spaces and diff --git a/docs/release notes/release0.8.1-beta1.md b/docs/release notes/release0.8.1-beta1.md index 24d333f0..afee8110 100644 --- a/docs/release notes/release0.8.1-beta1.md +++ b/docs/release notes/release0.8.1-beta1.md @@ -219,6 +219,15 @@ keys line up again. **Upgrading from a release that bundled Kokoro? Your existing copy is kept and keeps working — nothing to re-download.** This is the proof-of-concept for the footprint plan's "host redistributable components as verified release assets." +- **Proofread Mastodon posts before sending (per account).** In **Tools → Share + → Mastodon Accounts...**, select an account and tick **Spell-check posts before + sending**: pressing Post for that account then opens the Spelling Review (F7) on + the post text so you can fix misspellings before it goes out. Off by default and + per account, so existing accounts are unaffected until you turn it on. +- **Spell check a document before saving.** A new **Settings → Editing → Spell + check a document before saving** option (off by default) opens the Spelling + Review (F7) automatically when you Save or Save As, so you can correct the + document before the file is written; the save then proceeds. - **Hear how deep your indentation is.** Tab / Shift+Tab can now speak the new indentation depth — "4 spaces", "8 spaces", "1 tab" — instead of "Indented lines", honouring your tabs-vs-spaces and indent-width settings. Toggle with diff --git a/docs/user guide/userguide.epub b/docs/user guide/userguide.epub index 8bb2f328..cc03dc04 100644 Binary files a/docs/user guide/userguide.epub and b/docs/user guide/userguide.epub differ diff --git a/docs/user guide/userguide.html b/docs/user guide/userguide.html index c46881af..66fb9a6b 100644 --- a/docs/user guide/userguide.html +++ b/docs/user guide/userguide.html @@ -1479,6 +1479,15 @@

    Spelling Review (F7)

    misspelling, QUILL reads it letter by letter. The pause before spelling starts is configurable.
  • +

    Spell check a document +before saving

    +

    Turn on Settings → Editing → Spell check a document before +saving (off by default) and QUILL opens the same Spelling +Review (F7) automatically whenever you save, so you can correct +misspellings before the file is written. Review or skip the issues as +usual; the save then proceeds with your corrections. This applies to +Save and Save As for the document you +are editing.

    Editor shortcuts (without opening the dialog):

    @@ -4898,6 +4907,9 @@

    Settings you can change
  • system tray mode
  • persistent undo
  • spell check as you type
  • +
  • spell check a document before saving — when on, +saving opens the Spelling Review (F7) first so you can correct the +document before it is written. Off by default.
  • line-number visibility
  • whether Quill starts with no document open
  • suggest a filename from the first line — when on, @@ -5065,6 +5077,15 @@

    Adding a Mastodon account

    a default, or remove one (which deletes its saved sign-in from this computer). Your sign-in is stored securely in the Windows Credential Manager, never in a plain file.

    +

    Proofread posts before sending. In Mastodon +Accounts..., select an account and tick Spell-check +posts before sending to turn on per-account proofreading (off +by default). When it is on, pressing Post for that +account first opens the Spelling Review (F7) on the post text so you can +fix misspellings, and the post is sent only after you finish or skip the +review. The setting is per account, so you can enable it for some +accounts and not others; existing accounts are unaffected until you turn +it on.

    Working with Different Document Types

    Quill is strongest today with plain text, Markdown, HTML, RTF, EPUB, diff --git a/docs/user guide/userguide.md b/docs/user guide/userguide.md index 022b6104..bf9079e1 100644 --- a/docs/user guide/userguide.md +++ b/docs/user guide/userguide.md @@ -715,6 +715,14 @@ connection, nothing uploaded. - **Spell word aloud** — after announcing the misspelling, QUILL reads it letter by letter. The pause before spelling starts is configurable. +#### Spell check a document before saving + +Turn on **Settings → Editing → Spell check a document before saving** (off by +default) and QUILL opens the same Spelling Review (F7) automatically whenever you +save, so you can correct misspellings before the file is written. Review or skip +the issues as usual; the save then proceeds with your corrections. This applies +to **Save** and **Save As** for the document you are editing. + **Editor shortcuts** (without opening the dialog): | Key | Action | @@ -2558,6 +2566,7 @@ Quill's current settings and customization surface covers the things you are mos - system tray mode - persistent undo - spell check as you type +- **spell check a document before saving** — when on, saving opens the Spelling Review (F7) first so you can correct the document before it is written. Off by default. - line-number visibility - whether Quill starts with no document open - **suggest a filename from the first line** — when on, saving an untitled document pre-fills the Save dialog with a name taken from the first line (across formats; leading markup like a Markdown heading, quote, or list bullet is stripped). Off by default. @@ -2661,6 +2670,8 @@ The first time you post, Quill offers to add an account; you can also open **Too You can add several accounts, give each its own nickname, **set a default**, or **remove** one (which deletes its saved sign-in from this computer). Your sign-in is stored securely in the Windows Credential Manager, never in a plain file. +**Proofread posts before sending.** In **Mastodon Accounts...**, select an account and tick **Spell-check posts before sending** to turn on per-account proofreading (off by default). When it is on, pressing **Post** for that account first opens the Spelling Review (F7) on the post text so you can fix misspellings, and the post is sent only after you finish or skip the review. The setting is per account, so you can enable it for some accounts and not others; existing accounts are unaffected until you turn it on. + ## Working with Different Document Types Quill is strongest today with plain text, Markdown, HTML, RTF, EPUB, and extracted text workflows. It also has intake and extraction review features for imported material such as PDF/OCR sources and structured import support for Office-style formats. diff --git a/quill/core/mastodon/accounts.py b/quill/core/mastodon/accounts.py index 83dba706..58a9ddaf 100644 --- a/quill/core/mastodon/accounts.py +++ b/quill/core/mastodon/accounts.py @@ -46,6 +46,11 @@ class MastodonAccount: instance_url: str handle: str # e.g. "@user@mastodon.social", for display client_id: str + # When True, posting through this account first opens the F7 spelling review + # on the post text so the user can fix misspellings before it is sent. Off by + # default; accounts saved before this field existed load as False, so the + # feature is purely opt-in and existing accounts are unaffected (#post-spellcheck). + spell_check_before_post: bool = False @property def display_name(self) -> str: @@ -81,6 +86,9 @@ def list_accounts() -> list[MastodonAccount]: instance_url=str(entry.get("instance_url", "")), handle=str(entry.get("handle", "")), client_id=str(entry.get("client_id", "")), + # Missing on accounts saved before the field existed -> False + # (opt-in migration: existing accounts stay off). + spell_check_before_post=bool(entry.get("spell_check_before_post", False)), ) ) except KeyError: @@ -114,6 +122,7 @@ def _persist(accounts: list[MastodonAccount], default_id: str | None) -> None: "instance_url": a.instance_url, "handle": a.handle, "client_id": a.client_id, + "spell_check_before_post": a.spell_check_before_post, } for a in accounts ], @@ -130,6 +139,7 @@ def add_account( client_id: str, client_secret: str, access_token: str, + spell_check_before_post: bool = False, ) -> MastodonAccount: """Store a new account's metadata + secrets; return it. Becomes default if first.""" account = MastodonAccount( @@ -138,6 +148,7 @@ def add_account( instance_url=instance_url, handle=handle, client_id=client_id, + spell_check_before_post=spell_check_before_post, ) save_secret(_token_cred(account.id), access_token) save_secret(_secret_cred(account.id), client_secret) @@ -166,6 +177,20 @@ def set_default_account(account_id: str) -> None: _persist(accounts, account_id) +def set_spell_check_before_post(account_id: str, enabled: bool) -> None: + """Turn the pre-post spelling review on/off for one account (off by default).""" + from dataclasses import replace + + accounts = list_accounts() + if not any(a.id == account_id for a in accounts): + return + updated = [ + replace(a, spell_check_before_post=bool(enabled)) if a.id == account_id else a + for a in accounts + ] + _persist(updated, default_account_id()) + + def access_token_for(account_id: str) -> str: """Return the stored access token for *account_id* (``""`` when missing).""" return load_secret(_token_cred(account_id)) @@ -181,4 +206,5 @@ def access_token_for(account_id: str) -> str: "list_accounts", "remove_account", "set_default_account", + "set_spell_check_before_post", ] diff --git a/quill/core/settings.py b/quill/core/settings.py index da46ea28..615da01d 100644 --- a/quill/core/settings.py +++ b/quill/core/settings.py @@ -83,6 +83,9 @@ class Settings: tray_enabled: bool = False persistent_undo: bool = False spellcheck_as_you_type: bool = False + # When True, saving a document first opens the F7 spelling review so the user + # can correct misspellings before the file is written. Off by default. + spell_check_before_save: bool = False intellisense_as_you_type: bool = False snippet_trigger_expansion: bool = True preview_browser: str = "system" @@ -515,6 +518,7 @@ def from_dict(cls, data: dict[str, Any]) -> Settings: tray_enabled = bool(data.get("tray_enabled", False)) persistent_undo = bool(data.get("persistent_undo", False)) spellcheck_as_you_type = bool(data.get("spellcheck_as_you_type", False)) + spell_check_before_save = bool(data.get("spell_check_before_save", False)) intellisense_as_you_type = bool(data.get("intellisense_as_you_type", False)) snippet_trigger_expansion = bool(data.get("snippet_trigger_expansion", True)) preview_browser = str(data.get("preview_browser", "system")).strip() or "system" @@ -1035,6 +1039,7 @@ def from_dict(cls, data: dict[str, Any]) -> Settings: tray_enabled=tray_enabled, persistent_undo=persistent_undo, spellcheck_as_you_type=spellcheck_as_you_type, + spell_check_before_save=spell_check_before_save, intellisense_as_you_type=intellisense_as_you_type, snippet_trigger_expansion=snippet_trigger_expansion, preview_browser=preview_browser, diff --git a/quill/core/settings_specs.py b/quill/core/settings_specs.py index 6c5fe9e3..d4b01670 100644 --- a/quill/core/settings_specs.py +++ b/quill/core/settings_specs.py @@ -375,6 +375,16 @@ def _ai_tts_voice_choices() -> tuple[tuple[str, str], ...]: feature_id="core.spellcheck", keywords=("spelling", "spell check", "typos"), ), + SettingSpec( + "spell_check_before_save", + "Spell check a document before saving", + "editing", + "bool", + "Open the spelling review (F7) when you save, so you can correct " + "misspellings before the file is written. Off by default.", + feature_id="core.spellcheck", + keywords=("spelling", "spell check", "save", "proofread", "before saving"), + ), SettingSpec( "intellisense_as_you_type", "Word prediction and tag IntelliSense", diff --git a/quill/tools/module_size_budgets.json b/quill/tools/module_size_budgets.json index 8230836d..cf4d0759 100644 --- a/quill/tools/module_size_budgets.json +++ b/quill/tools/module_size_budgets.json @@ -25,6 +25,7 @@ "_rebaseline_2026_06_26_warm_settings_and_kokoro": "read_aloud.py 1258->1299, settings.py 1201->1209, main_frame.py 26142->26145, main_frame_speech.py 1277->1300, and a new main_frame_dictation_hotkeys.py entry at 601 for: (1) the live voice-preview phrase now matches the recorded samples (the phrase.txt tagline) instead of the quick-brown-fox line; (2) warm_dictation_model / warm_kokoro_model settings (default on) so users can disable background model warm-up; (3) bundled-Kokoro discovery (default_kokoro_model_dir prefers a user copy, else {QUILL_APP_ROOT}/kokoro-models) so Kokoro can ship in the installer without a download; (4) warm_kokoro_onnx() + prewarm_kokoro_model() background warm gated on the setting. Budgets re-baselined.", "_rebaseline_2026_06_26_dictation_transcribe_dialog": "main_frame_dictation_hotkeys.py 601->621 (+20) for the dictation transcription progress dialog (#700): the transcribe path now shows the same AIProgressDialog (percentage + Minimize to status bar, quiet mirroring via _set_status_quiet) as the audio/video file path, then closes on completion and hands the text to the controller (which inserts and announces the word count). Replaces the bare _run_background_task path. Budget re-baselined.", "_rebaseline_2026_06_26_transcribe_progress_dialog": "main_frame_speech.py 1300->1337 (+37) for the audio/video transcription progress dialog (#700): transcribe_audio_offline() now shows an AIProgressDialog with a real percentage (Faster Whisper reports segment-based progress), a \"Minimize to status bar\" button, and quiet status mirroring (_set_status_quiet) so a minimized run is not chatty; on completion it closes (clearing its status line) and opens the transcript, which announces the word count once. Replaces the bare _run_background_task status-bar path. Budget re-baselined.", + "_rebaseline_2026_06_29_spellcheck_before_post_and_save": "settings.py 1279->1284 (+5) and settings_specs.py 1820->1830 (+10) for the proofread-before-send / spell-check-before-save feature: a new Settings.spell_check_before_save field (default off) with its load() coercion, plus the searchable SettingSpec. settings.py / settings_specs.py are the canonical homes for user settings and their specs (same accepted settings-field rebaseline pattern). All other logic lives in untracked or headroom modules: the new wx-free quill/ui/spell_review.py helper, the per-account MastodonAccount.spell_check_before_post field (quill/core/mastodon/accounts.py, under cap), and the UI hooks in main_frame.py / mastodon_dialogs.py (within their budgets). Budgets re-baselined to current size.", "_rebaseline_2026_06_29_whisper_unbundle": "main_frame_dictation_hotkeys.py 623->635 (+12) for the whisper.cpp unbundle (PRD 10.2.4): the dictation pre-flight now OFFERS to download the offline speech engine (Tools > Speech > Download Offline Speech Engine, via release_assets) when it is missing, instead of just pointing at a menu -- so a fresh install is never stranded. whisper is no longer bundled in the installer (build + iss generator updated; iss regenerated); fresh installs fetch the ~8 MB engine on demand (verified), upgraders keep their copy. Budget re-baselined.", "_rebaseline_2026_06_29_kokoro_unbundle": "main_frame_speech.py 1423->1427 (+4) for the Kokoro unbundle (PRD 10.2.4): download_offline_speech_engine() now passes should_cancel/label to release_assets.fetch_component (cancel handled in-module rather than via a swallowed progress-raise). The Kokoro download handler itself (main_frame.py) was rewritten to route through release_assets and net shrank. Kokoro is no longer bundled in the installer; it downloads on demand from QUILL's verified release asset, and upgraders keep their existing copy. Budget re-baselined.", "_rebaseline_2026_06_29_on_demand_speech_engine": "main_frame_speech.py 1344->1423 (+79) and main_frame_menu.py 4075->4087 (+12) for the on-demand offline speech engine (whisper.cpp) download (PRD 10.2.4): download_offline_speech_engine() worker (mirrors the existing download_ffmpeg/download_faster_whisper handlers, which already live here) + a 'Download Offline Speech Engine...' item in Tools > Speech with its id and EVT_MENU binding. All acquisition logic is in the new wx-free quill/core/release_assets.py (pinned + SHA-256-verified fetch from QUILL's own GitHub release asset; under the 600 default cap). The engine still ships bundled in the installer; this is the recovery/optional path. Budgets re-baselined to current size.", @@ -288,9 +289,9 @@ "quill/core/publishing_clients.py": 624, "quill/core/quillins/validation.py": 1324, "quill/core/read_aloud.py": 1299, - "quill/core/settings.py": 1279, + "quill/core/settings.py": 1284, "quill/core/settings_registry.py": 158, - "quill/core/settings_specs.py": 1820, + "quill/core/settings_specs.py": 1830, "quill/core/watch_actions.py": 789, "quill/io/rtf.py": 712, "quill/io/rtf_model.py": 831, diff --git a/quill/ui/main_frame.py b/quill/ui/main_frame.py index 516890a2..06ce3bfc 100644 --- a/quill/ui/main_frame.py +++ b/quill/ui/main_frame.py @@ -9387,6 +9387,10 @@ def save_file(self) -> None: if self.document.path is None: self.save_file_as() return + # Optional pre-save proofread (off by default): review misspellings in the + # editor before the file is written. save_file_as runs its own copy. + if getattr(self.settings, "spell_check_before_save", False): + self.open_spell_check_dialog() if self.document.modified: backup_document(self.document) self._write_document_to_disk(self.document) @@ -9604,6 +9608,10 @@ def save_file_as(self) -> None: target = self._resolve_save_target(Path(dialog.GetPath()), chosen_filter) self._last_file_dir = str(target.parent) + # Optional pre-save proofread (off by default), after the user has chosen + # the destination but before the file is written. + if getattr(self.settings, "spell_check_before_save", False): + self.open_spell_check_dialog() self.document.set_text(self.editor.GetValue()) if self.document.modified and self.document.path is not None: backup_document(self.document) @@ -13400,6 +13408,7 @@ def post_to_mastodon(self) -> None: accounts=accounts, default_account_id=_accounts.default_account_id(), announce=self._announce_result, + spell_review=self._spell_review_textctrl, ) if dialog.show() == self._wx.ID_OK: url = dialog.posted_url or "" @@ -16269,6 +16278,26 @@ def show_dictionary_status(self) -> None: f"document={document_count}, project={project_count}" ) + def _spell_review_textctrl(self, text_ctrl: object) -> None: + """Run the F7 spelling review over a wx text control (e.g. a Mastodon post). + + Mirrors open_spell_check_dialog but targets the given control instead of + the editor, so corrections apply to the post text before it is sent (#549 + Mastodon proofread-before-send). + """ + from quill.ui.spell_review import review_textctrl + + review_textctrl( + self._wx, + self.frame, + text_ctrl, + dictionary=self._spell_dictionary(), + announce_fn=self._announce, + settings=self.settings, + show_modal=self._show_modal_dialog, + ) + self._invalidate_spell_dictionary_cache() + def open_spell_check_dialog(self) -> None: """Open the guided F7 Spelling Review dialog.""" from quill.core.spelling.session import ReviewSession diff --git a/quill/ui/mastodon_dialogs.py b/quill/ui/mastodon_dialogs.py index 3ddf073f..ab7743f0 100644 --- a/quill/ui/mastodon_dialogs.py +++ b/quill/ui/mastodon_dialogs.py @@ -33,11 +33,15 @@ def __init__( accounts: list[account_store.MastodonAccount], default_account_id: str | None, announce=None, + spell_review=None, ) -> None: import wx self._wx = wx self._announce = announce or (lambda _m: None) + # Optional callable(text_ctrl) -> None that runs the F7 spelling review + # over the post text. Provided by MainFrame; None in tests / headless use. + self._spell_review = spell_review self._accounts = accounts self.posted_url: str | None = None @@ -117,6 +121,15 @@ def _on_post(self, _event: object) -> None: if account is None: self._announce("Add an account first.") return + # Proofread before sending when this account opts in (off by default). + # The review edits the post text in place; re-read it afterwards. + if account.spell_check_before_post and self._spell_review is not None: + self._spell_review(self._text) + text = self._text.GetValue() + if not text.strip(): + self._announce("Cannot post: the text is empty.") + self._text.SetFocus() + return visibility = client.VISIBILITIES[max(0, self._visibility.GetSelection())][0] token = account_store.access_token_for(account.id) if not token: @@ -156,6 +169,12 @@ def __init__(self, parent: object, *, announce=None) -> None: self._list.SetName("Mastodon accounts") root.Add(self._list, 1, wx.EXPAND | wx.ALL, 12) + self._spellcheck_check = wx.CheckBox( + self.dialog, label="Spell-check &posts before sending (selected account)" + ) + self._spellcheck_check.SetName("Spell-check posts before sending") + root.Add(self._spellcheck_check, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 12) + row = wx.BoxSizer(wx.HORIZONTAL) add_button = wx.Button(self.dialog, label="&Add Account...") self._remove_button = wx.Button(self.dialog, label="&Remove") @@ -181,14 +200,36 @@ def __init__(self, parent: object, *, announce=None) -> None: self._remove_button.Bind(wx.EVT_BUTTON, lambda _e: self._on_remove()) self._default_button.Bind(wx.EVT_BUTTON, lambda _e: self._on_set_default()) self._list.Bind(wx.EVT_LISTBOX, lambda _e: self._update_button_states()) + self._spellcheck_check.Bind(wx.EVT_CHECKBOX, lambda _e: self._on_toggle_spellcheck()) self._refresh() self._list.SetFocus() def _update_button_states(self) -> None: - """Enable Remove / Set as Default only when an account is selected.""" - has_selection = self._selected_id() is not None + """Enable Remove / Set as Default and sync the spell-check box to selection.""" + account = self._selected_account_obj() + has_selection = account is not None self._remove_button.Enable(has_selection) self._default_button.Enable(has_selection) + # Read the flag fresh from the store so it is always correct regardless of + # toggles, and never resets the list selection (unlike _refresh). + self._spellcheck_check.Enable(has_selection) + self._spellcheck_check.SetValue(bool(account and account.spell_check_before_post)) + + def _selected_account_obj(self) -> account_store.MastodonAccount | None: + account_id = self._selected_id() + return account_store.get_account(account_id) if account_id else None + + def _on_toggle_spellcheck(self) -> None: + account_id = self._selected_id() + if account_id is None: + return + enabled = bool(self._spellcheck_check.GetValue()) + account_store.set_spell_check_before_post(account_id, enabled) + self._announce( + "Spell-check before posting turned on for this account." + if enabled + else "Spell-check before posting turned off for this account." + ) def _refresh(self) -> None: accounts = account_store.list_accounts() diff --git a/quill/ui/spell_review.py b/quill/ui/spell_review.py new file mode 100644 index 00000000..313fbd40 --- /dev/null +++ b/quill/ui/spell_review.py @@ -0,0 +1,63 @@ +"""Run the F7 spelling review over an arbitrary wx text control. + +``MainFrame.open_spell_check_dialog`` runs the guided spelling review against the +main editor. This helper runs the same review (``ReviewSession`` + +``SpellingReviewDialog``) against any ``wx.TextCtrl`` — e.g. the Mastodon compose +box — so corrections are applied back into that control before its text is used. +Kept out of ``main_frame`` so the compose dialog need not import the frame. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +def review_textctrl( + wx: Any, + parent: Any, + text_ctrl: Any, + *, + dictionary: Any, + announce_fn: Any, + settings: Any, + show_modal: Any, + scope_label: str = "post", + document_path: Path | None = None, +) -> None: + """Spell-check ``text_ctrl`` in place via the guided review dialog. + + No-op when the control is empty or already clean. Corrections are written + back into the control with ``Replace`` (the same call the editor path uses), + so the caller reads the corrected value afterwards. + """ + from quill.core.spelling.session import ReviewSession + from quill.ui.spelling_review_dialog import SpellingReviewDialog + + text = text_ctrl.GetValue() + if not text.strip(): + return + session = ReviewSession( + text=text, + dictionary=set(dictionary), + scope_start=0, + scope_end=len(text), + ) + if session.is_complete(): + announce_fn("No misspellings found.") + return + + def _apply(start: int, old_end: int, replacement: str) -> None: + text_ctrl.Replace(start, old_end, replacement) + + dlg = SpellingReviewDialog( + parent=parent, + session=session, + apply_fn=_apply, + announce_fn=announce_fn, + document_path=document_path, + project_root=Path.cwd(), + settings=settings, + scope_label=scope_label, + ) + dlg.show(show_modal) diff --git a/tests/unit/core/test_mastodon_accounts.py b/tests/unit/core/test_mastodon_accounts.py index 90455e78..60693987 100644 --- a/tests/unit/core/test_mastodon_accounts.py +++ b/tests/unit/core/test_mastodon_accounts.py @@ -95,3 +95,56 @@ def test_accounts_file_carries_a_schema_version(store: dict[str, str]) -> None: encoding="utf-8", ) assert [a.nickname for a in accounts.list_accounts()] == ["Main"] + + +def test_spell_check_before_post_defaults_off(store: dict[str, str]) -> None: + account = _add("Main") + assert account.spell_check_before_post is False + assert accounts.list_accounts()[0].spell_check_before_post is False + + +def test_set_spell_check_before_post_toggles_and_persists(store: dict[str, str]) -> None: + account = _add("Main") + accounts.set_spell_check_before_post(account.id, True) + assert accounts.get_account(account.id).spell_check_before_post is True + accounts.set_spell_check_before_post(account.id, False) + assert accounts.get_account(account.id).spell_check_before_post is False + + +def test_add_account_can_enable_pre_post_review(store: dict[str, str]) -> None: + account = accounts.add_account( + nickname="Pre", + instance_url="https://mastodon.social", + handle="@pre@mastodon.social", + client_id="cid", + client_secret="csecret", + access_token="tok", + spell_check_before_post=True, + ) + assert accounts.get_account(account.id).spell_check_before_post is True + + +def test_existing_account_without_field_migrates_to_off(store: dict[str, str]) -> None: + import json as _json + from pathlib import Path + + # Simulate an account saved before the field existed (opt-in migration). + Path(str(accounts.accounts_path())).write_text( + _json.dumps({ + "schema_version": 1, + "accounts": [ + { + "id": "abc123", + "nickname": "Legacy", + "instance_url": "https://mastodon.social", + "handle": "@legacy@mastodon.social", + "client_id": "cid", + } + ], + "default_id": "abc123", + }), + encoding="utf-8", + ) + account = accounts.get_account("abc123") + assert account is not None + assert account.spell_check_before_post is False