From 49279227afd050f5fde40ae2fca49c90eabe6c21 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 19 Sep 2024 10:42:33 +0700 Subject: [PATCH 01/21] feat(android): Add radio-button selections for auto-correct --- .../com/keyman/android/SystemKeyboard.java | 5 +- .../kmapro/LanguageSettingsActivity.java | 114 ++++++++---------- .../KMEA/app/src/main/assets/android-host.js | 39 +++++- .../engine/KMKeyboardWebViewClient.java | 3 +- .../java/com/keyman/engine/KMManager.java | 61 +++++++++- .../layout/language_settings_list_layout.xml | 53 +++++--- .../KMEA/app/src/main/res/values/strings.xml | 13 +- 7 files changed, 197 insertions(+), 91 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 4fb1c567922..23053d21d48 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -169,8 +169,9 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { if (kbInfo != null) { String langId = kbInfo.getLanguageID(); SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - boolean mayPredict = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(langId), true); - KMManager.setBannerOptions(mayPredict); + int maySuggest = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey(langId), KMManager.KMDefault_Suggestion); + // Enable banner if maySuggest is not SuggestionType.SUGGESTIONS_DISABLED (0) + KMManager.setBannerOptions(maySuggest > 0); } else { KMManager.setBannerOptions(false); } diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java index 75116bad8cc..61972d2fc06 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java @@ -16,9 +16,13 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.RadioGroup.OnCheckedChangeListener; import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -55,50 +59,6 @@ public final class LanguageSettingsActivity extends AppCompatActivity { private final static String TAG = "LanguageSettingsAct"; - private class PreferenceToggleListener implements View.OnClickListener { - String prefsKey; - String lgCode; - - public PreferenceToggleListener(String prefsKey, String lgCode) { - this.prefsKey = prefsKey; - this.lgCode = lgCode; - } - - @Override - public void onClick(View v) { - // For predictions/corrections toggle - SwitchCompat toggle = (SwitchCompat) v; - - SharedPreferences.Editor prefEditor = prefs.edit(); - - // predictionsToggle overrides correctionToggle and correctionsTextView - if (prefsKey.endsWith(KMManager.predictionPrefSuffix)) { - boolean override = toggle.isChecked(); - overrideCorrectionsToggle(override); - } - - // This will allow preemptively making settings for languages without models. - // Seems more trouble than it's worth to block this. - prefEditor.putBoolean(prefsKey, toggle.isChecked()); - prefEditor.apply(); - - // Don't use/apply language modeling settings for languages without models. - if (associatedLexicalModel.isEmpty()) { - return; - } - - Keyboard kbInfo = KMManager.getCurrentKeyboardInfo(context); - if(kbInfo != null) { - // If the active keyboard is for this language, immediately enact the new pref setting. - String kbdLgCode = kbInfo.getLanguageID(); - if (kbdLgCode.equals(lgCode)) { - // Not only registers the model but also applies our modeling preferences. - KMManager.registerAssociatedLexicalModel(lgCode); - } - } - } - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -139,33 +99,59 @@ public void onCreate(Bundle savedInstanceState) { FilteredKeyboardsAdapter adapter = new FilteredKeyboardsAdapter(context, KeyboardPickerActivity.getInstalledDataset(context), lgCode); - // The following two layouts/toggles will need to link with these objects. + // The following radio group will need to link with these objects. Context appContext = this.getApplicationContext(); prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - boolean mayPredict = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(lgCode), true); - boolean mayCorrect = prefs.getBoolean(KMManager.getLanguageCorrectionPreferenceKey(lgCode), true); - - RelativeLayout layout = (RelativeLayout)findViewById(R.id.corrections_toggle); + int maySuggest = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey(lgCode), KMManager.KMDefault_Suggestion); + + // Radio button change listeners + RadioGroup radioGroup = (RadioGroup) findViewById(R.id.suggestion_radio_group); + RadioButton radioButton; + // Initialize radio button group + switch(maySuggest) { + case 1: + radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_1); + break; + case 2: + radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_2); + break; + case 3: + radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_3); + break; + case 0: + default: + radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_0); + } + radioGroup.clearCheck(); + radioButton.setChecked(true); - correctionsTextView = (TextView) layout.findViewById(R.id.text1); - correctionsTextView.setText(getString(R.string.enable_corrections)); - correctionsToggle = layout.findViewById(R.id.toggle); - correctionsToggle.setChecked(mayCorrect); // Link to persistent option storage! Also needs handler. - String prefsKey = KMManager.getLanguageCorrectionPreferenceKey(lgCode); - correctionsToggle.setOnClickListener(new PreferenceToggleListener(prefsKey, lgCode)); + radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, @IdRes int checkId) { + RadioButton checkedButton = (RadioButton)radioGroup.findViewById(checkId); + int index = radioGroup.indexOfChild(checkedButton); + int radioButtonID = radioGroup.getCheckedRadioButtonId(); - layout = (RelativeLayout)findViewById(R.id.predictions_toggle); + KMManager.setMaySuggest(lgCode, index); - textView = (TextView) layout.findViewById(R.id.text1); - textView.setText(getString(R.string.enable_predictions)); - SwitchCompat predictionsToggle = layout.findViewById(R.id.toggle); - predictionsToggle.setChecked(mayPredict); // Link to persistent option storage! Also needs handler. - prefsKey = KMManager.getLanguagePredictionPreferenceKey(lgCode); - predictionsToggle.setOnClickListener(new PreferenceToggleListener(prefsKey, lgCode)); + // Don't use/apply language modeling settings for languages without models. + if (associatedLexicalModel.isEmpty()) { + return; + } - overrideCorrectionsToggle(mayPredict); + Keyboard kbInfo = KMManager.getCurrentKeyboardInfo(context); + if(kbInfo != null) { + // If the active keyboard is for this language, immediately enact the new pref setting. + String kbdLgCode = kbInfo.getLanguageID(); + if (kbdLgCode.equals(lgCode)) { + // Not only registers the model but also applies our modeling preferences. + KMManager.registerAssociatedLexicalModel(lgCode); + } + } + } + }); - layout = (RelativeLayout)findViewById(R.id.model_picker); + RelativeLayout layout = (RelativeLayout)findViewById(R.id.model_picker); textView = (TextView) layout.findViewById(R.id.text1); textView.setText(getString(R.string.model_label)); diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 5e5d7241235..b0a1ab47d29 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -229,11 +229,44 @@ function deregisterModel(modelID) { keyman.removeModel(modelID); } -function enableSuggestions(model, mayPredict, mayCorrect) { +function enableSuggestions(model, mayPredict, maySuggest) { // Set the options first so that KMW's ModelManager can properly handle model enablement states // the moment we actually register the new model. - keyman.core.languageProcessor.mayPredict = mayPredict; - keyman.core.languageProcessor.mayCorrect = mayCorrect; + // Use console_debug + window.console.log('enableSuggestions(mayPredict='+mayPredict+', maySuggest='+maySuggest+')'); + if (!mayPredict) { + keyman.core.languageProcessor.mayPredict = false; + keyman.core.languageProcessor.mayCorrect = false; + // keyman.core.languageProcessor.mayAutoCorrect = false; + } else { + switch(maySuggest) { + case 1 : + // SuggestionType.PREDICTIONS_ONLY + keyman.core.languageProcessor.mayPredict = true; + keyman.core.languageProcessor.mayCorrect = false; + //keyman.core.languageProcessor.mayAutoCorrect = false; + break; + case 2 : + // SuggestionType.PREDICTIONS_WITH_CORRECTIONS + keyman.core.languageProcessor.mayPredict = true; + keyman.core.languageProcessor.mayCorrect = true; + //keyman.core.languageProcessor.mayAutoCorrect = false; + break; + case 3 : + // SuggesionType.PREDICTIONS_WITH_AUTO_CORRECT + keyman.core.languageProcessor.mayPredict = true; + keyman.core.languageProcessor.mayCorrect = true; + //keyman.core.languageProcessor.mayAutoCorrect = true; + break; + case 0 : + default : + // SuggestionType.SUGGESTIONS_DISABLED + keyman.core.languageProcessor.mayPredict = false; + keyman.core.languageProcessor.mayCorrect = false; + //keyman.core.languageProcessor.mayAutoCorrect = false; + break; + } + } registerModel(model); } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index 1e28e4af0b3..b6f98cf7661 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -165,7 +165,8 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); boolean modelPredictionPref = false; if (!KMManager.getMayPredictOverride() && KMManager.currentLexicalModel != null) { - modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true); + modelPredictionPref = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey( + KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), KMManager.KMDefault_Suggestion) > 0; } KMManager.setBannerOptions(modelPredictionPref); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index 592f4be2a15..8bafa204beb 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -192,6 +192,36 @@ public enum EnterModeType { DEFAULT, // Default ENTER action } + // Enum for whether the suggestion banner allows predictions, corrections, auto-corrections + public enum SuggestionType { + // Suggestion Disabled - No Predictions, No corrections, No auto-corrections + SUGGESTIONS_DISABLED, + + // Suggestions Enabled + PREDICTIONS_ONLY, // Predictions with no corrections + PREDICTIONS_WITH_CORRECTIONS, // Predictions with corrections + PREDICTIONS_WITH_AUTO_CORRECT; // Predictions with auto-corrections + + public static SuggestionType fromInt(int mode) { + switch (mode) { + case 0: + return SUGGESTIONS_DISABLED; + case 1: + return PREDICTIONS_ONLY; + case 2: + return PREDICTIONS_WITH_CORRECTIONS; + case 3: + return PREDICTIONS_WITH_AUTO_CORRECT; + } + return SUGGESTIONS_DISABLED; + } + + public int toInt() { + int modes[] = { 0, 1, 2, 3 }; + return modes[this.ordinal()]; + } + } + protected static InputMethodService IMService; private static boolean debugMode = false; @@ -220,6 +250,7 @@ public enum EnterModeType { public final static String predictionPrefSuffix = ".mayPredict"; public final static String correctionPrefSuffix = ".mayCorrect"; + public final static String autoCorrectionPrefSuffix = ".mayAutoCorect"; // Special override for when the keyboard may have haptic feedback when typing. // haptic feedback disabled for hardware keystrokes @@ -315,6 +346,8 @@ public enum EnterModeType { public static final int KMMinimum_LongpressDelay = 300; public static final int KMMaximum_LongpressDelay = 1500; + // Default prediction/correction setting - corresponds to SuggestionType.PREDICTIONS_WITH_CORRECTIONS + public static final int KMDefault_Suggestion = 2; // Keyman files protected static final String KMFilename_KeyboardHtml = "keyboard.html"; @@ -707,6 +740,10 @@ public static String getLanguageCorrectionPreferenceKey(String langID) { return langID + correctionPrefSuffix; } + public static String getLanguageAutoCorrectionPreferenceKey(String langID) { + return langID + autoCorrectionPrefSuffix; + } + public static void hideSystemKeyboard() { if (SystemKeyboard != null) { SystemKeyboard.hideKeyboard(); @@ -1350,6 +1387,22 @@ public static boolean getMayPredictOverride() { return mayPredictOverride; } + /** + * Maps radio button index 0..3 to SuggestionType and then store as preference + * @param languageID as String + * @param suggestType Radio button index 0 to 3 + */ + public static void setMaySuggest(String languageID, int suggestType) { + if (suggestType < 0 || suggestType > 3) { + // Invalid values go to SUGGESTIONS_DISABLED + suggestType = 0; + } + SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(KMManager.getLanguageAutoCorrectionPreferenceKey(languageID), suggestType); + editor.commit(); + } + /** * Determines if the InputType field is a numeric field * @param inputType @@ -1464,22 +1517,22 @@ public static boolean registerLexicalModel(HashMap lexicalModelI // When entering password field, mayPredict should override to false SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + int maySuggest = prefs.getInt(getLanguageAutoCorrectionPreferenceKey(languageID), KMDefault_Suggestion); boolean mayPredict = (mayPredictOverride) ? false : - prefs.getBoolean(getLanguagePredictionPreferenceKey(languageID), true); - boolean mayCorrect = prefs.getBoolean(getLanguageCorrectionPreferenceKey(languageID), true); + maySuggest > 0; RelativeLayout.LayoutParams params; if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); // Do NOT re-layout here; it'll be triggered once the banner loads. - InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); + InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %d)", model, mayPredict, maySuggest)); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); // Do NOT re-layout here; it'll be triggered once the banner loads. - SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); + SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %d)", model, mayPredict, maySuggest)); } return true; } diff --git a/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml b/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml index c015e23e614..6399987b041 100644 --- a/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml +++ b/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml @@ -60,17 +60,44 @@ android:layout_height="2dp" android:background="?attr/colorAccent" /> - - - + + + + + + + + + + + + - - - - diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 506626cd244..5c5d6f3173e 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -211,12 +211,21 @@ Keyboard deleted - + Enable corrections - + Enable predictions + + Disable suggestions + + Predictions only + + Predictions with corrections + + Predictions with auto-corrections + Dictionary From 9096105231840b1a79615163c2e3a7fb5e362941 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 19 Sep 2024 13:39:02 +0700 Subject: [PATCH 02/21] chore(android): Disable debug log --- android/KMEA/app/src/main/assets/android-host.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index b0a1ab47d29..302b381a368 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -233,7 +233,7 @@ function enableSuggestions(model, mayPredict, maySuggest) { // Set the options first so that KMW's ModelManager can properly handle model enablement states // the moment we actually register the new model. // Use console_debug - window.console.log('enableSuggestions(mayPredict='+mayPredict+', maySuggest='+maySuggest+')'); + console_debug('enableSuggestions(mayPredict='+mayPredict+', maySuggest='+maySuggest+')'); if (!mayPredict) { keyman.core.languageProcessor.mayPredict = false; keyman.core.languageProcessor.mayCorrect = false; From 25ea6a74dcf3581ad7f97c830f37d95542d271be Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 24 Sep 2024 08:24:47 -0700 Subject: [PATCH 03/21] fix(developer): use richedit in debug memo to support Egyptian cartouches RichEdit allows text selection one character past end-of-string, so we need to cater for that as well in passing text ranges to the character grid. Fixes: #12454 --- .../components/KeymanDeveloperDebuggerMemo.pas | 18 ++++++++++-------- ...eveloper.UI.Debug.UfrmLdmlKeyboardDebug.pas | 14 ++++++++++++-- developer/src/tike/child/UfrmDebug.pas | 13 ++++++++++++- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas b/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas index fa062e68628..afd6f926c1a 100644 --- a/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas +++ b/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas @@ -1,18 +1,18 @@ (* Name: KeymanDeveloperDebuggerMemo Copyright: Copyright (C) 2003-2017 SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 8 Jun 2012 Modified Date: 8 Jun 2012 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 08 Jun 2012 - mcdurdin - I3323 - V9.0 - Extract debug-related code TPlus-Memo into subclass *) unit KeymanDeveloperDebuggerMemo; // I3323 @@ -24,6 +24,7 @@ interface Winapi.Messages, Winapi.Windows, Vcl.Controls, + Vcl.ComCtrls, Vcl.StdCtrls; type @@ -34,7 +35,7 @@ TMemoSelection = record Anchor: Integer; end; - TKeymanDeveloperDebuggerMemo = class(TMemo) + TKeymanDeveloperDebuggerMemo = class(TRichEdit) private FOnMessage: TKeymanDeveloperDebuggerMessageEvent; FAllowUnicodeInput: Boolean; @@ -77,6 +78,7 @@ constructor TKeymanDeveloperDebuggerMemo.Create(AOwner: TComponent); begin FAllowUnicodeInput := True; inherited Create(AOwner); + PlainText := True; end; procedure TKeymanDeveloperDebuggerMemo.CreateHandle; diff --git a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas index a00fdb9aeae..3c8386219f3 100644 --- a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas +++ b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas @@ -831,12 +831,22 @@ procedure TfrmLdmlKeyboardDebug.memoSelMove(Sender: TObject); end; procedure TfrmLdmlKeyboardDebug.UpdateCharacterGrid; // I4808 +var + start, len: Integer; begin if csDestroying in ComponentState then Exit; - TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, memo.SelStart, - memo.SelLength, memo.Selection.Anchor, True); + start := memo.SelStart; + len := memo.SelLength; + if start + len > Length(memo.Text) then + begin + // RichEdit has a virtual final character, which is selected when + // pressing Ctrl+A, etc. + len := Length(memo.Text) - start; + end; + TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, start, len + memo.Selection.Anchor, True); TCharacterGridRenderer.Size(sgChars, memo.Font); end; diff --git a/developer/src/tike/child/UfrmDebug.pas b/developer/src/tike/child/UfrmDebug.pas index 66116034dc6..fba5f96f735 100644 --- a/developer/src/tike/child/UfrmDebug.pas +++ b/developer/src/tike/child/UfrmDebug.pas @@ -1354,11 +1354,22 @@ procedure TfrmDebug.memoSelMove(Sender: TObject); end; procedure TfrmDebug.UpdateCharacterGrid; // I4808 +var + start, len: Integer; begin if csDestroying in ComponentState then Exit; - TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, memo.SelStart, memo.SelLength, memo.Selection.Anchor); + start := memo.SelStart; + len := memo.SelLength; + if start + len > Length(memo.Text) then + begin + // RichEdit has a virtual final character, which is selected when + // pressing Ctrl+A, etc. + len := Length(memo.Text) - start; + end; + + TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, start, len, memo.Selection.Anchor); TCharacterGridRenderer.Size(sgChars, memo.Font); end; From a6f9082ae143ef1ec316c457b65d82353d6bc569 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Fri, 27 Sep 2024 10:46:14 +0700 Subject: [PATCH 04/21] Apply suggestions from code review Co-authored-by: Marc Durdin --- .../KMEA/app/src/main/assets/android-host.js | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 302b381a368..b470896386b 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -229,42 +229,23 @@ function deregisterModel(modelID) { keyman.removeModel(modelID); } -function enableSuggestions(model, mayPredict, maySuggest) { +function enableSuggestions(model, suggestionType) { // Set the options first so that KMW's ModelManager can properly handle model enablement states // the moment we actually register the new model. // Use console_debug console_debug('enableSuggestions(mayPredict='+mayPredict+', maySuggest='+maySuggest+')'); - if (!mayPredict) { - keyman.core.languageProcessor.mayPredict = false; - keyman.core.languageProcessor.mayCorrect = false; - // keyman.core.languageProcessor.mayAutoCorrect = false; - } else { - switch(maySuggest) { - case 1 : - // SuggestionType.PREDICTIONS_ONLY - keyman.core.languageProcessor.mayPredict = true; - keyman.core.languageProcessor.mayCorrect = false; - //keyman.core.languageProcessor.mayAutoCorrect = false; - break; - case 2 : - // SuggestionType.PREDICTIONS_WITH_CORRECTIONS - keyman.core.languageProcessor.mayPredict = true; - keyman.core.languageProcessor.mayCorrect = true; - //keyman.core.languageProcessor.mayAutoCorrect = false; - break; - case 3 : - // SuggesionType.PREDICTIONS_WITH_AUTO_CORRECT - keyman.core.languageProcessor.mayPredict = true; - keyman.core.languageProcessor.mayCorrect = true; - //keyman.core.languageProcessor.mayAutoCorrect = true; - break; - case 0 : - default : - // SuggestionType.SUGGESTIONS_DISABLED - keyman.core.languageProcessor.mayPredict = false; - keyman.core.languageProcessor.mayCorrect = false; - //keyman.core.languageProcessor.mayAutoCorrect = false; - break; + const suggestionSettings = [ + // mayPredict, mayCorrect, mayAutoCorrect + [false, false, false], // 0 = SuggestionType.SUGGESTIONS_DISABLED + [true, false, false], // 1 = SuggestionType.PREDICTIONS_ONLY + [true, true, false], // 2 = SuggestionType.PREDICTIONS_WITH_CORRECTIONS + [true, true, true], // 3 = SuggestionType.PREDICTIONS_WITH_AUTO_CORRECT + ]; + const t = suggestionSettings[suggestionType] + ? suggestionSettings[suggestionType] : suggestionSettings[0]; + keyman.core.languageProcessor.mayPredict = t[0]; + keyman.core.languageProcessor.maySuggest = t[1]; + // keyman.core.languageProcessor.mayAutoCorrect = t[2]; } } From 6eba88ce5fef7a6d944c70240d92df0605f5425e Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Fri, 27 Sep 2024 16:37:10 +0700 Subject: [PATCH 05/21] fix(android): Use SuggestionType methods instead of magic numbers --- .../com/keyman/android/SystemKeyboard.java | 3 +- .../kmapro/LanguageSettingsActivity.java | 29 ++++++------------- .../KMEA/app/src/main/assets/android-host.js | 2 +- .../engine/KMKeyboardWebViewClient.java | 4 ++- .../java/com/keyman/engine/KMManager.java | 23 ++++++--------- 5 files changed, 24 insertions(+), 37 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 23053d21d48..e9d812e6d94 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -11,6 +11,7 @@ import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; import com.keyman.engine.KMHardwareKeyboardInterpreter; +import com.keyman.engine.KMManager.SuggestionType; import com.keyman.engine.KeyboardEventHandler.OnKeyboardEventListener; import com.keyman.engine.R; import com.keyman.engine.data.Keyboard; @@ -171,7 +172,7 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); int maySuggest = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey(langId), KMManager.KMDefault_Suggestion); // Enable banner if maySuggest is not SuggestionType.SUGGESTIONS_DISABLED (0) - KMManager.setBannerOptions(maySuggest > 0); + KMManager.setBannerOptions(maySuggest > SuggestionType.SUGGESTIONS_DISABLED.toInt()); } else { KMManager.setBannerOptions(false); } diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java index 61972d2fc06..977489f40a9 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java @@ -104,25 +104,16 @@ public void onCreate(Bundle savedInstanceState) { prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); int maySuggest = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey(lgCode), KMManager.KMDefault_Suggestion); - // Radio button change listeners + // Initialize Radio button group change listeners RadioGroup radioGroup = (RadioGroup) findViewById(R.id.suggestion_radio_group); - RadioButton radioButton; - // Initialize radio button group - switch(maySuggest) { - case 1: - radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_1); - break; - case 2: - radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_2); - break; - case 3: - radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_3); - break; - case 0: - default: - radioButton = (RadioButton) radioGroup.findViewById(R.id.suggestion_radio_0); - } radioGroup.clearCheck(); + + int[] RadioButtonArray = { + R.id.suggestion_radio_0, + R.id.suggestion_radio_1, + R.id.suggestion_radio_2, + R.id.suggestion_radio_3}; + RadioButton radioButton = (RadioButton)radioGroup.findViewById(RadioButtonArray[maySuggest]); radioButton.setChecked(true); radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { @@ -130,9 +121,7 @@ public void onCreate(Bundle savedInstanceState) { public void onCheckedChanged(RadioGroup group, @IdRes int checkId) { RadioButton checkedButton = (RadioButton)radioGroup.findViewById(checkId); int index = radioGroup.indexOfChild(checkedButton); - int radioButtonID = radioGroup.getCheckedRadioButtonId(); - - KMManager.setMaySuggest(lgCode, index); + KMManager.setMaySuggest(lgCode, KMManager.SuggestionType.fromInt(index)); // Don't use/apply language modeling settings for languages without models. if (associatedLexicalModel.isEmpty()) { diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index b470896386b..23c85ca6d59 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -233,7 +233,7 @@ function enableSuggestions(model, suggestionType) { // Set the options first so that KMW's ModelManager can properly handle model enablement states // the moment we actually register the new model. // Use console_debug - console_debug('enableSuggestions(mayPredict='+mayPredict+', maySuggest='+maySuggest+')'); + console_debug('enableSuggestions(model, maySuggest='+suggestionType+')'); const suggestionSettings = [ // mayPredict, mayCorrect, mayAutoCorrect [false, false, false], // 0 = SuggestionType.SUGGESTIONS_DISABLED diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index b6f98cf7661..620ae252f41 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -16,6 +16,7 @@ import com.keyman.engine.KeyboardEventHandler.EventType; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; +import com.keyman.engine.KMManager.SuggestionType; import com.keyman.engine.util.KMLog; import com.keyman.engine.data.Keyboard; @@ -166,7 +167,8 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { boolean modelPredictionPref = false; if (!KMManager.getMayPredictOverride() && KMManager.currentLexicalModel != null) { modelPredictionPref = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey( - KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), KMManager.KMDefault_Suggestion) > 0; + KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), KMManager.KMDefault_Suggestion) + > SuggestionType.SUGGESTIONS_DISABLED.toInt(); } KMManager.setBannerOptions(modelPredictionPref); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index 8bafa204beb..e434f3be7ae 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -1388,18 +1388,14 @@ public static boolean getMayPredictOverride() { } /** - * Maps radio button index 0..3 to SuggestionType and then store as preference + * Store SuggestionType as int in preference * @param languageID as String - * @param suggestType Radio button index 0 to 3 + * @param suggestType SuggestionType */ - public static void setMaySuggest(String languageID, int suggestType) { - if (suggestType < 0 || suggestType > 3) { - // Invalid values go to SUGGESTIONS_DISABLED - suggestType = 0; - } + public static void setMaySuggest(String languageID, SuggestionType suggestType) { SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(KMManager.getLanguageAutoCorrectionPreferenceKey(languageID), suggestType); + editor.putInt(KMManager.getLanguageAutoCorrectionPreferenceKey(languageID), suggestType.toInt()); editor.commit(); } @@ -1515,24 +1511,23 @@ public static boolean registerLexicalModel(HashMap lexicalModelI model = model.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex. model = model.replaceAll("\"", "'"); - // When entering password field, mayPredict should override to false + // When entering password field, maySuggest should override to disabled SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - int maySuggest = prefs.getInt(getLanguageAutoCorrectionPreferenceKey(languageID), KMDefault_Suggestion); - boolean mayPredict = (mayPredictOverride) ? false : - maySuggest > 0; + int maySuggest = mayPredictOverride ? SuggestionType.SUGGESTIONS_DISABLED.toInt() : + prefs.getInt(getLanguageAutoCorrectionPreferenceKey(languageID), KMDefault_Suggestion); RelativeLayout.LayoutParams params; if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); // Do NOT re-layout here; it'll be triggered once the banner loads. - InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %d)", model, mayPredict, maySuggest)); + InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %d)", model, maySuggest)); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); // Do NOT re-layout here; it'll be triggered once the banner loads. - SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %d)", model, mayPredict, maySuggest)); + SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %d)", model, maySuggest)); } return true; } From 39f2d3e3800f637ebf8a00bbe21ec1484d1e8ff2 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 30 Sep 2024 09:12:05 +0700 Subject: [PATCH 06/21] fix(android/engine): Remove extraneous braces --- android/KMEA/app/src/main/assets/android-host.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 23c85ca6d59..21f783ffc55 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -246,8 +246,6 @@ function enableSuggestions(model, suggestionType) { keyman.core.languageProcessor.mayPredict = t[0]; keyman.core.languageProcessor.maySuggest = t[1]; // keyman.core.languageProcessor.mayAutoCorrect = t[2]; - } - } registerModel(model); } From 5c6e7c565b40bededfa72890b964a136b5558d1f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 7 Oct 2024 14:49:43 +0700 Subject: [PATCH 07/21] chore(common): fix links in minimum-versions.md --- docs/minimum-versions.md | 16 ++++++++-------- docs/minimum-versions.md.in | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/minimum-versions.md b/docs/minimum-versions.md index c0b034dde0a..919f4520daf 100644 --- a/docs/minimum-versions.md +++ b/docs/minimum-versions.md @@ -10,33 +10,33 @@ Target Operating System and Platform Versions ### Keyman for Windows -Helpfile: [os.md](../../windows/docs/help/common/os.md) +Helpfile: [os.md](../windows/docs/help/common/os.md) ### Keyman for macOS -Helpfile: [requirements.md](../../mac/docs/help/about/requirements.md) +Helpfile: [requirements.md](../mac/docs/help/about/requirements.md) ### Keyman for Linux -Helpfile: [common/index.md](../../linux/docs/help/common/index.md#q-what-linux-distros-will-keyman-work-with) +Helpfile: [common/index.md](../linux/docs/help/common/index.md#q-what-linux-distros-will-keyman-work-with) ### Keyman iPhone and iPad -Helpfile: [system-requirements.md](../../ios/docs/help/about/system-requirements.md) +Helpfile: [system-requirements.md](../ios/docs/help/about/system-requirements.md) ### Keyman for Android -Helpfile: [system-requirements.md](../../android/docs/help/about/system-requirements.md) +Helpfile: [system-requirements.md](../android/docs/help/about/system-requirements.md) ---- ## Product Build Documentation -[linux-ubuntu.md](../../docs/build/linux-ubuntu.md) +[linux-ubuntu.md](../docs/build/linux-ubuntu.md) -[macos.md](../../docs/build/macos.md) +[macos.md](../docs/build/macos.md) -[windows.md](../../docs/build/windows.md) +[windows.md](../docs/build/windows.md) ## Keyman Engine Documentation diff --git a/docs/minimum-versions.md.in b/docs/minimum-versions.md.in index 35fa2adde30..ffe508e12f0 100644 --- a/docs/minimum-versions.md.in +++ b/docs/minimum-versions.md.in @@ -10,33 +10,33 @@ Target Operating System and Platform Versions ### Keyman for Windows -Helpfile: [os.md](../../windows/docs/help/common/os.md) +Helpfile: [os.md](../windows/docs/help/common/os.md) ### Keyman for macOS -Helpfile: [requirements.md](../../mac/docs/help/about/requirements.md) +Helpfile: [requirements.md](../mac/docs/help/about/requirements.md) ### Keyman for Linux -Helpfile: [common/index.md](../../linux/docs/help/common/index.md#q-what-linux-distros-will-keyman-work-with) +Helpfile: [common/index.md](../linux/docs/help/common/index.md#q-what-linux-distros-will-keyman-work-with) ### Keyman iPhone and iPad -Helpfile: [system-requirements.md](../../ios/docs/help/about/system-requirements.md) +Helpfile: [system-requirements.md](../ios/docs/help/about/system-requirements.md) ### Keyman for Android -Helpfile: [system-requirements.md](../../android/docs/help/about/system-requirements.md) +Helpfile: [system-requirements.md](../android/docs/help/about/system-requirements.md) ---- ## Product Build Documentation -[linux-ubuntu.md](../../docs/build/linux-ubuntu.md) +[linux-ubuntu.md](../docs/build/linux-ubuntu.md) -[macos.md](../../docs/build/macos.md) +[macos.md](../docs/build/macos.md) -[windows.md](../../docs/build/windows.md) +[windows.md](../docs/build/windows.md) ## Keyman Engine Documentation From e208d56419938c7b0d7c5d3ed3a21d067bff0239 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 8 Oct 2024 14:21:22 +0700 Subject: [PATCH 08/21] fix(developer): use RichEdit 4.1 for debugger memos Adds support for using RichEdit 4.1, and sets the richedit control into the correct mode for rendering arbitrary Unicode correctly. Removes some unnecessary per-character refresh for improved performance. Fixes: #12454 --- .../Keyman.Developer.UI.RichEdit41.pas | 150 ++++++++++++++++++ .../KeymanDeveloperDebuggerMemo.pas | 33 +++- ...veloper.UI.Debug.UfrmLdmlKeyboardDebug.pas | 37 +++-- developer/src/tike/child/UfrmDebug.pas | 15 +- developer/src/tike/tike.dpr | 3 +- developer/src/tike/tike.dproj | 1 + 6 files changed, 213 insertions(+), 26 deletions(-) create mode 100644 developer/src/common/delphi/components/Keyman.Developer.UI.RichEdit41.pas diff --git a/developer/src/common/delphi/components/Keyman.Developer.UI.RichEdit41.pas b/developer/src/common/delphi/components/Keyman.Developer.UI.RichEdit41.pas new file mode 100644 index 00000000000..086c9126769 --- /dev/null +++ b/developer/src/common/delphi/components/Keyman.Developer.UI.RichEdit41.pas @@ -0,0 +1,150 @@ +unit Keyman.Developer.UI.RichEdit41; + +interface + +uses + Vcl.Controls, + Vcl.ComCtrls, + Vcl.Themes, + Winapi.Windows; + +type + TRichEdit41 = class(TCustomRichEdit) + strict private + class constructor Create; + class destructor Destroy; + protected + procedure CreateParams(var Params: TCreateParams); override; + published + property Align; + property Alignment; + property Anchors; + property BevelEdges; + property BevelInner; + property BevelOuter; + property BevelKind default bkNone; + property BevelWidth; + property BiDiMode; + property BorderStyle; + property BorderWidth; + property Color; + property Ctl3D; + property DragCursor; + property DragKind; + property DragMode; + property Enabled; + property Font; + property HideSelection; + property HideScrollBars; + property ImeMode; + property ImeName; + property Constraints; + property Lines; + property MaxLength; + property ParentBiDiMode; + property ParentColor; + property ParentCtl3D; + property ParentFont; + property ParentShowHint; + property PlainText; + property PopupMenu; + property ReadOnly; + property ScrollBars; + property ShowHint; + property TabOrder; + property TabStop default True; + property Touch; + property Visible; + property WantTabs; + property WantReturns; + property WordWrap; + property StyleElements; + property Zoom; + property OnChange; + property OnClick; + property OnContextPopup; + property OnDblClick; + property OnDragDrop; + property OnDragOver; + property OnEndDock; + property OnEndDrag; + property OnEnter; + property OnExit; + property OnGesture; + property OnKeyDown; + property OnKeyPress; + property OnKeyUp; + property OnMouseActivate; + property OnMouseDown; + property OnMouseEnter; + property OnMouseLeave; + property OnMouseMove; + property OnMouseUp; + property OnMouseWheel; + property OnMouseWheelDown; + property OnMouseWheelUp; + property OnProtectChange; + property OnResizeRequest; + property OnSaveClipboard; + property OnSelectionChange; + property OnStartDock; + property OnStartDrag; + end; + +procedure Register; + +implementation + +uses + System.Classes, + Winapi.RichEdit; + +{ TRichEdit41 } + +class constructor TRichEdit41.Create; +begin + TCustomStyleEngine.RegisterStyleHook(TRichEdit41, TRichEditStyleHook); +end; + +class destructor TRichEdit41.Destroy; +begin + TCustomStyleEngine.UnRegisterStyleHook(TRichEdit41, TRichEditStyleHook); +end; + +var + FRichEditModule: THandle = 0; + +procedure TRichEdit41.CreateParams(var Params: TCreateParams); +const + HideScrollBars: array[Boolean] of DWORD = (ES_DISABLENOSCROLL, 0); + HideSelections: array[Boolean] of DWORD = (ES_NOHIDESEL, 0); + RichEditClassName = 'RICHEDIT50W'; + RichEditModuleName = 'MSFTEDIT.DLL'; +begin + if FRichEditModule = 0 then + begin + FRichEditModule := LoadLibrary(RichEditModuleName); + if FRichEditModule <= HINSTANCE_ERROR then FRichEditModule := 0; + end; + + inherited CreateParams(Params); + + CreateSubClass(Params, RichEditClassName); + + with Params do + begin + Style := Style or HideScrollBars[Self.HideScrollBars] or + HideSelections[HideSelection]; + end; +end; + +procedure Register; +begin + RegisterComponents('Keyman', [TRichEdit41]); +end; + +initialization +finalization + if FRichEditModule <> 0 then FreeLibrary(FRichEditModule); +end. + diff --git a/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas b/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas index afd6f926c1a..3cd9397b212 100644 --- a/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas +++ b/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas @@ -22,10 +22,13 @@ interface uses System.Classes, Winapi.Messages, + Winapi.RichEdit, Winapi.Windows, Vcl.Controls, Vcl.ComCtrls, - Vcl.StdCtrls; + Vcl.StdCtrls, + + Keyman.Developer.UI.RichEdit41; type TKeymanDeveloperDebuggerMessageEvent = procedure(Sender: TObject; var Message: TMessage; var Handled: Boolean) of object; @@ -35,10 +38,11 @@ TMemoSelection = record Anchor: Integer; end; - TKeymanDeveloperDebuggerMemo = class(TRichEdit) + TKeymanDeveloperDebuggerMemo = class(TRichEdit41) private FOnMessage: TKeymanDeveloperDebuggerMessageEvent; FAllowUnicodeInput: Boolean; + FSelectionChanging: Boolean; FIsDebugging: Boolean; procedure SetAllowUnicode(const Value: Boolean); function GetSelection: TMemoSelection; @@ -53,6 +57,7 @@ TKeymanDeveloperDebuggerMemo = class(TRichEdit) property AllowUnicode: Boolean read FAllowUnicodeInput write SetAllowUnicode default True; property OnMessage: TKeymanDeveloperDebuggerMessageEvent read FOnMessage write FOnMessage; property Selection: TMemoSelection read GetSelection write SetSelection; + property SelectionChanging: Boolean read FSelectionChanging; property IsDebugging: Boolean read FIsDebugging write FIsDebugging; end; @@ -82,11 +87,17 @@ constructor TKeymanDeveloperDebuggerMemo.Create(AOwner: TComponent); end; procedure TKeymanDeveloperDebuggerMemo.CreateHandle; +const + TO_ADVANCEDTYPOGRAPHY = 1; begin inherited; if FAllowUnicodeInput then SetWindowLongW(Handle, GWL_WNDPROC, GetWindowLong(Handle, GWL_WNDPROC)) else SetWindowLongA(Handle, GWL_WNDPROC, GetWindowLong(Handle, GWL_WNDPROC)); + SendMessage(Handle, WM_SETTEXT, 0, NativeUInt(PChar(''))); + SendMessage(Handle, EM_SETTEXTMODE, TM_PLAINTEXT or TM_MULTICODEPAGE, 0); + SendMessage(Handle, EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY); + SendMessage(Handle, EM_SETLANGOPTIONS, 0, $0040 {IMF_NOIMPLICITLANG} or $0200 {IMF_NOKBDLIDFIXUP}); end; function TKeymanDeveloperDebuggerMemo.GetSelection: TMemoSelection; @@ -95,9 +106,21 @@ function TKeymanDeveloperDebuggerMemo.GetSelection: TMemoSelection; // it out with this kludge. I am not aware of side effects from this // at this time. SendMessage(Handle, EM_GETSEL, NativeUInt(@Result.Start), NativeUInt(@Result.Finish)); - SendMessage(Handle, EM_SETSEL, -1, 0); - SendMessage(Handle, EM_GETSEL, NativeUInt(@Result.Anchor), 0); - SetSelection(Result); + if Result.Start <> Result.Finish then + begin + // We only need to play the selection test game if there is a non-zero + // selection length + FSelectionChanging := True; + Lines.BeginUpdate; + try + SendMessage(Handle, EM_SETSEL, -1, 0); + SendMessage(Handle, EM_GETSEL, NativeUInt(@Result.Anchor), 0); + SetSelection(Result); + finally + Lines.EndUpdate; + FSelectionChanging := False; + end; + end; end; procedure TKeymanDeveloperDebuggerMemo.SetAllowUnicode(const Value: Boolean); diff --git a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas index 3c8386219f3..17125230266 100644 --- a/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas +++ b/developer/src/tike/child/Keyman.Developer.UI.Debug.UfrmLdmlKeyboardDebug.pas @@ -24,6 +24,7 @@ interface Vcl.Menus, Vcl.StdCtrls, Winapi.Messages, + Winapi.RichEdit, Winapi.Windows, CaptionPanel, @@ -524,6 +525,11 @@ TMemoSelectionState = record end; end; +type + TSetTextEx = record + flags: DWord; + codepage: UINT; + end; var actions: pkm_core_actions; context_items: pkm_core_context_item; @@ -535,6 +541,8 @@ TMemoSelectionState = record context_items_length: Integer; state: TMemoSelectionState; Adjustment: Integer; + ste: TSetTextEx; + str: string; begin FIgnoreNextUIKey := True; @@ -618,11 +626,20 @@ TMemoSelectionState = record // Merge left of context, context, and right of context and update memo // insertion point position - memo.Text := lhs + output + rhs; - selection.Start := lhs.Length + output.Length; - selection.Finish := selection.Start; - selection.Anchor := selection.Start; - memo.Selection := selection; + memo.Lines.BeginUpdate; + try + // Setting text directly for improved performance + ste.flags := $01 {ST_KEEPUNDO} or $04 {ST_NEWCHARS} or $08 {ST_UNICODE} or $20 {ST_PLAINTEXTONLY}; + ste.codepage := 1200 {Unicode}; + str := lhs + output + rhs; + SendMessage(memo.Handle, (WM_USER + 97) {EM_SETTEXTEX}, NativeUint(@ste), NativeUInt(PChar(str))); + selection.Start := lhs.Length + output.Length; + selection.Finish := selection.Start; + selection.Anchor := selection.Start; + memo.Selection := selection; + finally + memo.Lines.EndUpdate; + end; end; // actions.persist_options are not currently supported by LDML @@ -639,12 +656,6 @@ TMemoSelectionState = record finally km_core_context_items_dispose(context_items); - - UpdateCharacterGrid; - - // We want to refresh the memo and character grid for rapid typing - memo.Update; - sgChars.Update; end; end; @@ -824,7 +835,7 @@ procedure TfrmLdmlKeyboardDebug.memoSelMove(Sender: TObject); frmKeymanDeveloper.barStatus.Panels[0].Text := 'Debugger Active'; end; - if not memo.ReadOnly then + if not memo.ReadOnly and not memo.SelectionChanging then begin UpdateCharacterGrid; // I4808 end; @@ -845,7 +856,7 @@ procedure TfrmLdmlKeyboardDebug.UpdateCharacterGrid; // I4808 // pressing Ctrl+A, etc. len := Length(memo.Text) - start; end; - TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, start, len + TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, start, len, memo.Selection.Anchor, True); TCharacterGridRenderer.Size(sgChars, memo.Font); end; diff --git a/developer/src/tike/child/UfrmDebug.pas b/developer/src/tike/child/UfrmDebug.pas index fba5f96f735..d322d406a7e 100644 --- a/developer/src/tike/child/UfrmDebug.pas +++ b/developer/src/tike/child/UfrmDebug.pas @@ -583,10 +583,6 @@ procedure TfrmDebug.Run; finally FRunning := False; EnableUI; - UpdateCharacterGrid; - // We want to refresh the memo and character grid for rapid typing - memo.Update; - sgChars.Update; end; if UIStatus <> duiTest then @@ -802,8 +798,13 @@ TMemoSelectionState = record Assert(False, AssertionMessage); // Unrecognised backspace type end; - memo.Text := Copy(memo.Text, 1, m) + Copy(memo.Text, n+1, MaxInt); - memo.SelStart := m; + memo.Lines.BeginUpdate; + try + memo.Text := Copy(memo.Text, 1, m) + Copy(memo.Text, n+1, MaxInt); + memo.SelStart := m; + finally + memo.Lines.EndUpdate; + end; RealignMemoSelectionState(state); end; @@ -1346,7 +1347,7 @@ procedure TfrmDebug.memoSelMove(Sender: TObject); frmKeymanDeveloper.barStatus.Panels[0].Text := 'Debugger Active'; end; - if not memo.ReadOnly then + if not memo.ReadOnly and not memo.SelectionChanging then begin FSavedSelection := memo.Selection; UpdateCharacterGrid; // I4808 diff --git a/developer/src/tike/tike.dpr b/developer/src/tike/tike.dpr index d3a1935315c..e686949fc0b 100644 --- a/developer/src/tike/tike.dpr +++ b/developer/src/tike/tike.dpr @@ -299,7 +299,8 @@ uses Keyman.Developer.System.ProjectOwningFile in 'main\Keyman.Developer.System.ProjectOwningFile.pas', Keyman.Developer.System.Main in 'main\Keyman.Developer.System.Main.pas', Keyman.Developer.System.LaunchProjects in 'main\Keyman.Developer.System.LaunchProjects.pas', - Keyman.System.Debug.DebugUtils in 'debug\Keyman.System.Debug.DebugUtils.pas'; + Keyman.System.Debug.DebugUtils in 'debug\Keyman.System.Debug.DebugUtils.pas', + Keyman.Developer.UI.RichEdit41 in '..\common\delphi\components\Keyman.Developer.UI.RichEdit41.pas'; {$R *.RES} {$R ICONS.RES} diff --git a/developer/src/tike/tike.dproj b/developer/src/tike/tike.dproj index b343fc80375..2a626a30acc 100644 --- a/developer/src/tike/tike.dproj +++ b/developer/src/tike/tike.dproj @@ -584,6 +584,7 @@ + Cfg_2 From 3b9c6ef2e4e8d264a3f8b47aab97f3518d662f56 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 8 Oct 2024 18:30:22 +0200 Subject: [PATCH 09/21] refactor(web): move `KeyboardObject` type to `common/web/types` Fixes: #11612 --- common/web/types/build.sh | 23 +- common/web/types/src/keyboard-object.ts | 198 ++++++++++++++++++ common/web/types/src/main.ts | 3 + .../web/types}/src/outputTarget.interface.ts | 3 + web/src/app/browser/src/keymanEngine.ts | 3 +- .../prediction/languageProcessor.interface.ts | 3 +- .../src/prediction/predictionContext.ts | 3 +- .../engine/js-processor/src/outputTarget.ts | 3 +- web/src/engine/keyboard/src/defaultRules.ts | 3 +- web/src/engine/keyboard/src/index.ts | 3 +- .../keyboard/src/keyboards/defaultLayouts.ts | 29 +-- .../engine/keyboard/src/keyboards/keyboard.ts | 169 +-------------- web/src/engine/main/src/keyboardInterface.ts | 2 +- 13 files changed, 235 insertions(+), 210 deletions(-) create mode 100644 common/web/types/src/keyboard-object.ts rename {web/src/engine/keyboard => common/web/types}/src/outputTarget.interface.ts (98%) diff --git a/common/web/types/build.sh b/common/web/types/build.sh index 7141bc10720..0d7abba7e98 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -82,9 +82,26 @@ function do_configure() { function do_test() { eslint . tsc --build test - readonly C8_THRESHOLD=50 - c8 -skip-full --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha "${builder_extra_params[@]}" - builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal." + + readonly C8_THRESHOLD=60 + # Exclude files from coverage analysis that only define types: + exclude=(\ + src/keyboard-object.ts \ + src/lexical-model-types.ts \ + src/outputTarget.interface.ts \ + src/*.d.ts \ + src/main.ts \ + src/schemas/* \ + src/schema-validators.ts \ + src/schemas.ts \ + ) + # shellcheck disable=SC2068 + c8 --skip-full --reporter=lcov --reporter=text --lines "${C8_THRESHOLD}" \ + --statements "${C8_THRESHOLD}" --branches "${C8_THRESHOLD}" \ + --functions "${C8_THRESHOLD}" ${exclude[@]/#/--exclude } \ + mocha "${builder_extra_params[@]}" + + builder_echo warning "Coverage thresholds are currently ${C8_THRESHOLD}%, which is lower than ideal." builder_echo warning "Please increase threshold in build.sh as test coverage improves." } diff --git a/common/web/types/src/keyboard-object.ts b/common/web/types/src/keyboard-object.ts new file mode 100644 index 00000000000..dd6f2446729 --- /dev/null +++ b/common/web/types/src/keyboard-object.ts @@ -0,0 +1,198 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { OutputTarget } from './outputTarget.interface.js'; +import { TouchLayoutPlatform as LayoutFormFactorSpec } from './keyman-touch-layout/keyman-touch-layout-file.js'; + +export type ComplexKeyboardStore = (string | { t: 'd', d: number } | { ['t']: 'b' })[]; + +type KeyEvent = {}; + +/** + * Stores preprocessed properties of a keyboard for quick retrieval later. + */ +export class CacheTag { + stores: { [storeName: string]: ComplexKeyboardStore }; + + constructor() { + this.stores = {}; + } +} + +export interface EncodedVisualKeyboard { + /** Represents CSS font styling to use for VisualKeyboard text */ + F: string; + /** Should there be a 102nd key? */ + K102?: boolean, + /** + * Keyboard Layer Specification: an object-based map of layer name to the keycaps for its + * 65 keys. The 65 keys are ordered from left to right, then top to bottom. + * + * The key ID corresponding to each index of the array is specified within `Codes.dfltCodes`. + * Entries corresponding to `K_*` in `Codes.dfltCodes` are reserved for future use. + */ + KLS?: { [layerName: string]: string[] }, + /** + * @deprecated + * The older form for data in KLS - defines keycaps for 'default' keys, then 'shift' keys, + * in a single concatenated array. + */ + BK?: string[]; +} + +export type LayoutSpec = { + "desktop"?: LayoutFormFactorSpec, + "phone"?: LayoutFormFactorSpec, + "tablet"?: LayoutFormFactorSpec +} + +export type KeyboardObject = { + /** + * Used internally by Keyman Engine for Web to hold preprocessed stores. + */ + _kmw?: CacheTag; + + /** + * group-start: the function triggering processing for the keyboard's + * "Unicode" start group, corresponding to `begin Unicode > use(_____)` in + * Keyman keyboard language. + * @param outputTarget The context to which the keystroke applies + * @param keystroke The full, pre-processed keystroke triggering + * keyboard-rule application. + */ + gs(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; + + /** + * group-newcontext: the function triggering processing for the keyboard's + * "NewContext" start group, corresponding to `begin NewContext > use(_____)` + * in Keyman keyboard language. + * @param outputTarget The new context to be used with future keystrokes + * @param keystroke A 'null' `KeyEvent` providing current modifier + state information. + */ + gn?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; + + /** + * group-postkeystroke: the function triggering processing for the keyboard's + * "PostKeystroke" start group, corresponding to `begin PostKeystroke > + * use(_____)` in Keyman keyboard language. + * @param outputTarget The context altered by a recent keystroke. As a + * precondition, all changes due to `gs` / `begin Unicode` should already be + * applied. + * @param keystroke A 'null' `KeyEvent` providing current modifier + state information. + */ + gpk?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; + + /** + * Keyboard ID: the uniquely-identifying name for this keyboard. Includes the standard + * `Keyboard_` prefix. May be 'namespaced' with a prefix corresponding to a package name + * within app/webview. + */ + KI: string; + /** + * Keyboard Name: the human-readable name of the keyboard. + */ + KN: string; + /** + * Encoded data usable to construct a desktop/hardware-oriented on-screen keyboard. + */ + KV: EncodedVisualKeyboard; + /** + * Keyboard Language Code: set within select keyboards. + * + * Currently, it's only used to determine the need for CJK-picker support. Is missing + * in most compiled keyboards. + */ + KLC?: string; + /** + * @deprecated + * Keyboard Language Code: set within select keyboards. + * + * Currently, it's only used to determine the need for CJK-picker support. + * Is (probably) an older name of KLC with the identical purpose. Is missing + * in most compiled keyboards. + */ + LanguageCode?: string; + /** + * Keyboard CSS: provides the definition for custom keyboard style sheets + */ + KCSS?: string; + /** + * Keyboard is RTL: a simple flag noting if the keyboard's script is RTL. + */ + KRTL?: boolean; + /** + * Keyboard Modifier BitMask: a set of bitflags indicating which modifiers + * the keyboard's rules utilize. See also: `ModifierKeyConstants`. + */ + KMBM?: number; + /** + * Keyboard Supplementary plane: set to 1 if the keyboard uses non-BMP Unicode + * characters. + */ + KS?: number; + /** + * Keyman Visual Keyboard Layout: defines the touch-layout definitions used for + * 'phone' and 'tablet' form-factors. + */ + KVKL?: LayoutSpec; + /** + * Keyboard is Mnemonic: set to 1 if the keyboard uses a mnemonic layout. + */ + KM?: number; + /** + * KeyBoard VERsion: the version of this keyboard. + */ + KBVER?: string; + /** + * Keyman VERsion: the version of Keyman Developer used to compile this keyboard. + */ + KVER?: string; + /** + * Keyman Variable Stores: an array of the names of all variable stores used by the + * keyboard. + */ + KVS?: (`s${number}`)[]; + /** + * Keyboard Help: HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores. + * + * Reference: https://help.keyman.com/developer/language/reference/kmw_helptext, + * https://help.keyman.com/developer/language/reference/kmw_helpfile + */ + KH?: string; + /** + * Keyboard Virtual Key Dictionary: the Developer-compiled, minified dictionary of virtual-key codes + */ + KVKD?: string; + /** + * Keyboard Display Underlying: set to 1 if the desktop form of the keyboard + * should show the US QWERTY underlying keycaps. These may also appear on + * touch layouts if set and no touch-layout information is available. + */ + KDU?: number; + /** + * Virtual Key Dictionary: the engine pre-processed, unminified dictionary. This is built within + * Keyman Engine for Web at runtime as needed based on the definitions in `KVKD`. + */ + VKDictionary?: Record, + /** + * Keyboard Help File: Embedded JS script designed for use with a keyboard's + * HTML help text. Always defined within the file referenced by &kmw_embedjs + * in a keyboard's source, though that file may also contain _other_ script + * definitions as well. (`KHF` must be explicitly defined within that file.) + * @param e Will be provided with the root element (a
) of the On-Screen Keyboard. + * @returns + */ + KHF?: (e: any) => string; + + /** + * Keyboard Notify Shift: Provided by CJK-picker keyboards to properly + * interface them with Keyman Engine for Web. + * @param {number} _PCommand event code (16,17,18) or 0; 16-18 + * correspond to modifier codes when pressed, while 0 corresponds to loss of focus + * @param {Object} _PTarget target element + * @param {number} _PData 1 or 0 + * @returns + */ + KNS?: (_PCommand: number, _PTarget: OutputTarget, _PData: number) => void; +} & Record<`s${number}`, string> + diff --git a/common/web/types/src/main.ts b/common/web/types/src/main.ts index df21b0fed35..eb0b8a7596f 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -31,3 +31,6 @@ export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js' export { ElementString } from './kmx/kmx-plus/element-string.js'; export { USVString, CasingForm, CasingFunction, TextWithProbability, LexiconTraversal, LexicalModel, LexicalModelPunctuation, Transform, Suggestion, Reversion, Keep, SuggestionTag, Context, Distribution, Outcome, WithOutcome, ProbabilityMass, Configuration, Capabilities, WordBreakingFunction, Span } from './lexical-model-types.js'; + +export { CacheTag, ComplexKeyboardStore, EncodedVisualKeyboard, LayoutSpec, KeyboardObject } from './keyboard-object.js'; +export { OutputTarget } from './outputTarget.interface.js'; diff --git a/web/src/engine/keyboard/src/outputTarget.interface.ts b/common/web/types/src/outputTarget.interface.ts similarity index 98% rename from web/src/engine/keyboard/src/outputTarget.interface.ts rename to common/web/types/src/outputTarget.interface.ts index 410094c812c..34faa5667aa 100644 --- a/web/src/engine/keyboard/src/outputTarget.interface.ts +++ b/common/web/types/src/outputTarget.interface.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ export interface OutputTarget { /** * Signifies that this OutputTarget has no default key processing behaviors. This should be false diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 0df40efe5d3..2dcab2730e8 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -1,3 +1,4 @@ +import { KeyboardObject } from '@keymanapp/common-types'; import { KeymanEngine as KeymanEngineBase, DeviceDetector } from 'keyman/engine/main'; import { getAbsoluteY } from 'keyman/engine/dom-utils'; import { OutputTarget } from 'keyman/engine/element-wrappers'; @@ -6,7 +7,7 @@ import { VisualKeyboard } from 'keyman/engine/osk'; import { ErrorStub, KeyboardStub, CloudQueryResult, toPrefixedKeyboardId as prefixed } from 'keyman/engine/keyboard-storage'; -import { DeviceSpec, Keyboard, KeyboardObject } from "keyman/engine/keyboard"; +import { DeviceSpec, Keyboard } from "keyman/engine/keyboard"; import * as views from './viewsAnchorpoint.js'; import { BrowserConfiguration, BrowserInitOptionDefaults, BrowserInitOptionSpec } from './configuration.js'; diff --git a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts index 303fa99fc3d..52ed1ca5104 100644 --- a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts +++ b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts @@ -1,6 +1,5 @@ -import { Suggestion, Reversion } from '@keymanapp/common-types'; +import { OutputTarget, Suggestion, Reversion } from '@keymanapp/common-types'; import { EventEmitter } from "eventemitter3"; -import { OutputTarget } from "keyman/engine/keyboard"; export class ReadySuggestions { suggestions: Suggestion[]; diff --git a/web/src/engine/interfaces/src/prediction/predictionContext.ts b/web/src/engine/interfaces/src/prediction/predictionContext.ts index 758ef8f46b2..67c433d3f23 100644 --- a/web/src/engine/interfaces/src/prediction/predictionContext.ts +++ b/web/src/engine/interfaces/src/prediction/predictionContext.ts @@ -1,7 +1,6 @@ import { EventEmitter } from "eventemitter3"; -import { Keep, Reversion, Suggestion } from '@keymanapp/common-types'; +import { Keep, type OutputTarget, Reversion, Suggestion } from '@keymanapp/common-types'; import { type LanguageProcessorSpec , ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js'; -import { type OutputTarget } from "keyman/engine/keyboard"; interface PredictionContextEventMap { update: (suggestions: Suggestion[]) => void; diff --git a/web/src/engine/js-processor/src/outputTarget.ts b/web/src/engine/js-processor/src/outputTarget.ts index c055e47602e..29620466928 100644 --- a/web/src/engine/js-processor/src/outputTarget.ts +++ b/web/src/engine/js-processor/src/outputTarget.ts @@ -1,14 +1,13 @@ import { extendString } from "@keymanapp/web-utils"; import { findCommonSubstringEndIndex } from "./stringDivergence.js"; import { Mock } from "./mock.js"; -import { OutputTarget as OutputTargetInterface } from 'keyman/engine/keyboard'; extendString(); // Defines deadkey management in a manner attachable to each element interface. import { type KeyEvent } from 'keyman/engine/keyboard'; import { Deadkey, DeadkeyTracker } from "./deadkeys.js"; -import { ProbabilityMass, Transform } from '@keymanapp/common-types'; +import { OutputTarget as OutputTargetInterface, ProbabilityMass, Transform } from '@keymanapp/common-types'; // Also relies on string-extensions provided by the web-utils package. diff --git a/web/src/engine/keyboard/src/defaultRules.ts b/web/src/engine/keyboard/src/defaultRules.ts index 5512114d937..c1e8c979681 100644 --- a/web/src/engine/keyboard/src/defaultRules.ts +++ b/web/src/engine/keyboard/src/defaultRules.ts @@ -4,10 +4,9 @@ * Implementation of default rules */ -import { ModifierKeyConstants } from '@keymanapp/common-types'; +import { ModifierKeyConstants, type OutputTarget } from '@keymanapp/common-types'; import Codes from './codes.js'; import type KeyEvent from './keyEvent.js'; -import { type OutputTarget } from './outputTarget.interface.js'; export enum EmulationKeystrokes { Enter = '\n', diff --git a/web/src/engine/keyboard/src/index.ts b/web/src/engine/keyboard/src/index.ts index 28b6331c219..830e37fa907 100644 --- a/web/src/engine/keyboard/src/index.ts +++ b/web/src/engine/keyboard/src/index.ts @@ -31,12 +31,11 @@ export * from "./defaultRules.js"; export { default as KeyEvent } from "./keyEvent.js"; export * from "./keyEvent.js"; export { default as KeyMapping } from "./keyMapping.js"; -export { OutputTarget } from "./outputTarget.interface.js"; export * from "@keymanapp/web-utils"; // At the top level, there should be no default export. -// Without the line below... OutputTarget would likely be aliased there, as it's +// Without the line below... KeyMapping would likely be aliased there, as it's // the last `export { default as _ }` => `export * from` pairing seen above. export default undefined; diff --git a/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts b/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts index 3b97659d4bb..9de871b4486 100644 --- a/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts +++ b/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts @@ -4,7 +4,7 @@ ***/ import { Version, deepCopy } from "@keymanapp/web-utils"; -import { ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; +import { EncodedVisualKeyboard, LayoutSpec, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; import LayoutFormFactorSpec = TouchLayout.TouchLayoutPlatform; import LayoutLayerBase = TouchLayout.TouchLayoutLayer; @@ -19,27 +19,6 @@ export { ButtonClasses }; import Codes from "../codes.js"; import type Keyboard from "./keyboard.js"; -export interface EncodedVisualKeyboard { - /** Represents CSS font styling to use for VisualKeyboard text */ - F: string; - /** Should there be a 102nd key? */ - K102?: boolean, - /** - * Keyboard Layer Specification: an object-based map of layer name to the keycaps for its - * 65 keys. The 65 keys are ordered from left to right, then top to bottom. - * - * The key ID corresponding to each index of the array is specified within `Codes.dfltCodes`. - * Entries corresponding to `K_*` in `Codes.dfltCodes` are reserved for future use. - */ - KLS?: {[layerName: string]: string[]}, - /** - * @deprecated - * The older form for data in KLS - defines keycaps for 'default' keys, then 'shift' keys, - * in a single concatenated array. - */ - BK?: string[]; -} - // The following types provide type definitions for the full JSON format we use for visual keyboard definitions. export type ButtonClass = 0 | 1 | 2 | 3 | 4 | /*5 | 6 | 7 |*/ 8 | 9 | 10; @@ -57,12 +36,6 @@ export interface LayoutFormFactor extends LayoutFormFactorSpec { layer: LayoutLayer[] }; -export type LayoutSpec = { - "desktop"?: LayoutFormFactorSpec, - "phone"?: LayoutFormFactorSpec, - "tablet"?: LayoutFormFactorSpec -} - const KEY_102_WIDTH = 200; // This class manages default layout construction for consumption by OSKs without a specified layout. diff --git a/web/src/engine/keyboard/src/keyboards/keyboard.ts b/web/src/engine/keyboard/src/keyboards/keyboard.ts index 42463cd6e7f..01e584a36d5 100644 --- a/web/src/engine/keyboard/src/keyboards/keyboard.ts +++ b/web/src/engine/keyboard/src/keyboards/keyboard.ts @@ -1,27 +1,13 @@ import Codes from "../codes.js"; -import { EncodedVisualKeyboard, LayoutSpec, Layouts } from "./defaultLayouts.js"; +import { Layouts } from "./defaultLayouts.js"; import { ActiveKey, ActiveLayout, ActiveSubKey } from "./activeLayout.js"; import KeyEvent from "../keyEvent.js"; -import { type OutputTarget } from "../outputTarget.interface.js"; -import { ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; +import { CacheTag, ComplexKeyboardStore, KeyboardObject, LayoutSpec, ModifierKeyConstants, type OutputTarget, TouchLayout } from "@keymanapp/common-types"; type TouchLayoutSpec = TouchLayout.TouchLayoutPlatform & { isDefault?: boolean}; import { Version, DeviceSpec } from "@keymanapp/web-utils"; import StateKeyMap from "./stateKeyMap.js"; -type ComplexKeyboardStore = ( string | { t: 'd', d: number } | { ['t']: 'b' })[]; - -/** - * Stores preprocessed properties of a keyboard for quick retrieval later. - */ -class CacheTag { - stores: {[storeName: string]: ComplexKeyboardStore}; - - constructor() { - this.stores = {}; - } -} - export enum LayoutState { NOT_LOADED = undefined, POLYFILLED = 1, @@ -32,157 +18,6 @@ export interface VariableStoreDictionary { [name: string]: string; }; -export type KeyboardObject = { - /** - * Used internally by Keyman Engine for Web to hold preprocessed stores. - */ - _kmw?: CacheTag; - - /** - * group-start: the function triggering processing for the keyboard's - * "Unicode" start group, corresponding to `begin Unicode > use(_____)` in - * Keyman keyboard language. - * @param outputTarget The context to which the keystroke applies - * @param keystroke The full, pre-processed keystroke triggering - * keyboard-rule application. - */ - gs(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; - - /** - * group-newcontext: the function triggering processing for the keyboard's - * "NewContext" start group, corresponding to `begin NewContext > use(_____)` - * in Keyman keyboard language. - * @param outputTarget The new context to be used with future keystrokes - * @param keystroke A 'null' `KeyEvent` providing current modifier + state information. - */ - gn?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; - - /** - * group-postkeystroke: the function triggering processing for the keyboard's - * "PostKeystroke" start group, corresponding to `begin PostKeystroke > - * use(_____)` in Keyman keyboard language. - * @param outputTarget The context altered by a recent keystroke. As a - * precondition, all changes due to `gs` / `begin Unicode` should already be - * applied. - * @param keystroke A 'null' `KeyEvent` providing current modifier + state information. - */ - gpk?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; - - /** - * Keyboard ID: the uniquely-identifying name for this keyboard. Includes the standard - * `Keyboard_` prefix. May be 'namespaced' with a prefix corresponding to a package name - * within app/webview. - */ - KI: string; - /** - * Keyboard Name: the human-readable name of the keyboard. - */ - KN: string; - /** - * Encoded data usable to construct a desktop/hardware-oriented on-screen keyboard. - */ - KV: EncodedVisualKeyboard; - /** - * Keyboard Language Code: set within select keyboards. - * - * Currently, it's only used to determine the need for CJK-picker support. Is missing - * in most compiled keyboards. - */ - KLC?: string; - /** - * @deprecated - * Keyboard Language Code: set within select keyboards. - * - * Currently, it's only used to determine the need for CJK-picker support. - * Is (probably) an older name of KLC with the identical purpose. Is missing - * in most compiled keyboards. - */ - LanguageCode?: string; - /** - * Keyboard CSS: provides the definition for custom keyboard style sheets - */ - KCSS?: string; - /** - * Keyboard is RTL: a simple flag noting if the keyboard's script is RTL. - */ - KRTL?: boolean; - /** - * Keyboard Modifier BitMask: a set of bitflags indicating which modifiers - * the keyboard's rules utilize. See also: `ModifierKeyConstants`. - */ - KMBM?: number; - /** - * Keyboard Supplementary plane: set to 1 if the keyboard uses non-BMP Unicode - * characters. - */ - KS?: number; - /** - * Keyman Visual Keyboard Layout: defines the touch-layout definitions used for - * 'phone' and 'tablet' form-factors. - */ - KVKL?: LayoutSpec; - /** - * Keyboard is Mnemonic: set to 1 if the keyboard uses a mnemonic layout. - */ - KM?: number; - /** - * KeyBoard VERsion: the version of this keyboard. - */ - KBVER?: string; - /** - * Keyman VERsion: the version of Keyman Developer used to compile this keyboard. - */ - KVER?: string; - /** - * Keyman Variable Stores: an array of the names of all variable stores used by the - * keyboard. - */ - KVS?: (`s${number}`)[]; - /** - * Keyboard Help: HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores. - * - * Reference: https://help.keyman.com/developer/language/reference/kmw_helptext, - * https://help.keyman.com/developer/language/reference/kmw_helpfile - */ - KH?: string; - /** - * Keyboard Virtual Key Dictionary: the Developer-compiled, minified dictionary of virtual-key codes - */ - KVKD?: string; - /** - * Keyboard Display Underlying: set to 1 if the desktop form of the keyboard - * should show the US QWERTY underlying keycaps. These may also appear on - * touch layouts if set and no touch-layout information is available. - */ - KDU?: number; - /** - * Virtual Key Dictionary: the engine pre-processed, unminified dictionary. This is built within - * Keyman Engine for Web at runtime as needed based on the definitions in `KVKD`. - */ - VKDictionary?: Record, - /** - * Keyboard Help File: Embedded JS script designed for use with a keyboard's - * HTML help text. Always defined within the file referenced by &kmw_embedjs - * in a keyboard's source, though that file may also contain _other_ script - * definitions as well. (`KHF` must be explicitly defined within that file.) - * @param e Will be provided with the root element (a
) of the On-Screen Keyboard. - * @returns - */ - KHF?: (e: any) => string; - - /** - * Keyboard Notify Shift: Provided by CJK-picker keyboards to properly - * interface them with Keyman Engine for Web. - * @param {number} _PCommand event code (16,17,18) or 0; 16-18 - * correspond to modifier codes when pressed, while 0 corresponds to loss of focus - * @param {Object} _PTarget target element - * @param {number} _PData 1 or 0 - * @returns - */ - KNS?: (_PCommand: number, _PTarget: OutputTarget, _PData: number) => void; -} & Record<`s${number}`, string> - - /** * Acts as a wrapper class for Keyman keyboards compiled to JS, providing type information * and keyboard-centered functionality in an object-oriented way without modifying the diff --git a/web/src/engine/main/src/keyboardInterface.ts b/web/src/engine/main/src/keyboardInterface.ts index 83f3cf0e624..aca05fee586 100644 --- a/web/src/engine/main/src/keyboardInterface.ts +++ b/web/src/engine/main/src/keyboardInterface.ts @@ -1,4 +1,4 @@ -import { KeyboardObject } from "keyman/engine/keyboard"; +import { KeyboardObject } from '@keymanapp/common-types'; import { KeyboardInterface as KeyboardInterfaceBase } from 'keyman/engine/js-processor'; import { KeyboardStub, RawKeyboardStub, toUnprefixedKeyboardId as unprefixed } from 'keyman/engine/keyboard-storage'; From 9a3a091f3f4a0acf28155659c95296064e26de28 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Tue, 8 Oct 2024 14:07:33 -0400 Subject: [PATCH 10/21] auto: increment master version to 18.0.124 --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 9439ba6224d..f472d180844 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 18.0.123 alpha 2024-10-08 + +* chore(developer,common): deps: replace xml2js with fast-xml-parser (#12502) +* chore(ios): renew certificate (#12512) + ## 18.0.122 alpha 2024-10-07 * feat(mac): both option keys generate right alt if no left alt mapping (#12458) diff --git a/VERSION.md b/VERSION.md index fa985f788c5..759872df52a 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.123 \ No newline at end of file +18.0.124 \ No newline at end of file From 15993ff32a923208a9db251726fdb3bc50f320e3 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 9 Oct 2024 17:21:49 +0700 Subject: [PATCH 11/21] fix(android): Apply review suggestions --- .../src/main/java/com/keyman/android/SystemKeyboard.java | 2 +- .../java/com/keyman/engine/KMKeyboardWebViewClient.java | 2 +- .../app/src/main/java/com/keyman/engine/KMManager.java | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index e9d812e6d94..5ca3de5807f 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -172,7 +172,7 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); int maySuggest = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey(langId), KMManager.KMDefault_Suggestion); // Enable banner if maySuggest is not SuggestionType.SUGGESTIONS_DISABLED (0) - KMManager.setBannerOptions(maySuggest > SuggestionType.SUGGESTIONS_DISABLED.toInt()); + KMManager.setBannerOptions(maySuggest != SuggestionType.SUGGESTIONS_DISABLED.toInt()); } else { KMManager.setBannerOptions(false); } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index 620ae252f41..5ce294909c1 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -168,7 +168,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!KMManager.getMayPredictOverride() && KMManager.currentLexicalModel != null) { modelPredictionPref = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey( KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), KMManager.KMDefault_Suggestion) - > SuggestionType.SUGGESTIONS_DISABLED.toInt(); + != SuggestionType.SUGGESTIONS_DISABLED.toInt(); } KMManager.setBannerOptions(modelPredictionPref); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index e434f3be7ae..e776476aff6 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -217,8 +217,7 @@ public static SuggestionType fromInt(int mode) { } public int toInt() { - int modes[] = { 0, 1, 2, 3 }; - return modes[this.ordinal()]; + return this.ordinal(); } } @@ -346,8 +345,8 @@ public int toInt() { public static final int KMMinimum_LongpressDelay = 300; public static final int KMMaximum_LongpressDelay = 1500; - // Default prediction/correction setting - corresponds to SuggestionType.PREDICTIONS_WITH_CORRECTIONS - public static final int KMDefault_Suggestion = 2; + // Default prediction/correction setting + public static final int KMDefault_Suggestion = SuggestionType.PREDICTIONS_WITH_CORRECTIONS.toInt(); // Keyman files protected static final String KMFilename_KeyboardHtml = "keyboard.html"; From 3232c2a1a1c05754246831d83f078445e472c2a1 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 9 Oct 2024 15:46:37 +0200 Subject: [PATCH 12/21] chore(common): allow to run `build.sh` scripts in `bashdb` debugger One of the traps prevents debugging a build script in the `bashdb` debugger. This change skips trapping `err` and `exit` when run under a debugger and so allows to debug the script with `bashdb`. --- resources/builder.inc.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh index c4cf586f42b..f72c36ca726 100755 --- a/resources/builder.inc.sh +++ b/resources/builder.inc.sh @@ -1488,7 +1488,10 @@ _builder_parse_expanded_parameters() { # Note: if an error occurs within a script's function in a `set -e` script, it becomes an exit # instead for the function's caller. So, we need both `err` and `exit` here. # See https://medium.com/@dirk.avery/the-bash-trap-trap-ce6083f36700. - trap _builder_failure_trap err exit + if [[ -z "$(trap -p DEBUG)" ]]; then + # not running in bashdb + trap _builder_failure_trap err exit + fi } _builder_pad() { From 2184749a33c52dd96bba35ed411f1e2c6e98bb7d Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 9 Oct 2024 15:53:31 +0200 Subject: [PATCH 13/21] chore(linux): allow to skip API change When running locally it might be beneficial to be able run the API verification checks without having to build a binary package. This change allows to omit the `--bin-pkg` parameter and outputs a warning instead if it's missing. --- linux/scripts/deb-packaging.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/linux/scripts/deb-packaging.sh b/linux/scripts/deb-packaging.sh index 22840396d54..4f9be7d329d 100755 --- a/linux/scripts/deb-packaging.sh +++ b/linux/scripts/deb-packaging.sh @@ -82,6 +82,10 @@ output_error() { } check_api_not_changed() { + if [[ -z "${BIN_PKG:-}" ]]; then + output_warning "Skipping check for API change because binary Debian package not specified" + return + fi # Checks that the API did not change compared to what's documented in the .symbols file tmpDir=$(mktemp -d) # shellcheck disable=SC2064 From 13caa2e07494b180e68367af2160b85de5607f50 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 9 Oct 2024 16:04:03 +0200 Subject: [PATCH 14/21] refactor(linux): move API verification functions to separate file This will make it easier to unit test individual functions. --- linux/scripts/deb-packaging.sh | 260 +--------------------- linux/scripts/test/deb-packaging.tests.sh | 14 +- linux/scripts/verify_api.inc.sh | 258 +++++++++++++++++++++ 3 files changed, 268 insertions(+), 264 deletions(-) create mode 100644 linux/scripts/verify_api.inc.sh diff --git a/linux/scripts/deb-packaging.sh b/linux/scripts/deb-packaging.sh index 4f9be7d329d..923e5955c62 100755 --- a/linux/scripts/deb-packaging.sh +++ b/linux/scripts/deb-packaging.sh @@ -12,6 +12,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" ## END STANDARD BUILD SCRIPT INCLUDE +. "${KEYMAN_ROOT}/linux/scripts/verify_api.inc.sh" + builder_describe \ "Helper for building Debian packages." \ "dependencies Install dependencies as found in debian/control" \ @@ -61,262 +63,6 @@ source_action() { mv builddebs/* "${OUTPUT_PATH:-..}" } -output_log() { - echo "$1" >&2 - builder_echo "$1" -} - -output_ok() { - echo ":heavy_check_mark: $1" >&2 - builder_echo green "OK: $1" -} - -output_warning() { - echo ":warning: $1" >&2 - builder_echo warning "WARNING: $1" -} - -output_error() { - echo ":x: $1" >&2 - builder_echo error "ERROR: $1" -} - -check_api_not_changed() { - if [[ -z "${BIN_PKG:-}" ]]; then - output_warning "Skipping check for API change because binary Debian package not specified" - return - fi - # Checks that the API did not change compared to what's documented in the .symbols file - tmpDir=$(mktemp -d) - # shellcheck disable=SC2064 - trap "rm -rf \"${tmpDir}\"" ERR - dpkg -x "${BIN_PKG}" "${tmpDir}" - mkdir -p debian/tmp/DEBIAN - dpkg-gensymbols -v"${VERSION}" -p"${PKG_NAME}" -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/"${LIB_NAME}".so* -c4 - output_ok "${LIB_NAME} API didn't change" - cd "${REPO_ROOT}/linux" - rm -rf "${tmpDir}" - trap ERR -} - -# -# Compare the SHA of the base and head commits for changes to the .symbols file -# -is_symbols_file_changed() { - local CHANGED_REF CHANGED_BASE - CHANGED_REF=$(git rev-parse "${GIT_SHA}":"linux/debian/${PKG_NAME}.symbols") - CHANGED_BASE=$(git rev-parse "${GIT_BASE}":"linux/debian/${PKG_NAME}.symbols") - if [[ "${CHANGED_REF}" == "${CHANGED_BASE}" ]]; then - return 1 - fi - return 0 -} - -get_changes() { - local WHAT_CHANGED - WHAT_CHANGED=$(git diff -I "^${LIB_NAME}.so" "$1".."$2" | diffstat -m -t | grep "${PKG_NAME}.symbols" ) - - IFS=',' read -r -a CHANGES <<< "${WHAT_CHANGED:-0,0,0}" -} - -check_updated_version_number() { - # Checks that the package version number got updated in the .symbols file if it got changed - # shellcheck disable=SC2310 - if is_symbols_file_changed; then - # .symbols file changed, now check if the package version got updated as well - # Note: We don't check that ALL changes in that file have an updated package version - - # we hope this gets flagged in code review. - # Note: This version number check may not match the actual released version, if the branch - # is out of date when it is merged to the release branch (master/beta/stable-x.y). If this - # is considered important, then make sure the branch is up to date, and wait for test - # builds to complete, before merging. - get_changes "${GIT_BASE}" "${GIT_SHA}" - INSERTED="${CHANGES[0]}" - DELETED="${CHANGES[1]}" - MODIFIED="${CHANGES[2]}" - - if (( DELETED > 0 )) && (( MODIFIED == 0 )) && (( INSERTED == 0)); then - # If only lines got removed we basically skip this test. A later check will - # test that the API version got updated. - output_ok "${PKG_NAME}.symbols file did change but only removed lines" - elif ! git log -p -1 -- "debian/${PKG_NAME}.symbols" | grep -q "${VERSION}"; then - output_error "${PKG_NAME}.symbols file got changed without changing the package version number of the symbol" - EXIT_CODE=1 - else - output_ok "${PKG_NAME}.symbols file got updated with package version number" - fi - else - output_ok "${PKG_NAME}.symbols file didn't change" - fi -} - -get_api_version_in_symbols_file() { - # Retrieve symbols file at commit $1 and extract "1" from - # "libkeymancore.so.1 libkeymancore1 #MINVER#" - local firstline tmpfile - local sha="$1" - - tmpfile=$(mktemp) - if ! git cat-file blob "${sha}:linux/debian/${PKG_NAME}.symbols" > "${tmpfile}" 2>/dev/null; then - rm "${tmpfile}" - echo "-1" - return - fi - - firstline="$(head -1 "${tmpfile}")" - firstline="${firstline#"${LIB_NAME}".so.}" - firstline="${firstline%% *}" - - rm "${tmpfile}" - echo "${firstline}" -} - -get_api_version_from_core() { - # Retrieve CORE_API_VERSION.md from commit $1 and extract major version - # number ("1") from "1.0.0" - local api_version tmpfile - local sha="$1" - tmpfile=$(mktemp) - - if ! git cat-file blob "${sha}:core/CORE_API_VERSION.md" > "${tmpfile}" 2>/dev/null; then - rm "${tmpfile}" - echo "-1" - return - fi - - api_version=$(cat "${tmpfile}") - api_version=${api_version%%.*} - - rm "${tmpfile}" - echo "${api_version}" -} - -# Check if the API version got updated -# Returns: -# 0 - if the API version got updated -# 1 - the .symbols file got changed but the API version didn't get updated -# 2 - if we're in the alpha tier and the API version got updated since -# the last stable version -# NOTE: it is up to the caller to check if this is a major version -# change that requires an API version update. -# Check if the API version got updated -# Returns: -# 0 - if the API version got updated -# 1 - the .symbols file got changed but the API version didn't get updated -# 2 - if we're in the alpha tier and the API version got updated since -# the last stable version -# NOTE: it is up to the caller to check if this is a major version -# change that requires an API version update. -is_api_version_updated() { - local OLD_API_VERSION NEW_API_VERSION TIER - OLD_API_VERSION=$(get_api_version_in_symbols_file "${GIT_BASE}") - NEW_API_VERSION=$(get_api_version_in_symbols_file "${GIT_SHA}") - if (( NEW_API_VERSION > OLD_API_VERSION )); then - echo "0" - return - fi - - # API version didn't change. However, that might be ok if we're in alpha - # and a major change happened previously. - TIER=$(cat "${REPO_ROOT}/TIER.md") - case ${TIER} in - alpha) - local STABLE_VERSION STABLE_API_VERSION STABLE_BRANCH - STABLE_VERSION=$((${VERSION%%.*} - 1)) - STABLE_BRANCH="origin/stable-${STABLE_VERSION}.0" - STABLE_API_VERSION=$(get_api_version_in_symbols_file "${STABLE_BRANCH}") - if (( STABLE_API_VERSION == -1 )); then - # .symbols file doesn't exist in stable branch, so let's check CORE_API_VERSION.md. That - # doesn't exist in 16.0 but appeared in 17.0. - STABLE_API_VERSION=$(get_api_version_from_core "${STABLE_BRANCH}") - if (( STABLE_API_VERSION == -1 )); then - # CORE_API_VERSION.md doesn't exist either - if (( NEW_API_VERSION > 0 )); then - # .symbols and CORE_API_VERSION.md file don't exist in stable branch; however, we - # incremented the version number compared to 16.0, so that's ok - echo "2" - return - fi - fi - fi - if (( NEW_API_VERSION > STABLE_API_VERSION )); then - echo "2" - return - fi ;; - *) - ;; - esac - - echo "1" -} - -check_for_major_api_changes() { - # Checks that API version number gets updated if API changes - local WHAT_CHANGED CHANGES INSERTED DELETED MODIFIED UPDATED - - # shellcheck disable=2310 - if ! is_symbols_file_changed; then - output_ok "No major API change" - return - fi - - get_changes "${GIT_BASE}" "${GIT_SHA}" - INSERTED="${CHANGES[0]}" - DELETED="${CHANGES[1]}" - MODIFIED="${CHANGES[2]}" - - if (( DELETED > 0 )) || (( MODIFIED > 0 )); then - builder_echo "Major API change: ${DELETED} lines deleted and ${MODIFIED} lines modified" - UPDATED=$(is_api_version_updated) - if [[ ${UPDATED} == 1 ]]; then - output_error "Major API change without updating API version number in ${PKG_NAME}.symbols file" - EXIT_CODE=2 - elif [[ ${UPDATED} == 2 ]]; then - output_ok "API version number got previously updated in ${PKG_NAME}.symbols file after major API change; no change within alpha necessary" - else - output_ok "API version number got updated in ${PKG_NAME}.symbols file after major API change" - fi - elif (( INSERTED > 0 )); then - output_ok "Minor API change: ${INSERTED} lines added" - # We currently don't check version number for minor API changes - else - output_ok "No major API change" - fi -} - -check_for_api_version_consistency() { - # Checks that the (major) API version number in the .symbols file and - # in CORE_API_VERSION.md are the same - local symbols_version api_version - symbols_version=$(get_api_version_in_symbols_file "HEAD") - api_version=$(get_api_version_from_core "HEAD") - - if (( symbols_version == api_version )); then - output_ok "API version in .symbols file and in CORE_API_VERSION.md is the same" - else - output_error "API version in .symbols file and in CORE_API_VERSION.md is different" - EXIT_CODE=3 - fi -} - -verify_action() { - local SONAME - SONAME=$(get_api_version_from_core "HEAD") - LIB_NAME=libkeymancore - PKG_NAME="${LIB_NAME}${SONAME}" - if [[ ! -f debian/${PKG_NAME}.symbols ]]; then - output_error "Missing ${PKG_NAME}.symbols file" - exit 0 - fi - - EXIT_CODE=0 - check_api_not_changed - check_updated_version_number - check_for_major_api_changes - check_for_api_version_consistency - exit "${EXIT_CODE}" -} - builder_run_action dependencies dependencies_action builder_run_action source source_action -builder_run_action verify verify_action +builder_run_action verify verify_api_action diff --git a/linux/scripts/test/deb-packaging.tests.sh b/linux/scripts/test/deb-packaging.tests.sh index 04ff42b7115..134421e0a43 100755 --- a/linux/scripts/test/deb-packaging.tests.sh +++ b/linux/scripts/test/deb-packaging.tests.sh @@ -224,10 +224,10 @@ test_check_updated_version_number__LineRemoved_InAlpha_FileMissingInStable_ApiVe git checkout master # simulate a commit that renamed the .symbols file and updated the API version git mv linux/debian/libkeymancore1.symbols linux/debian/libfoo2.symbols - sed -i 's/libkeymancore/libfoo/' linux/scripts/deb-packaging.sh + sed -i 's/libkeymancore/libfoo/' linux/scripts/verify_api.inc.sh # shellcheck disable=2016 # single quotes are intentional here - sed -i 's/${SONAME}/2/' linux/scripts/deb-packaging.sh - git add linux/scripts/deb-packaging.sh + sed -i 's/${SONAME}/2/' linux/scripts/verify_api.inc.sh + git add linux/scripts/verify_api.inc.sh echo "2.0.0" > core/CORE_API_VERSION.md git add core/CORE_API_VERSION.md sed -i 's/libkeymancore1/libfoo2/' linux/debian/libfoo2.symbols @@ -251,8 +251,8 @@ test_check_updated_version_number__LineRemoved_InAlpha_FileMissingInStable_ApiVe git checkout master # simulate a commit that renamed the .symbols file git mv linux/debian/libkeymancore1.symbols linux/debian/libfoo1.symbols - sed -i 's/libkeymancore/libfoo/' linux/scripts/deb-packaging.sh - git add linux/scripts/deb-packaging.sh + sed -i 's/libkeymancore/libfoo/' linux/scripts/verify_api.inc.sh + git add linux/scripts/verify_api.inc.sh sed -i 's/libkeymancore/libfoo/' linux/debian/libfoo1.symbols git add linux/debian/libfoo1.symbols git commit -m "renamed library" @@ -344,8 +344,8 @@ test_check_updated_version_number__LineRemoved_InBeta_FileMissingInStable_ApiVer git checkout -b beta # simulate a commit that renamed the .symbols file git mv linux/debian/libkeymancore1.symbols linux/debian/libfoo1.symbols - sed -i 's/libkeymancore/libfoo/' linux/scripts/deb-packaging.sh - git add linux/scripts/deb-packaging.sh + sed -i 's/libkeymancore/libfoo/' linux/scripts/verify_api.inc.sh + git add linux/scripts/verify_api.inc.sh sed -i 's/libkeymancore/libfoo/' linux/debian/libfoo1.symbols git add linux/debian/libfoo1.symbols git commit -m "renamed library" diff --git a/linux/scripts/verify_api.inc.sh b/linux/scripts/verify_api.inc.sh new file mode 100644 index 00000000000..63072bc4f7c --- /dev/null +++ b/linux/scripts/verify_api.inc.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2154 # (variables are set in build-utils.sh) + +output_log() { + echo "$1" >&2 + builder_echo "$1" +} + +output_ok() { + echo ":heavy_check_mark: $1" >&2 + builder_echo green "OK: $1" +} + +output_warning() { + echo ":warning: $1" >&2 + builder_echo warning "WARNING: $1" +} + +output_error() { + echo ":x: $1" >&2 + builder_echo error "ERROR: $1" +} + +check_api_not_changed() { + if [[ -z "${BIN_PKG:-}" ]]; then + output_warning "Skipping check for API change because binary Debian package not specified" + return + fi + # Checks that the API did not change compared to what's documented in the .symbols file + tmpDir=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf \"${tmpDir}\"" ERR + dpkg -x "${BIN_PKG}" "${tmpDir}" + mkdir -p debian/tmp/DEBIAN + dpkg-gensymbols -v"${VERSION}" -p"${PKG_NAME}" -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/"${LIB_NAME}".so* -c4 + output_ok "${LIB_NAME} API didn't change" + cd "${REPO_ROOT}/linux" + rm -rf "${tmpDir}" + trap ERR +} + +# +# Compare the SHA of the base and head commits for changes to the .symbols file +# +is_symbols_file_changed() { + local CHANGED_REF CHANGED_BASE + CHANGED_REF=$(git rev-parse "${GIT_SHA}":"linux/debian/${PKG_NAME}.symbols") + CHANGED_BASE=$(git rev-parse "${GIT_BASE}":"linux/debian/${PKG_NAME}.symbols") + if [[ "${CHANGED_REF}" == "${CHANGED_BASE}" ]]; then + return 1 + fi + return 0 +} + +get_changes() { + local WHAT_CHANGED + WHAT_CHANGED=$(git diff -I "^${LIB_NAME}.so" "$1".."$2" | diffstat -m -t | grep "${PKG_NAME}.symbols" ) + + IFS=',' read -r -a CHANGES <<< "${WHAT_CHANGED:-0,0,0}" +} + +check_updated_version_number() { + # Checks that the package version number got updated in the .symbols file if it got changed + # shellcheck disable=SC2310 + if is_symbols_file_changed; then + # .symbols file changed, now check if the package version got updated as well + # Note: We don't check that ALL changes in that file have an updated package version - + # we hope this gets flagged in code review. + # Note: This version number check may not match the actual released version, if the branch + # is out of date when it is merged to the release branch (master/beta/stable-x.y). If this + # is considered important, then make sure the branch is up to date, and wait for test + # builds to complete, before merging. + get_changes "${GIT_BASE}" "${GIT_SHA}" + INSERTED="${CHANGES[0]}" + DELETED="${CHANGES[1]}" + MODIFIED="${CHANGES[2]}" + + if (( DELETED > 0 )) && (( MODIFIED == 0 )) && (( INSERTED == 0)); then + # If only lines got removed we basically skip this test. A later check will + # test that the API version got updated. + output_ok "${PKG_NAME}.symbols file did change but only removed lines" + elif ! git log -p -1 -- "debian/${PKG_NAME}.symbols" | grep -q "${VERSION}"; then + output_error "${PKG_NAME}.symbols file got changed without changing the package version number of the symbol" + EXIT_CODE=1 + else + output_ok "${PKG_NAME}.symbols file got updated with package version number" + fi + else + output_ok "${PKG_NAME}.symbols file didn't change" + fi +} + +get_api_version_in_symbols_file() { + # Retrieve symbols file at commit $1 and extract "1" from + # "libkeymancore.so.1 libkeymancore1 #MINVER#" + local firstline tmpfile + local sha="$1" + + tmpfile=$(mktemp) + if ! git cat-file blob "${sha}:linux/debian/${PKG_NAME}.symbols" > "${tmpfile}" 2>/dev/null; then + rm "${tmpfile}" + echo "-1" + return + fi + + firstline="$(head -1 "${tmpfile}")" + firstline="${firstline#"${LIB_NAME}".so.}" + firstline="${firstline%% *}" + + rm "${tmpfile}" + echo "${firstline}" +} + +get_api_version_from_core() { + # Retrieve CORE_API_VERSION.md from commit $1 and extract major version + # number ("1") from "1.0.0" + local api_version tmpfile + local sha="$1" + tmpfile=$(mktemp) + + if ! git cat-file blob "${sha}:core/CORE_API_VERSION.md" > "${tmpfile}" 2>/dev/null; then + rm "${tmpfile}" + echo "-1" + return + fi + + api_version=$(cat "${tmpfile}") + api_version=${api_version%%.*} + + rm "${tmpfile}" + echo "${api_version}" +} + +# Check if the API version got updated +# Returns: +# 0 - if the API version got updated +# 1 - the .symbols file got changed but the API version didn't get updated +# 2 - if we're in the alpha tier and the API version got updated since +# the last stable version +# NOTE: it is up to the caller to check if this is a major version +# change that requires an API version update. +# Check if the API version got updated +# Returns: +# 0 - if the API version got updated +# 1 - the .symbols file got changed but the API version didn't get updated +# 2 - if we're in the alpha tier and the API version got updated since +# the last stable version +# NOTE: it is up to the caller to check if this is a major version +# change that requires an API version update. +is_api_version_updated() { + local OLD_API_VERSION NEW_API_VERSION TIER + OLD_API_VERSION=$(get_api_version_in_symbols_file "${GIT_BASE}") + NEW_API_VERSION=$(get_api_version_in_symbols_file "${GIT_SHA}") + if (( NEW_API_VERSION > OLD_API_VERSION )); then + echo "0" + return + fi + + # API version didn't change. However, that might be ok if we're in alpha + # and a major change happened previously. + TIER=$(cat "${REPO_ROOT}/TIER.md") + case ${TIER} in + alpha) + local STABLE_VERSION STABLE_API_VERSION STABLE_BRANCH + STABLE_VERSION=$((${VERSION%%.*} - 1)) + STABLE_BRANCH="origin/stable-${STABLE_VERSION}.0" + STABLE_API_VERSION=$(get_api_version_in_symbols_file "${STABLE_BRANCH}") + if (( STABLE_API_VERSION == -1 )); then + # .symbols file doesn't exist in stable branch, so let's check CORE_API_VERSION.md. That + # doesn't exist in 16.0 but appeared in 17.0. + STABLE_API_VERSION=$(get_api_version_from_core "${STABLE_BRANCH}") + if (( STABLE_API_VERSION == -1 )); then + # CORE_API_VERSION.md doesn't exist either + if (( NEW_API_VERSION > 0 )); then + # .symbols and CORE_API_VERSION.md file don't exist in stable branch; however, we + # incremented the version number compared to 16.0, so that's ok + echo "2" + return + fi + fi + fi + if (( NEW_API_VERSION > STABLE_API_VERSION )); then + echo "2" + return + fi ;; + *) + ;; + esac + + echo "1" +} + +check_for_major_api_changes() { + # Checks that API version number gets updated if API changes + local WHAT_CHANGED CHANGES INSERTED DELETED MODIFIED UPDATED + + # shellcheck disable=2310 + if ! is_symbols_file_changed; then + output_ok "No major API change" + return + fi + + get_changes "${GIT_BASE}" "${GIT_SHA}" + INSERTED="${CHANGES[0]}" + DELETED="${CHANGES[1]}" + MODIFIED="${CHANGES[2]}" + + if (( DELETED > 0 )) || (( MODIFIED > 0 )); then + builder_echo "Major API change: ${DELETED} lines deleted and ${MODIFIED} lines modified" + UPDATED=$(is_api_version_updated) + if [[ ${UPDATED} == 1 ]]; then + output_error "Major API change without updating API version number in ${PKG_NAME}.symbols file" + EXIT_CODE=2 + elif [[ ${UPDATED} == 2 ]]; then + output_ok "API version number got previously updated in ${PKG_NAME}.symbols file after major API change; no change within alpha necessary" + else + output_ok "API version number got updated in ${PKG_NAME}.symbols file after major API change" + fi + elif (( INSERTED > 0 )); then + output_ok "Minor API change: ${INSERTED} lines added" + # We currently don't check version number for minor API changes + else + output_ok "No major API change" + fi +} + +check_for_api_version_consistency() { + # Checks that the (major) API version number in the .symbols file and + # in CORE_API_VERSION.md are the same + local symbols_version api_version + symbols_version=$(get_api_version_in_symbols_file "HEAD") + api_version=$(get_api_version_from_core "HEAD") + + if (( symbols_version == api_version )); then + output_ok "API version in .symbols file and in CORE_API_VERSION.md is the same" + else + output_error "API version in .symbols file and in CORE_API_VERSION.md is different" + EXIT_CODE=3 + fi +} + +verify_api_action() { + local SONAME + SONAME=$(get_api_version_from_core "HEAD") + LIB_NAME=libkeymancore + PKG_NAME="${LIB_NAME}${SONAME}" + if [[ ! -f debian/${PKG_NAME}.symbols ]]; then + output_error "Missing ${PKG_NAME}.symbols file" + exit 0 + fi + + EXIT_CODE=0 + check_api_not_changed + check_updated_version_number + check_for_major_api_changes + check_for_api_version_consistency + exit "${EXIT_CODE}" +} From e5c1f810374d9c43a92daf766c5c43c0cba44e46 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 9 Oct 2024 19:18:08 +0200 Subject: [PATCH 15/21] fix(linux): fix problem with API checks with merge commits This change fixes a problem if both `master` and a branch added the same symbol with different version numbers. This causes a merge conflict which previously caused the API checks to output an error. However, if the version number in the .symbols file got updated since the base then it's no error which this change implements. --- linux/scripts/test/deb-packaging.tests.sh | 127 +++++++--------------- linux/scripts/test/test.inc.sh | 89 +++++++++++++++ linux/scripts/test/test.sh | 5 + linux/scripts/test/verify_api.tests.sh | 105 ++++++++++++++++++ linux/scripts/verify_api.inc.sh | 61 ++++++++++- 5 files changed, 295 insertions(+), 92 deletions(-) create mode 100644 linux/scripts/test/test.inc.sh create mode 100755 linux/scripts/test/test.sh create mode 100755 linux/scripts/test/verify_api.tests.sh diff --git a/linux/scripts/test/deb-packaging.tests.sh b/linux/scripts/test/deb-packaging.tests.sh index 134421e0a43..8da8fd8ec8d 100755 --- a/linux/scripts/test/deb-packaging.tests.sh +++ b/linux/scripts/test/deb-packaging.tests.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu ## START STANDARD BUILD SCRIPT INCLUDE @@ -7,93 +7,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" ## END STANDARD BUILD SCRIPT INCLUDE -mockDebPkgTools() { - echo "#!/bin/bash - " > "${tmpDir}/dpkg" - chmod +x "${tmpDir}/dpkg" - cp "${tmpDir}/dpkg" "${tmpDir}/dpkg-gensymbols" - PATH=${tmpDir}:${PATH} -} - -createBase() { - TIER=$1 - remoteDir=$(mktemp -d) - cd "${remoteDir}" - git init --bare --initial-branch=master . - - workDir=$(mktemp -d) - cd "${workDir}" - git init --initial-branch=master . - git remote add origin "${remoteDir}" - mkdir -p linux/debian - echo "libkeymancore.so.1 libkeymancore1 #MINVER# -* Build-Depends-Package: libkeymancore-dev - - km_core_actions_dispose@Base 17.0.197 - km_core_context_clear@Base 17.0.195 - km_core_context_get@Base 17.0.195 - km_core_context_item_list_size@Base 17.0.195 - km_core_context_items_dispose@Base 17.0.195 -" > linux/debian/libkeymancore1.symbols - git add linux/debian/libkeymancore1.symbols - - mkdir -p core - echo "1.0.0" > core/CORE_API_VERSION.md - git add core/CORE_API_VERSION.md - - echo "16.0.145" > VERSION.md - git add VERSION.md - - echo "stable" > TIER.md - git add TIER.md - - mkdir -p linux/scripts - cp -r "${REPO_ROOT}"/linux/scripts/* linux/scripts - git add linux/scripts - - mkdir -p resources/build - cp -r "${REPO_ROOT}"/resources/build/* resources/build - cp "${REPO_ROOT}"/resources/builder.inc.sh resources/ - git add resources - - git commit -m "Initial" - git push origin master - - git branch -c stable-16.0 - git push origin stable-16.0 - git tag -m "16.0.145" release-16.0.145 - git push origin release-16.0.145 - - echo "${TIER}" > TIER.md - git add TIER.md - - echo "17.0.255" > VERSION.md - git add VERSION.md - git commit -m "Commit for Alpha" - git push origin master - - git checkout -b chore - - BINPKG_NAME=${tmpDir}/libkeymancore1_17.0.257-1+noble1_amd64.deb - touch "${BINPKG_NAME}" -} - -setup() { - OLDPATH=${PATH} - tmpDir=$(mktemp -d) - mockDebPkgTools -} - -teardown() { - PATH=${OLDPATH} - rm -rf ${tmpDir} -} - -run_test() { - setup - $1 > /tmp/$1.log 2>&1 && echo -e "${COLOR_GREEN}$1: OK${COLOR_RESET}" || echo -e "${COLOR_RED}$1: FAILED${COLOR_RESET}" - teardown -} +. "${THIS_SCRIPT%/*}/test.inc.sh" test_check_updated_version_number__NoChange_OK() { createBase alpha @@ -120,6 +34,7 @@ test_check_updated_version_number__LineAdded_OK() { } test_check_updated_version_number__LineAddedWithoutVerUpd_ERROR() { + local output createBase alpha sed -i 's/ km_core_actions_dispose@Base 17.0.197/ km_core_actions_dispose@Base 17.0.197\n km_core_added@Base 17.0.197/' linux/debian/libkeymancore1.symbols @@ -150,6 +65,7 @@ test_check_updated_version_number__LineRemovedWithAPIUpd_OK() { } test_check_updated_version_number__LineRemoved_OnlyCoreApiUpd_ERROR() { + local output createBase alpha echo "2.0.0" > core/CORE_API_VERSION.md git add core/CORE_API_VERSION.md @@ -165,6 +81,7 @@ test_check_updated_version_number__LineRemoved_OnlyCoreApiUpd_ERROR() { } test_check_updated_version_number__LineRemoved_OnlySymbolsFileUpd_ERROR() { + local output createBase alpha git mv linux/debian/libkeymancore{1,2}.symbols sed -i 's/libkeymancore1/libkeymancore2/' linux/debian/libkeymancore2.symbols @@ -181,6 +98,7 @@ test_check_updated_version_number__LineRemoved_OnlySymbolsFileUpd_ERROR() { } test_check_updated_version_number__LineRemovedWithAPIUpd_NotMetadataUpd_ERROR() { + local output createBase alpha echo "2.0.0" > core/CORE_API_VERSION.md git add core/CORE_API_VERSION.md @@ -247,6 +165,7 @@ test_check_updated_version_number__LineRemoved_InAlpha_FileMissingInStable_ApiVe } test_check_updated_version_number__LineRemoved_InAlpha_FileMissingInStable_ApiVerUnchanged_ERROR() { + local output createBase alpha git checkout master # simulate a commit that renamed the .symbols file @@ -271,6 +190,7 @@ test_check_updated_version_number__LineRemoved_InAlpha_FileMissingInStable_ApiVe } test_check_updated_version_number__LineRemoved_InAlpha_ChangeFromStable_ERROR() { + local output createBase alpha sed -i '6d' linux/debian/libkeymancore1.symbols @@ -285,6 +205,7 @@ test_check_updated_version_number__LineRemoved_InAlpha_ChangeFromStable_ERROR() } test_check_updated_version_number__LineRemoved_InBeta_ApiVerUnchanged_ERROR() { + local output createBase beta # simulate a commit that already introduced an API version change in Beta @@ -340,6 +261,7 @@ test_check_updated_version_number__LineRemoved_InBeta_ApiVerChanged_OK() { } test_check_updated_version_number__LineRemoved_InBeta_FileMissingInStable_ApiVerUnchanged_ERROR() { + local output createBase alpha git checkout -b beta # simulate a commit that renamed the .symbols file @@ -363,6 +285,34 @@ test_check_updated_version_number__LineRemoved_InBeta_FileMissingInStable_ApiVer [[ "${output[*]}" == *" ERROR: Major API change without updating API version number in libfoo1.symbols file"* ]] } +test_check_updated_version_number__LineInsertedInBranch_OK() { + createBase alpha + + local base_sha=$(git rev-parse master) + + # Add a line in chore branch + echo " km_core_foo@Base 17.0.200" >> linux/debian/libkeymancore1.symbols + git add linux/debian/libkeymancore1.symbols + git commit -m "API method added in chore branch" + + # Add a line in master branch + git checkout master + echo " km_core_foo@Base 17.0.205" >> linux/debian/libkeymancore1.symbols + git add linux/debian/libkeymancore1.symbols + git commit -m "API method changed in master branch" + echo "readme" > README.md + git add README.md + git commit -m "Some change on master" + git checkout chore + + # merge master into chore + git merge --strategy-option=ours master + + echo "## Calling API verification" + pwd + linux/scripts/deb-packaging.sh --bin-pkg "${BINPKG_NAME}" --git-sha "$(git rev-parse HEAD)" --git-base "${base_sha}" verify +} + echo "(test logs are in /tmp/.log)" run_test test_check_updated_version_number__NoChange_OK run_test test_check_updated_version_number__LineAdded_OK @@ -378,5 +328,6 @@ run_test test_check_updated_version_number__LineRemoved_InAlpha_ChangeFromStable run_test test_check_updated_version_number__LineRemoved_InBeta_ApiVerUnchanged_ERROR run_test test_check_updated_version_number__LineRemoved_InBeta_ApiVerChanged_OK run_test test_check_updated_version_number__LineRemoved_InBeta_FileMissingInStable_ApiVerUnchanged_ERROR +run_test test_check_updated_version_number__LineInsertedInBranch_OK # TODO: still some test cases missing for the different checks diff --git a/linux/scripts/test/test.inc.sh b/linux/scripts/test/test.inc.sh new file mode 100644 index 00000000000..6e21ef2b5d9 --- /dev/null +++ b/linux/scripts/test/test.inc.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +setup() { + OLDPATH=${PATH} + tmpDir="$(mktemp -d)" + mockDebPkgTools +} + +teardown() { + PATH=${OLDPATH} + rm -rf "${tmpDir}" +} + +mockDebPkgTools() { + echo "#!/bin/bash + " > "${tmpDir}/dpkg" + chmod +x "${tmpDir}/dpkg" + cp "${tmpDir}/dpkg" "${tmpDir}/dpkg-gensymbols" + PATH=${tmpDir}:${PATH} +} + +createBase() { + TIER=$1 + remoteDir=$(mktemp -d) + cd "${remoteDir}" + git init --bare --initial-branch=master . + + workDir=$(mktemp -d) + cd "${workDir}" + git init --initial-branch=master . + git remote add origin "${remoteDir}" + mkdir -p linux/debian + echo "libkeymancore.so.1 libkeymancore1 #MINVER# +* Build-Depends-Package: libkeymancore-dev + + km_core_actions_dispose@Base 17.0.197 + km_core_context_clear@Base 17.0.195 + km_core_context_get@Base 17.0.198 + km_core_context_item_list_size@Base 17.0.195 + km_core_context_items_dispose@Base 17.0.195 +" > linux/debian/libkeymancore1.symbols + git add linux/debian/libkeymancore1.symbols + + mkdir -p core + echo "1.0.0" > core/CORE_API_VERSION.md + git add core/CORE_API_VERSION.md + + echo "16.0.145" > VERSION.md + git add VERSION.md + + echo "stable" > TIER.md + git add TIER.md + + mkdir -p linux/scripts + cp -r "${REPO_ROOT}"/linux/scripts/* linux/scripts + git add linux/scripts + + mkdir -p resources/build + cp -r "${REPO_ROOT}"/resources/build/* resources/build + cp "${REPO_ROOT}"/resources/builder.inc.sh resources/ + git add resources + + git commit -m "Initial" + git push origin master + + git branch -c stable-16.0 + git push origin stable-16.0 + git tag -m "16.0.145" release-16.0.145 + git push origin release-16.0.145 + + echo "${TIER}" > TIER.md + git add TIER.md + + echo "17.0.255" > VERSION.md + git add VERSION.md + git commit -m "Commit for Alpha" + git push origin master + + git checkout -b chore + + BINPKG_NAME=${tmpDir}/libkeymancore1_17.0.257-1+noble1_amd64.deb + touch "${BINPKG_NAME}" +} + +run_test() { + setup + $1 > "/tmp/$1.log" 2>&1 && echo -e "${COLOR_GREEN}$1: OK${COLOR_RESET}" || echo -e "${COLOR_RED}$1: FAILED${COLOR_RESET}" + teardown +} diff --git a/linux/scripts/test/test.sh b/linux/scripts/test/test.sh new file mode 100755 index 00000000000..f0a56655d58 --- /dev/null +++ b/linux/scripts/test/test.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -eu + +"$(dirname "$0")/deb-packaging.tests.sh" +"$(dirname "$0")/verify_api.tests.sh" diff --git a/linux/scripts/test/verify_api.tests.sh b/linux/scripts/test/verify_api.tests.sh new file mode 100755 index 00000000000..05f60cea836 --- /dev/null +++ b/linux/scripts/test/verify_api.tests.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Unit tests for verify_api.inc.sh +set -eu +shopt -s inherit_errexit + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "${THIS_SCRIPT%/*}/test.inc.sh" +. "${KEYMAN_ROOT}/linux/scripts/verify_api.inc.sh" + + +fixture_setup() { + SONAME=1 + LIB_NAME=libkeymancore + PKG_NAME="${LIB_NAME}${SONAME}" + EXIT_CODE=-1 +} + +test_compare_versions__less_patch() { + [[ $(compare_versions "17.0.197" "17.0.198") == -1 ]] +} + +test_compare_versions__less_minor() { + [[ $(compare_versions "17.0.197" "17.1.197") == -1 ]] +} + +test_compare_versions__less_major() { + [[ $(compare_versions "17.0.197" "18.0.0") == -1 ]] +} + +test_compare_versions__greater() { + [[ $(compare_versions "17.0.198" "17.0.197") == 1 ]] +} + +test_compare_versions__same() { + [[ $(compare_versions "17.0.197" "17.0.197") == 0 ]] +} + +test_compare_versions__greater0() { + [[ $(compare_versions "17.0.197" "0") == 1 ]] +} + +test_get_highest_version_in_symbols_file() { + local output + createBase alpha + echo ' (c++|optional)"typeinfo name for std::codecvt@Base" 17.0.244' >> linux/debian/libkeymancore1.symbols + git add linux/debian/libkeymancore1.symbols + git commit -m "API method added" + + # Execute + output=$(get_highest_version_in_symbols_file "$(git rev-parse HEAD)") + echo "${output[*]}" # for logging + [[ "${output}" == "17.0.244" ]] +} + +test_check_updated_version_number__LineInsertedInBranch_OK() { + local output + createBase alpha + + local base_sha=$(git rev-parse master) + + # Add a line in chore branch + sed -i 's/km_core_actions_dispose@Base 17.0.197/km_core_actions_dispose@Base 17.0.201/' linux/debian/libkeymancore1.symbols + echo " km_core_foo@Base 17.0.200" >> linux/debian/libkeymancore1.symbols + git add linux/debian/libkeymancore1.symbols + git commit -m "API method added in chore branch" + + # Add a line in master branch + git checkout master + sed -i 's/km_core_actions_dispose@Base 17.0.197/km_core_actions_dispose@Base 17.0.199/' linux/debian/libkeymancore1.symbols + git add linux/debian/libkeymancore1.symbols + git commit -m "API method changed in master branch" + echo "readme" > README.md + git add README.md + git commit -m "Some change on master" + git checkout chore + + # merge master into chore + git merge --strategy-option=ours master + + echo "## Calling API verification" + pwd + GIT_SHA="$(git rev-parse HEAD)" + GIT_BASE="${base_sha}" + + output=$(check_updated_version_number) + echo "${output[*]}" # for logging + [[ "${output[*]}" == *"OK: libkeymancore1.symbols file got updated with package version number"* ]] +} + + +fixture_setup +echo "(test logs are in /tmp/.log)" +run_test test_compare_versions__less_patch +run_test test_compare_versions__less_minor +run_test test_compare_versions__less_major +run_test test_compare_versions__greater +run_test test_compare_versions__same +run_test test_compare_versions__greater0 +run_test test_get_highest_version_in_symbols_file +run_test test_check_updated_version_number__LineInsertedInBranch_OK diff --git a/linux/scripts/verify_api.inc.sh b/linux/scripts/verify_api.inc.sh index 63072bc4f7c..adaa9187c27 100644 --- a/linux/scripts/verify_api.inc.sh +++ b/linux/scripts/verify_api.inc.sh @@ -79,17 +79,70 @@ check_updated_version_number() { # If only lines got removed we basically skip this test. A later check will # test that the API version got updated. output_ok "${PKG_NAME}.symbols file did change but only removed lines" - elif ! git log -p -1 -- "debian/${PKG_NAME}.symbols" | grep -q "${VERSION}"; then - output_error "${PKG_NAME}.symbols file got changed without changing the package version number of the symbol" - EXIT_CODE=1 else - output_ok "${PKG_NAME}.symbols file got updated with package version number" + local version_base version_head + version_head=$(get_highest_version_in_symbols_file "${GIT_SHA}") + version_base=$(get_highest_version_in_symbols_file "${GIT_BASE}") + if (( $(compare_versions "${version_head}" "${version_base}") > 0 )); then + output_ok "${PKG_NAME}.symbols file got updated with package version number" + else + output_error "${PKG_NAME}.symbols file got changed without changing the package version number of the symbol" + EXIT_CODE=1 + fi fi else output_ok "${PKG_NAME}.symbols file didn't change" fi } +compare_versions() { + local first_parts second_parts + IFS='.' read -r -a first_parts <<< "$1" + IFS='.' read -r -a second_parts <<< "$2" + if (( first_parts[0] < second_parts[0] )); then + echo -1 + elif (( first_parts[0] > second_parts[0] )); then + echo 1 + elif (( first_parts[1] < second_parts[1] )); then + echo -1 + elif (( first_parts[1] > second_parts[1] )); then + echo 1 + elif (( first_parts[2] < second_parts[2] )); then + echo -1 + elif (( first_parts[2] > second_parts[2] )); then + echo 1 + else + echo 0 + fi +} + +get_highest_version_in_symbols_file() { + local sha="$1" + local symbol_lines line line_version + local max_version=0 + + tmpfile=$(mktemp) + if ! git cat-file blob "${sha}:linux/debian/${PKG_NAME}.symbols" > "${tmpfile}" 2>/dev/null; then + rm "${tmpfile}" + return 1 + fi + + # Start with fourth line which is where symbols start + mapfile -s 3 symbol_lines < "${tmpfile}" + for line in "${symbol_lines[@]}"; do + # km_core_actions_dispose@Base 17.0.197 + line_version=${line##* } + if (( $(compare_versions "${line_version}" "${max_version}") > 0 )); then + max_version="${line_version}" + fi + done + + echo "${max_version}" + + rm "${tmpfile}" + return 0 +} + get_api_version_in_symbols_file() { # Retrieve symbols file at commit $1 and extract "1" from # "libkeymancore.so.1 libkeymancore1 #MINVER#" From e81e2f7fc751e85b82e793a6b324f2e1648af488 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Wed, 9 Oct 2024 14:01:46 -0400 Subject: [PATCH 16/21] auto: increment master version to 18.0.125 --- HISTORY.md | 6 ++++++ VERSION.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index f472d180844..8f140893043 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # Keyman Version History +## 18.0.124 alpha 2024-10-09 + +* chore(common): fix links in minimum-versions.md (#12507) +* fix(developer): use richedit in debug memo to support Egyptian cartouches (#12464) +* feat(android): Add controls for auto-correct (#12443) + ## 18.0.123 alpha 2024-10-08 * chore(developer,common): deps: replace xml2js with fast-xml-parser (#12502) diff --git a/VERSION.md b/VERSION.md index 759872df52a..f95211c71ee 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.124 \ No newline at end of file +18.0.125 \ No newline at end of file From c4cebd124bb477c72a6b19b35588894cb9f58fdf Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 9 Oct 2024 20:32:55 +0200 Subject: [PATCH 17/21] chore(linux): Improve output of API Verification - remove useless branch name from name (this is always `master` due to the way the workflow gets triggered) - add PR# to step summary so that it's visible on the Summary page - fix PR check message to say "API verification" instead of "Package build" --- .github/workflows/api-verification.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/api-verification.yml b/.github/workflows/api-verification.yml index 6eeccdaccb0..de7e967b24f 100644 --- a/.github/workflows/api-verification.yml +++ b/.github/workflows/api-verification.yml @@ -1,5 +1,4 @@ name: "API Verification" -run-name: "API Verification for ${{ github.ref_name }}" on: workflow_run: workflows: [Ubuntu packaging] @@ -68,6 +67,8 @@ jobs: - name: "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }})" run: | + echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }})" >> $GITHUB_STEP_SUMMARY + BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb") cd ${{ github.workspace }}/keyman/linux ./scripts/deb-packaging.sh \ @@ -94,19 +95,19 @@ jobs: if: needs.api_verification.result == 'success' run: | echo "RESULT=success" >> $GITHUB_ENV - echo "MSG=Package build succeeded" >> $GITHUB_ENV + echo "MSG=API verification succeeded" >> $GITHUB_ENV - name: Set cancelled if: needs.api_verification.result == 'cancelled' run: | echo "RESULT=error" >> $GITHUB_ENV - echo "MSG=Package build cancelled" >> $GITHUB_ENV + echo "MSG=API verification cancelled" >> $GITHUB_ENV - name: Set failure if: needs.api_verification.result == 'failure' run: | echo "RESULT=failure" >> $GITHUB_ENV - echo "MSG=Package build failed" >> $GITHUB_ENV + echo "MSG=API verification failed" >> $GITHUB_ENV - name: Set final status run: | From 9885a477ecb705d5c52602f0bcffd7ffabf55caf Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 9 Oct 2024 20:18:03 +0200 Subject: [PATCH 18/21] refactor(web): address code review comments --- common/web/types/.c8rc.json | 12 +++++++ common/web/types/build.sh | 16 ++------- common/web/types/src/keyboard-object.ts | 34 +++++-------------- common/web/types/src/main.ts | 3 +- .../prediction/languageProcessor.interface.ts | 3 +- .../src/prediction/predictionContext.ts | 5 +-- .../engine/js-processor/src/outputTarget.ts | 3 +- web/src/engine/keyboard/src/defaultRules.ts | 3 +- web/src/engine/keyboard/src/index.ts | 3 +- .../engine/keyboard/src/keyboards/keyboard.ts | 32 ++++++++++++++--- .../keyboard}/src/outputTarget.interface.ts | 0 11 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 common/web/types/.c8rc.json rename {common/web/types => web/src/engine/keyboard}/src/outputTarget.interface.ts (100%) diff --git a/common/web/types/.c8rc.json b/common/web/types/.c8rc.json new file mode 100644 index 00000000000..f7137ed999b --- /dev/null +++ b/common/web/types/.c8rc.json @@ -0,0 +1,12 @@ +{ + "exclude": [ + "src/keyboard-object.ts", + "src/lexical-model-types.ts", + "src/outputTarget.interface.ts", + "src/*.d.ts", + "src/main.ts", + "src/schemas/*", + "src/schema-validators.ts", + "src/schemas.ts" + ] +} diff --git a/common/web/types/build.sh b/common/web/types/build.sh index 0d7abba7e98..3f7b48f8fcb 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -84,21 +84,11 @@ function do_test() { tsc --build test readonly C8_THRESHOLD=60 - # Exclude files from coverage analysis that only define types: - exclude=(\ - src/keyboard-object.ts \ - src/lexical-model-types.ts \ - src/outputTarget.interface.ts \ - src/*.d.ts \ - src/main.ts \ - src/schemas/* \ - src/schema-validators.ts \ - src/schemas.ts \ - ) - # shellcheck disable=SC2068 + + # Excludes are defined in .c8rc.json c8 --skip-full --reporter=lcov --reporter=text --lines "${C8_THRESHOLD}" \ --statements "${C8_THRESHOLD}" --branches "${C8_THRESHOLD}" \ - --functions "${C8_THRESHOLD}" ${exclude[@]/#/--exclude } \ + --functions "${C8_THRESHOLD}" \ mocha "${builder_extra_params[@]}" builder_echo warning "Coverage thresholds are currently ${C8_THRESHOLD}%, which is lower than ideal." diff --git a/common/web/types/src/keyboard-object.ts b/common/web/types/src/keyboard-object.ts index dd6f2446729..afd0029af0d 100644 --- a/common/web/types/src/keyboard-object.ts +++ b/common/web/types/src/keyboard-object.ts @@ -1,23 +1,15 @@ /* * Keyman is copyright (C) SIL Global. MIT License. */ -import { OutputTarget } from './outputTarget.interface.js'; import { TouchLayoutPlatform as LayoutFormFactorSpec } from './keyman-touch-layout/keyman-touch-layout-file.js'; export type ComplexKeyboardStore = (string | { t: 'd', d: number } | { ['t']: 'b' })[]; -type KeyEvent = {}; +// A stub for KeyEvent which is properly defined in KeymanWeb +type KeyEventStub = {}; -/** - * Stores preprocessed properties of a keyboard for quick retrieval later. - */ -export class CacheTag { - stores: { [storeName: string]: ComplexKeyboardStore }; - - constructor() { - this.stores = {}; - } -} +// A stub for OutputTarget which is properly defined in KeymanWeb +type OutputTargetStub = {}; export interface EncodedVisualKeyboard { /** Represents CSS font styling to use for VisualKeyboard text */ @@ -47,11 +39,6 @@ export type LayoutSpec = { } export type KeyboardObject = { - /** - * Used internally by Keyman Engine for Web to hold preprocessed stores. - */ - _kmw?: CacheTag; - /** * group-start: the function triggering processing for the keyboard's * "Unicode" start group, corresponding to `begin Unicode > use(_____)` in @@ -60,7 +47,7 @@ export type KeyboardObject = { * @param keystroke The full, pre-processed keystroke triggering * keyboard-rule application. */ - gs(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; + gs(outputTarget: OutputTargetStub, keystroke: KeyEventStub): boolean; /** * group-newcontext: the function triggering processing for the keyboard's @@ -69,7 +56,7 @@ export type KeyboardObject = { * @param outputTarget The new context to be used with future keystrokes * @param keystroke A 'null' `KeyEvent` providing current modifier + state information. */ - gn?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; + gn?(outputTarget: OutputTargetStub, keystroke: KeyEventStub): boolean; /** * group-postkeystroke: the function triggering processing for the keyboard's @@ -80,7 +67,7 @@ export type KeyboardObject = { * applied. * @param keystroke A 'null' `KeyEvent` providing current modifier + state information. */ - gpk?(outputTarget: OutputTarget, keystroke: KeyEvent): boolean; + gpk?(outputTarget: OutputTargetStub, keystroke: KeyEventStub): boolean; /** * Keyboard ID: the uniquely-identifying name for this keyboard. Includes the standard @@ -169,11 +156,6 @@ export type KeyboardObject = { * touch layouts if set and no touch-layout information is available. */ KDU?: number; - /** - * Virtual Key Dictionary: the engine pre-processed, unminified dictionary. This is built within - * Keyman Engine for Web at runtime as needed based on the definitions in `KVKD`. - */ - VKDictionary?: Record, /** * Keyboard Help File: Embedded JS script designed for use with a keyboard's * HTML help text. Always defined within the file referenced by &kmw_embedjs @@ -193,6 +175,6 @@ export type KeyboardObject = { * @param {number} _PData 1 or 0 * @returns */ - KNS?: (_PCommand: number, _PTarget: OutputTarget, _PData: number) => void; + KNS?: (_PCommand: number, _PTarget: OutputTargetStub, _PData: number) => void; } & Record<`s${number}`, string> diff --git a/common/web/types/src/main.ts b/common/web/types/src/main.ts index eb0b8a7596f..bd1cafbf57f 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -32,5 +32,4 @@ export { ElementString } from './kmx/kmx-plus/element-string.js'; export { USVString, CasingForm, CasingFunction, TextWithProbability, LexiconTraversal, LexicalModel, LexicalModelPunctuation, Transform, Suggestion, Reversion, Keep, SuggestionTag, Context, Distribution, Outcome, WithOutcome, ProbabilityMass, Configuration, Capabilities, WordBreakingFunction, Span } from './lexical-model-types.js'; -export { CacheTag, ComplexKeyboardStore, EncodedVisualKeyboard, LayoutSpec, KeyboardObject } from './keyboard-object.js'; -export { OutputTarget } from './outputTarget.interface.js'; +export { ComplexKeyboardStore, EncodedVisualKeyboard, LayoutSpec, KeyboardObject } from './keyboard-object.js'; diff --git a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts index 52ed1ca5104..971566058e0 100644 --- a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts +++ b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts @@ -1,5 +1,6 @@ -import { OutputTarget, Suggestion, Reversion } from '@keymanapp/common-types'; +import { Suggestion, Reversion } from '@keymanapp/common-types'; import { EventEmitter } from "eventemitter3"; +import { OutputTarget } from 'keyman/engine/keyboard'; export class ReadySuggestions { suggestions: Suggestion[]; diff --git a/web/src/engine/interfaces/src/prediction/predictionContext.ts b/web/src/engine/interfaces/src/prediction/predictionContext.ts index 67c433d3f23..943160eb9db 100644 --- a/web/src/engine/interfaces/src/prediction/predictionContext.ts +++ b/web/src/engine/interfaces/src/prediction/predictionContext.ts @@ -1,6 +1,7 @@ import { EventEmitter } from "eventemitter3"; -import { Keep, type OutputTarget, Reversion, Suggestion } from '@keymanapp/common-types'; -import { type LanguageProcessorSpec , ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js'; +import { Keep, Reversion, Suggestion } from '@keymanapp/common-types'; +import { type LanguageProcessorSpec, ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js'; +import { type OutputTarget } from 'keyman/engine/keyboard'; interface PredictionContextEventMap { update: (suggestions: Suggestion[]) => void; diff --git a/web/src/engine/js-processor/src/outputTarget.ts b/web/src/engine/js-processor/src/outputTarget.ts index 29620466928..c055e47602e 100644 --- a/web/src/engine/js-processor/src/outputTarget.ts +++ b/web/src/engine/js-processor/src/outputTarget.ts @@ -1,13 +1,14 @@ import { extendString } from "@keymanapp/web-utils"; import { findCommonSubstringEndIndex } from "./stringDivergence.js"; import { Mock } from "./mock.js"; +import { OutputTarget as OutputTargetInterface } from 'keyman/engine/keyboard'; extendString(); // Defines deadkey management in a manner attachable to each element interface. import { type KeyEvent } from 'keyman/engine/keyboard'; import { Deadkey, DeadkeyTracker } from "./deadkeys.js"; -import { OutputTarget as OutputTargetInterface, ProbabilityMass, Transform } from '@keymanapp/common-types'; +import { ProbabilityMass, Transform } from '@keymanapp/common-types'; // Also relies on string-extensions provided by the web-utils package. diff --git a/web/src/engine/keyboard/src/defaultRules.ts b/web/src/engine/keyboard/src/defaultRules.ts index c1e8c979681..b6ad708fd96 100644 --- a/web/src/engine/keyboard/src/defaultRules.ts +++ b/web/src/engine/keyboard/src/defaultRules.ts @@ -4,9 +4,10 @@ * Implementation of default rules */ -import { ModifierKeyConstants, type OutputTarget } from '@keymanapp/common-types'; +import { ModifierKeyConstants } from '@keymanapp/common-types'; import Codes from './codes.js'; import type KeyEvent from './keyEvent.js'; +import { type OutputTarget } from './outputTarget.interface.js'; export enum EmulationKeystrokes { Enter = '\n', diff --git a/web/src/engine/keyboard/src/index.ts b/web/src/engine/keyboard/src/index.ts index 830e37fa907..28b6331c219 100644 --- a/web/src/engine/keyboard/src/index.ts +++ b/web/src/engine/keyboard/src/index.ts @@ -31,11 +31,12 @@ export * from "./defaultRules.js"; export { default as KeyEvent } from "./keyEvent.js"; export * from "./keyEvent.js"; export { default as KeyMapping } from "./keyMapping.js"; +export { OutputTarget } from "./outputTarget.interface.js"; export * from "@keymanapp/web-utils"; // At the top level, there should be no default export. -// Without the line below... KeyMapping would likely be aliased there, as it's +// Without the line below... OutputTarget would likely be aliased there, as it's // the last `export { default as _ }` => `export * from` pairing seen above. export default undefined; diff --git a/web/src/engine/keyboard/src/keyboards/keyboard.ts b/web/src/engine/keyboard/src/keyboards/keyboard.ts index 01e584a36d5..0d58f467067 100644 --- a/web/src/engine/keyboard/src/keyboards/keyboard.ts +++ b/web/src/engine/keyboard/src/keyboards/keyboard.ts @@ -2,12 +2,24 @@ import Codes from "../codes.js"; import { Layouts } from "./defaultLayouts.js"; import { ActiveKey, ActiveLayout, ActiveSubKey } from "./activeLayout.js"; import KeyEvent from "../keyEvent.js"; -import { CacheTag, ComplexKeyboardStore, KeyboardObject, LayoutSpec, ModifierKeyConstants, type OutputTarget, TouchLayout } from "@keymanapp/common-types"; +import { type OutputTarget } from '../outputTarget.interface.js'; +import { ComplexKeyboardStore, KeyboardObject, LayoutSpec, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; type TouchLayoutSpec = TouchLayout.TouchLayoutPlatform & { isDefault?: boolean}; import { Version, DeviceSpec } from "@keymanapp/web-utils"; import StateKeyMap from "./stateKeyMap.js"; +/** + * Stores preprocessed properties of a keyboard for quick retrieval later. + */ +class CacheTag { + stores: { [storeName: string]: ComplexKeyboardStore }; + + constructor() { + this.stores = {}; + } +} + export enum LayoutState { NOT_LOADED = undefined, POLYFILLED = 1, @@ -18,13 +30,25 @@ export interface VariableStoreDictionary { [name: string]: string; }; +type KmwKeyboardObject = KeyboardObject & { + /** + * Used internally by Keyman Engine for Web to hold preprocessed stores. + */ + _kmw?: CacheTag; + /** + * Virtual Key Dictionary: the engine pre-processed, unminified dictionary. This is built within + * Keyman Engine for Web at runtime as needed based on the definitions in `KVKD`. + */ + VKDictionary?: Record, +}; + /** * Acts as a wrapper class for Keyman keyboards compiled to JS, providing type information * and keyboard-centered functionality in an object-oriented way without modifying the * wrapped keyboard itself. */ export default class Keyboard { - public static DEFAULT_SCRIPT_OBJECT: KeyboardObject = { + public static DEFAULT_SCRIPT_OBJECT: KmwKeyboardObject = { 'gs': function(outputTarget: OutputTarget, keystroke: KeyEvent) { return false; }, // no matching rules; rely on defaultRuleOutput entirely 'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this. 'KN': '', @@ -38,7 +62,7 @@ export default class Keyboard { * * TODO: Make this private instead. But there are a LOT of references that must be rooted out first. */ - public readonly scriptObject: KeyboardObject; + public readonly scriptObject: KmwKeyboardObject; private layoutStates: {[layout: string]: LayoutState}; constructor(keyboardScript: any) { @@ -544,7 +568,7 @@ export default class Keyboard { * @return {number} key code > 255 on success, or 0 if not found */ getVKDictionaryCode(keyName: string) { - const dict = this.scriptObject['VKDictionary'] || {} as KeyboardObject['VKDictionary']; + const dict = this.scriptObject['VKDictionary'] || {} as KmwKeyboardObject['VKDictionary']; if(!this.scriptObject['VKDictionary']) { if(typeof this.scriptObject['KVKD'] == 'string') { // Build the VK dictionary diff --git a/common/web/types/src/outputTarget.interface.ts b/web/src/engine/keyboard/src/outputTarget.interface.ts similarity index 100% rename from common/web/types/src/outputTarget.interface.ts rename to web/src/engine/keyboard/src/outputTarget.interface.ts From 4f3a68494cb9263398a486a8b539de936e472806 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 8 Oct 2024 18:30:22 +0200 Subject: [PATCH 19/21] refactor(web): address code review comments --- common/web/types/build.sh | 8 ++------ common/web/types/src/main.ts | 2 +- web/src/app/browser/src/keymanEngine.ts | 3 ++- web/src/engine/keyboard/src/keyboards/defaultLayouts.ts | 5 ++++- web/src/engine/keyboard/src/keyboards/keyboard.ts | 9 +++++++-- web/src/engine/main/src/keyboardInterface.ts | 4 +++- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/common/web/types/build.sh b/common/web/types/build.sh index 3f7b48f8fcb..7dac43dba81 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -86,12 +86,8 @@ function do_test() { readonly C8_THRESHOLD=60 # Excludes are defined in .c8rc.json - c8 --skip-full --reporter=lcov --reporter=text --lines "${C8_THRESHOLD}" \ - --statements "${C8_THRESHOLD}" --branches "${C8_THRESHOLD}" \ - --functions "${C8_THRESHOLD}" \ - mocha "${builder_extra_params[@]}" - - builder_echo warning "Coverage thresholds are currently ${C8_THRESHOLD}%, which is lower than ideal." + c8 -skip-full --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha "${builder_extra_params[@]}" + builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal." builder_echo warning "Please increase threshold in build.sh as test coverage improves." } diff --git a/common/web/types/src/main.ts b/common/web/types/src/main.ts index bd1cafbf57f..c3b24c5123d 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -32,4 +32,4 @@ export { ElementString } from './kmx/kmx-plus/element-string.js'; export { USVString, CasingForm, CasingFunction, TextWithProbability, LexiconTraversal, LexicalModel, LexicalModelPunctuation, Transform, Suggestion, Reversion, Keep, SuggestionTag, Context, Distribution, Outcome, WithOutcome, ProbabilityMass, Configuration, Capabilities, WordBreakingFunction, Span } from './lexical-model-types.js'; -export { ComplexKeyboardStore, EncodedVisualKeyboard, LayoutSpec, KeyboardObject } from './keyboard-object.js'; +export * as KeymanWebKeyboard from './keyboard-object.js'; diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 2dcab2730e8..3c30740e81b 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -1,4 +1,4 @@ -import { KeyboardObject } from '@keymanapp/common-types'; +import { KeymanWebKeyboard } from '@keymanapp/common-types'; import { KeymanEngine as KeymanEngineBase, DeviceDetector } from 'keyman/engine/main'; import { getAbsoluteY } from 'keyman/engine/dom-utils'; import { OutputTarget } from 'keyman/engine/element-wrappers'; @@ -8,6 +8,7 @@ import { } from 'keyman/engine/osk'; import { ErrorStub, KeyboardStub, CloudQueryResult, toPrefixedKeyboardId as prefixed } from 'keyman/engine/keyboard-storage'; import { DeviceSpec, Keyboard } from "keyman/engine/keyboard"; +import KeyboardObject = KeymanWebKeyboard.KeyboardObject; import * as views from './viewsAnchorpoint.js'; import { BrowserConfiguration, BrowserInitOptionDefaults, BrowserInitOptionSpec } from './configuration.js'; diff --git a/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts b/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts index 9de871b4486..4b08f4bef34 100644 --- a/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts +++ b/web/src/engine/keyboard/src/keyboards/defaultLayouts.ts @@ -4,7 +4,10 @@ ***/ import { Version, deepCopy } from "@keymanapp/web-utils"; -import { EncodedVisualKeyboard, LayoutSpec, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; +import { KeymanWebKeyboard, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; + +import EncodedVisualKeyboard = KeymanWebKeyboard.EncodedVisualKeyboard; +import LayoutSpec = KeymanWebKeyboard.LayoutSpec; import LayoutFormFactorSpec = TouchLayout.TouchLayoutPlatform; import LayoutLayerBase = TouchLayout.TouchLayoutLayer; diff --git a/web/src/engine/keyboard/src/keyboards/keyboard.ts b/web/src/engine/keyboard/src/keyboards/keyboard.ts index 0d58f467067..753ae3bd55b 100644 --- a/web/src/engine/keyboard/src/keyboards/keyboard.ts +++ b/web/src/engine/keyboard/src/keyboards/keyboard.ts @@ -2,8 +2,13 @@ import Codes from "../codes.js"; import { Layouts } from "./defaultLayouts.js"; import { ActiveKey, ActiveLayout, ActiveSubKey } from "./activeLayout.js"; import KeyEvent from "../keyEvent.js"; -import { type OutputTarget } from '../outputTarget.interface.js'; -import { ComplexKeyboardStore, KeyboardObject, LayoutSpec, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; +import { type OutputTarget } from "../outputTarget.interface.js"; +import { KeymanWebKeyboard, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types"; + +import ComplexKeyboardStore = KeymanWebKeyboard.ComplexKeyboardStore; +import KeyboardObject = KeymanWebKeyboard.KeyboardObject; +import LayoutSpec = KeymanWebKeyboard.LayoutSpec; + type TouchLayoutSpec = TouchLayout.TouchLayoutPlatform & { isDefault?: boolean}; import { Version, DeviceSpec } from "@keymanapp/web-utils"; diff --git a/web/src/engine/main/src/keyboardInterface.ts b/web/src/engine/main/src/keyboardInterface.ts index aca05fee586..df1a24aa5d6 100644 --- a/web/src/engine/main/src/keyboardInterface.ts +++ b/web/src/engine/main/src/keyboardInterface.ts @@ -1,7 +1,9 @@ -import { KeyboardObject } from '@keymanapp/common-types'; +import { KeymanWebKeyboard } from '@keymanapp/common-types'; import { KeyboardInterface as KeyboardInterfaceBase } from 'keyman/engine/js-processor'; import { KeyboardStub, RawKeyboardStub, toUnprefixedKeyboardId as unprefixed } from 'keyman/engine/keyboard-storage'; +import KeyboardObject = KeymanWebKeyboard.KeyboardObject; + import { ContextManagerBase } from './contextManagerBase.js'; import { VariableStoreCookieSerializer } from "./variableStoreCookieSerializer.js"; import KeymanEngine from "./keymanEngine.js"; From d849b30856e4b388f4f3ed79dd6fbcec8c57b37a Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 10 Oct 2024 09:13:27 +0200 Subject: [PATCH 20/21] refactor(web): reverting unnecessary busy changes --- .../interfaces/src/prediction/languageProcessor.interface.ts | 2 +- web/src/engine/interfaces/src/prediction/predictionContext.ts | 4 ++-- web/src/engine/keyboard/src/defaultRules.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts index 971566058e0..303fa99fc3d 100644 --- a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts +++ b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts @@ -1,6 +1,6 @@ import { Suggestion, Reversion } from '@keymanapp/common-types'; import { EventEmitter } from "eventemitter3"; -import { OutputTarget } from 'keyman/engine/keyboard'; +import { OutputTarget } from "keyman/engine/keyboard"; export class ReadySuggestions { suggestions: Suggestion[]; diff --git a/web/src/engine/interfaces/src/prediction/predictionContext.ts b/web/src/engine/interfaces/src/prediction/predictionContext.ts index 943160eb9db..758ef8f46b2 100644 --- a/web/src/engine/interfaces/src/prediction/predictionContext.ts +++ b/web/src/engine/interfaces/src/prediction/predictionContext.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "eventemitter3"; import { Keep, Reversion, Suggestion } from '@keymanapp/common-types'; -import { type LanguageProcessorSpec, ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js'; -import { type OutputTarget } from 'keyman/engine/keyboard'; +import { type LanguageProcessorSpec , ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js'; +import { type OutputTarget } from "keyman/engine/keyboard"; interface PredictionContextEventMap { update: (suggestions: Suggestion[]) => void; diff --git a/web/src/engine/keyboard/src/defaultRules.ts b/web/src/engine/keyboard/src/defaultRules.ts index b6ad708fd96..5512114d937 100644 --- a/web/src/engine/keyboard/src/defaultRules.ts +++ b/web/src/engine/keyboard/src/defaultRules.ts @@ -7,7 +7,7 @@ import { ModifierKeyConstants } from '@keymanapp/common-types'; import Codes from './codes.js'; import type KeyEvent from './keyEvent.js'; -import { type OutputTarget } from './outputTarget.interface.js'; +import { type OutputTarget } from './outputTarget.interface.js'; export enum EmulationKeystrokes { Enter = '\n', From d8e916804771e6b6d5d14f239984b5665cf0b69b Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 10 Oct 2024 14:03:14 -0400 Subject: [PATCH 21/21] auto: increment master version to 18.0.126 --- HISTORY.md | 8 ++++++++ VERSION.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 8f140893043..b442c74e502 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,13 @@ # Keyman Version History +## 18.0.125 alpha 2024-10-10 + +* chore(common): allow to run `build.sh` scripts in `bashdb` debugger (#12518) +* chore(linux): Improve output of API Verification (#12522) +* chore(linux): allow to skip API change (#12519) +* fix(linux): fix problem with API checks with merge commits (#12520) +* refactor(web): move `KeyboardObject` type to `common/web/types` (#12514) + ## 18.0.124 alpha 2024-10-09 * chore(common): fix links in minimum-versions.md (#12507) diff --git a/VERSION.md b/VERSION.md index f95211c71ee..689f7b264ea 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.125 \ No newline at end of file +18.0.126 \ No newline at end of file