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: | diff --git a/HISTORY.md b/HISTORY.md index 9439ba6224d..b442c74e502 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,24 @@ # 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) +* 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) +* 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..689f7b264ea 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.123 \ No newline at end of file +18.0.126 \ No newline at end of file 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..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 @@ -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; @@ -169,8 +170,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 != 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 75116bad8cc..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 @@ -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,48 @@ 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); - 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)); + // Initialize Radio button group change listeners + RadioGroup radioGroup = (RadioGroup) findViewById(R.id.suggestion_radio_group); + radioGroup.clearCheck(); - layout = (RelativeLayout)findViewById(R.id.predictions_toggle); + 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); - 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)); + radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, @IdRes int checkId) { + RadioButton checkedButton = (RadioButton)radioGroup.findViewById(checkId); + int index = radioGroup.indexOfChild(checkedButton); + KMManager.setMaySuggest(lgCode, KMManager.SuggestionType.fromInt(index)); + + // 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..21f783ffc55 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -229,11 +229,23 @@ function deregisterModel(modelID) { keyman.removeModel(modelID); } -function enableSuggestions(model, mayPredict, mayCorrect) { +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. - keyman.core.languageProcessor.mayPredict = mayPredict; - keyman.core.languageProcessor.mayCorrect = mayCorrect; + // Use console_debug + console_debug('enableSuggestions(model, maySuggest='+suggestionType+')'); + 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]; 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..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 @@ -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; @@ -165,7 +166,9 @@ 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) + != 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 592f4be2a15..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 @@ -192,6 +192,35 @@ 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() { + return this.ordinal(); + } + } + protected static InputMethodService IMService; private static boolean debugMode = false; @@ -220,6 +249,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 +345,8 @@ public enum EnterModeType { public static final int KMMinimum_LongpressDelay = 300; public static final int KMMaximum_LongpressDelay = 1500; + // 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"; @@ -707,6 +739,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 +1386,18 @@ public static boolean getMayPredictOverride() { return mayPredictOverride; } + /** + * Store SuggestionType as int in preference + * @param languageID as String + * @param suggestType SuggestionType + */ + 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.toInt()); + editor.commit(); + } + /** * Determines if the InputType field is a numeric field * @param inputType @@ -1462,24 +1510,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); - boolean mayPredict = (mayPredictOverride) ? false : - prefs.getBoolean(getLanguagePredictionPreferenceKey(languageID), true); - boolean mayCorrect = prefs.getBoolean(getLanguageCorrectionPreferenceKey(languageID), true); + 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, %s)", model, mayPredict, mayCorrect)); + 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, %s)", model, mayPredict, mayCorrect)); + SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %d)", model, 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 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 7141bc10720..7dac43dba81 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -82,7 +82,10 @@ function do_configure() { function do_test() { eslint . tsc --build test - readonly C8_THRESHOLD=50 + + 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." 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..afd0029af0d --- /dev/null +++ b/common/web/types/src/keyboard-object.ts @@ -0,0 +1,180 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { TouchLayoutPlatform as LayoutFormFactorSpec } from './keyman-touch-layout/keyman-touch-layout-file.js'; + +export type ComplexKeyboardStore = (string | { t: 'd', d: number } | { ['t']: 'b' })[]; + +// A stub for KeyEvent which is properly defined in KeymanWeb +type KeyEventStub = {}; + +// A stub for OutputTarget which is properly defined in KeymanWeb +type OutputTargetStub = {}; + +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 = { + /** + * 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: OutputTargetStub, keystroke: KeyEventStub): 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: OutputTargetStub, keystroke: KeyEventStub): 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: OutputTargetStub, keystroke: KeyEventStub): 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; + /** + * 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: 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 df21b0fed35..c3b24c5123d 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -31,3 +31,5 @@ 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 * as KeymanWebKeyboard from './keyboard-object.js'; 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 fa062e68628..3cd9397b212 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 @@ -22,9 +22,13 @@ interface uses System.Classes, Winapi.Messages, + Winapi.RichEdit, Winapi.Windows, Vcl.Controls, - Vcl.StdCtrls; + Vcl.ComCtrls, + Vcl.StdCtrls, + + Keyman.Developer.UI.RichEdit41; type TKeymanDeveloperDebuggerMessageEvent = procedure(Sender: TObject; var Message: TMessage; var Handled: Boolean) of object; @@ -34,10 +38,11 @@ TMemoSelection = record Anchor: Integer; end; - TKeymanDeveloperDebuggerMemo = class(TMemo) + TKeymanDeveloperDebuggerMemo = class(TRichEdit41) private FOnMessage: TKeymanDeveloperDebuggerMessageEvent; FAllowUnicodeInput: Boolean; + FSelectionChanging: Boolean; FIsDebugging: Boolean; procedure SetAllowUnicode(const Value: Boolean); function GetSelection: TMemoSelection; @@ -52,6 +57,7 @@ TKeymanDeveloperDebuggerMemo = class(TMemo) 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; @@ -77,14 +83,21 @@ constructor TKeymanDeveloperDebuggerMemo.Create(AOwner: TComponent); begin FAllowUnicodeInput := True; inherited Create(AOwner); + PlainText := True; 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; @@ -93,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 a00fdb9aeae..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,19 +835,29 @@ 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; 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..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 @@ -1354,11 +1355,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; 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 diff --git a/docs/minimum-versions.md b/docs/minimum-versions.md index 2b7a65b300a..9c0af391628 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 diff --git a/linux/scripts/deb-packaging.sh b/linux/scripts/deb-packaging.sh index 22840396d54..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,258 +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() { - # 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..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 @@ -224,10 +142,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 @@ -247,12 +165,13 @@ 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 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" @@ -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,12 +261,13 @@ 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 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" @@ -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 new file mode 100644 index 00000000000..adaa9187c27 --- /dev/null +++ b/linux/scripts/verify_api.inc.sh @@ -0,0 +1,311 @@ +#!/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" + else + 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#" + 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}" +} 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() { diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 0df40efe5d3..3c30740e81b 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -1,3 +1,4 @@ +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'; @@ -6,7 +7,8 @@ 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 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 3b97659d4bb..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 { 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; @@ -19,27 +22,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 +39,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..753ae3bd55b 100644 --- a/web/src/engine/keyboard/src/keyboards/keyboard.ts +++ b/web/src/engine/keyboard/src/keyboards/keyboard.ts @@ -1,21 +1,24 @@ 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 { 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"; 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}; + stores: { [storeName: string]: ComplexKeyboardStore }; constructor() { this.stores = {}; @@ -32,156 +35,17 @@ export interface VariableStoreDictionary { [name: string]: string; }; -export type KeyboardObject = { +type KmwKeyboardObject = 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 @@ -189,7 +53,7 @@ export type KeyboardObject = { * 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': '', @@ -203,7 +67,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) { @@ -709,7 +573,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/web/src/engine/keyboard/src/outputTarget.interface.ts b/web/src/engine/keyboard/src/outputTarget.interface.ts index 410094c812c..34faa5667aa 100644 --- a/web/src/engine/keyboard/src/outputTarget.interface.ts +++ b/web/src/engine/keyboard/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/engine/main/src/keyboardInterface.ts b/web/src/engine/main/src/keyboardInterface.ts index 83f3cf0e624..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 "keyman/engine/keyboard"; +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";