From b6e5f6a137fc794781828656e0aa48935963b3fe Mon Sep 17 00:00:00 2001 From: zefutoff Date: Wed, 7 Oct 2026 22:13:02 +0200 Subject: [PATCH] feat: add reset-to-default controls --- CHANGELOG.md | 1 + ROADMAP.md | 2 +- .../GrammalecteEngineInstrumentedTest.kt | 150 ++++++++++++++++++ .../unofficial/RuleSettingsActivity.kt | 112 +++++++++++++ .../engine/GrammalecteQuickJsEngine.kt | 29 ++++ .../engine/RealEngineSmokeTest.kt | 96 +++++++++++ 6 files changed, 389 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec0523..e5a9b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Persistent in-app Grammalecte rule settings shared by the native spell checker, PROCESS_TEXT workflow and correction IME. - Persistent dictionary selection between all variants, classic orthography and the 1990 reform, shared by all correction modes. - Persistent personal dictionary with in-app word management, shared by all correction modes. +- Global reset-to-default control restoring the bundled dictionary, Grammalecte rule defaults and clearing the personal dictionary. ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index 385cff0..1e7b09c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -82,7 +82,7 @@ Exit criterion: correction remains usable in common editable applications even w - [x] Grammalecte rule-category settings - [x] Dictionary choice: all variants / classic / 1990 reform - [x] Personal dictionary -- [ ] Reset-to-default controls +- [x] Reset-to-default controls - [ ] Import/export of non-sensitive preferences - [ ] Clear status screen for native spell-checker and IME activation diff --git a/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/GrammalecteEngineInstrumentedTest.kt b/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/GrammalecteEngineInstrumentedTest.kt index 2faa4c6..7fdbac1 100644 --- a/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/GrammalecteEngineInstrumentedTest.kt +++ b/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/GrammalecteEngineInstrumentedTest.kt @@ -278,6 +278,156 @@ class GrammalecteEngineInstrumentedTest { } } + @Test + fun resetToDefaultsPersistsAcrossEngineRestart() { + val context = + ApplicationProvider.getApplicationContext() + + val rulePreferences = + GrammalecteRulePreferences(context) + + val dictionaryPreferences = + GrammalecteDictionaryPreferences(context) + + val personalPreferences = + GrammalectePersonalDictionaryPreferences(context) + + val personalWord = + "grammalecteandroidique" + + rulePreferences.reset() + dictionaryPreferences.reset() + personalPreferences.reset() + + var firstEngine: GrammalecteQuickJsEngine? = + GrammalecteQuickJsEngine(context) + + var secondEngine: GrammalecteQuickJsEngine? = null + + fun isValid( + engine: GrammalecteQuickJsEngine, + word: String, + ): Boolean = + engine + .checkWord( + word = word, + localeTag = "fr-FR", + suggestionLimit = 5, + ).valid + + try { + val currentEngine = + requireNotNull(firstEngine) + + currentEngine.setDictionary( + GrammalecteDictionary.CLASSIC, + ) + + currentEngine.addPersonalDictionaryWord( + personalWord, + ) + + currentEngine.setRuleOption( + id = "apos", + enabled = false, + ) + + assertEquals( + GrammalecteDictionary.CLASSIC, + currentEngine.selectedDictionary(), + ) + + assertTrue( + personalWord in + currentEngine.personalDictionaryWords(), + ) + + assertFalse( + currentEngine + .ruleOptions() + .first { option -> + option.id == "apos" + }.enabled, + ) + + currentEngine.resetToDefaults() + + currentEngine.close() + firstEngine = null + + secondEngine = + GrammalecteQuickJsEngine(context) + + val reopenedEngine = + requireNotNull(secondEngine) + + assertEquals( + GrammalecteDictionary.ALL_VARIANTS, + reopenedEngine.selectedDictionary(), + ) + + assertTrue( + reopenedEngine + .personalDictionaryWords() + .isEmpty(), + ) + + assertFalse( + "Expected personal word to remain removed after restart", + isValid( + reopenedEngine, + personalWord, + ), + ) + + assertTrue( + "Expected classic spelling after restart", + isValid( + reopenedEngine, + "coût", + ), + ) + + assertTrue( + "Expected reformed spelling after restart", + isValid( + reopenedEngine, + "cout", + ), + ) + + assertTrue( + "Expected rule options to remain at defaults after restart", + reopenedEngine + .ruleOptions() + .all { option -> + option.enabled == + option.defaultEnabled + }, + ) + + assertEquals( + GrammalecteDictionary.ALL_VARIANTS, + dictionaryPreferences.selected(), + ) + + assertTrue( + personalPreferences.words().isEmpty(), + ) + + assertTrue( + rulePreferences.overrides().isEmpty(), + ) + } finally { + firstEngine?.close() + secondEngine?.close() + + rulePreferences.reset() + dictionaryPreferences.reset() + personalPreferences.reset() + } + } + private fun withEngine(block: (GrammalecteQuickJsEngine) -> Unit) { val context = ApplicationProvider.getApplicationContext() diff --git a/app/src/main/kotlin/fr/grammalecteandroid/unofficial/RuleSettingsActivity.kt b/app/src/main/kotlin/fr/grammalecteandroid/unofficial/RuleSettingsActivity.kt index 20ec62b..76d7f07 100644 --- a/app/src/main/kotlin/fr/grammalecteandroid/unofficial/RuleSettingsActivity.kt +++ b/app/src/main/kotlin/fr/grammalecteandroid/unofficial/RuleSettingsActivity.kt @@ -1,6 +1,7 @@ package fr.grammalecteandroid.unofficial import android.app.Activity +import android.app.AlertDialog import android.content.Intent import android.graphics.Typeface import android.graphics.drawable.GradientDrawable @@ -40,6 +41,7 @@ class RuleSettingsActivity : Activity() { private lateinit var dictionaryGroup: RadioGroup private lateinit var optionsContainer: LinearLayout private lateinit var resetButton: Button + private lateinit var resetAllButton: Button private var engine: GrammalecteQuickJsEngine? = null private var selectedDictionary = @@ -149,6 +151,51 @@ class RuleSettingsActivity : Activity() { matchWrapParams(), ) + content.addView( + sectionTitle( + "Réinitialisation", + ), + matchWrapParams(), + ) + + content.addView( + TextView(this).apply { + text = + "Restaure le dictionnaire principal, les règles " + + "Grammalecte et vide le dictionnaire personnel." + + textSize = 14f + + setTextColor( + getColor(R.color.surface_text_secondary), + ) + + setPadding( + 0, + 0, + 0, + smallSpacing, + ) + }, + matchWrapParams(), + ) + + resetAllButton = + Button(this).apply { + text = "Réinitialiser tous les réglages" + isAllCaps = false + isEnabled = false + + setOnClickListener { + confirmResetAllSettings() + } + } + + content.addView( + resetAllButton, + matchWrapParams(), + ) + content.addView( sectionTitle( "Dictionnaire", @@ -353,6 +400,7 @@ class RuleSettingsActivity : Activity() { "Chargement des réglages..." resetButton.isEnabled = false + resetAllButton.isEnabled = false dictionaryGroup.removeAllViews() optionsContainer.removeAllViews() @@ -375,6 +423,7 @@ class RuleSettingsActivity : Activity() { .onSuccess { (dictionary, options) -> renderDictionary(dictionary) renderOptions(options) + resetAllButton.isEnabled = true }.onFailure { error -> showError(error) } @@ -708,6 +757,67 @@ class RuleSettingsActivity : Activity() { } } + private fun confirmResetAllSettings() { + AlertDialog + .Builder(this) + .setTitle( + "Réinitialiser tous les réglages ?", + ).setMessage( + "Le dictionnaire principal reviendra à « Toutes variantes », " + + "les règles Grammalecte retrouveront leurs valeurs par défaut " + + "et tous les mots du dictionnaire personnel seront supprimés.\n\n" + + "Cette action est irréversible.", + ).setNegativeButton( + "Annuler", + null, + ).setPositiveButton( + "Réinitialiser", + ) { _, _ -> + resetAllSettings() + }.show() + } + + private fun resetAllSettings() { + resetAllButton.isEnabled = false + resetButton.isEnabled = false + setDictionaryControlsEnabled(false) + + statusView.text = + "Réinitialisation de tous les réglages..." + + executor.execute { + val result = + runCatching { + val currentEngine = + currentEngine() + + currentEngine.resetToDefaults() + + currentEngine.selectedDictionary() to + currentEngine.ruleOptions() + } + + mainHandler.post { + if (destroyed) { + return@post + } + + result + .onSuccess { (dictionary, options) -> + renderDictionary(dictionary) + renderOptions(options) + + resetAllButton.isEnabled = true + + statusView.text = + "Tous les réglages ont été réinitialisés." + }.onFailure { error -> + showError(error) + } + } + } + } + private fun currentEngine(): GrammalecteQuickJsEngine = engine ?: GrammalecteQuickJsEngine( @@ -725,6 +835,8 @@ class RuleSettingsActivity : Activity() { ) resetButton.isEnabled = true + resetAllButton.isEnabled = true + setDictionaryControlsEnabled(true) } private fun sectionTitle(text: String): TextView = diff --git a/engine-grammalecte/src/main/kotlin/fr/grammalecteandroid/engine/GrammalecteQuickJsEngine.kt b/engine-grammalecte/src/main/kotlin/fr/grammalecteandroid/engine/GrammalecteQuickJsEngine.kt index 092d309..175e7f2 100644 --- a/engine-grammalecte/src/main/kotlin/fr/grammalecteandroid/engine/GrammalecteQuickJsEngine.kt +++ b/engine-grammalecte/src/main/kotlin/fr/grammalecteandroid/engine/GrammalecteQuickJsEngine.kt @@ -279,6 +279,35 @@ class GrammalecteQuickJsEngine internal constructor( } } + fun resetToDefaults() { + synchronized(lock) { + applyDictionary( + GrammalecteDictionary.ALL_VARIANTS, + ) + + applyPersonalDictionaryWords( + emptyList(), + ) + + callBridge( + "__grammalecteAndroid.resetRuleOptions", + ) + + dictionaryPreferences?.reset() + personalDictionaryPreferences?.reset() + rulePreferences?.reset() + + appliedDictionary = + GrammalecteDictionary.ALL_VARIANTS + + appliedPersonalDictionaryWords = + emptyList() + + appliedRuleOverrides = + rulePreferences?.overrides() + } + } + private fun synchronizeStoredDictionary() { val preferences = dictionaryPreferences ?: return diff --git a/engine-grammalecte/src/test/kotlin/fr/grammalecteandroid/engine/RealEngineSmokeTest.kt b/engine-grammalecte/src/test/kotlin/fr/grammalecteandroid/engine/RealEngineSmokeTest.kt index e89ef8c..11ab710 100644 --- a/engine-grammalecte/src/test/kotlin/fr/grammalecteandroid/engine/RealEngineSmokeTest.kt +++ b/engine-grammalecte/src/test/kotlin/fr/grammalecteandroid/engine/RealEngineSmokeTest.kt @@ -395,6 +395,102 @@ class RealEngineSmokeTest { } } + @Test + fun resetToDefaultsRestoresEveryUserControl() { + val engine = + GrammalecteQuickJsEngine( + FileAssetTextLoader(findAssetRoot()), + ) + + val personalWord = + "zefutologique" + + fun isValid(word: String): Boolean = + engine + .checkWord( + word = word, + localeTag = "fr-FR", + suggestionLimit = 5, + ).valid + + try { + engine.setDictionary( + GrammalecteDictionary.CLASSIC, + ) + + engine.addPersonalDictionaryWord( + personalWord, + ) + + engine.setRuleOption( + id = "apos", + enabled = false, + ) + + assertEquals( + GrammalecteDictionary.CLASSIC, + engine.selectedDictionary(), + ) + + assertTrue( + personalWord in + engine.personalDictionaryWords(), + ) + + assertTrue( + isValid(personalWord), + ) + + assertFalse( + isValid("cout"), + ) + + assertFalse( + engine + .ruleOptions() + .first { option -> + option.id == "apos" + }.enabled, + ) + + engine.resetToDefaults() + + assertEquals( + GrammalecteDictionary.ALL_VARIANTS, + engine.selectedDictionary(), + ) + + assertTrue( + engine.personalDictionaryWords().isEmpty(), + ) + + assertFalse( + isValid(personalWord), + ) + + assertTrue( + isValid("coût"), + ) + + assertTrue( + isValid("cout"), + ) + + val options = + engine.ruleOptions() + + assertTrue( + "Expected every rule option to match its default", + options.all { option -> + option.enabled == + option.defaultEnabled + }, + ) + } finally { + engine.close() + } + } + @Test fun concurrentCallsShareRuntimeSafely() { val engine =