diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 033055f46d..0ab3a071d2 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "com.tavultesoft.kmapro" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 28 //println "===DUMPING PROPERTIES===" diff --git a/android/KMEA/app/src/main/assets/keyboard.html b/android/KMEA/app/src/main/assets/keyboard.html index 8a4e5adf3c..a0eebfa92a 100644 --- a/android/KMEA/app/src/main/assets/keyboard.html +++ b/android/KMEA/app/src/main/assets/keyboard.html @@ -12,6 +12,7 @@ var device = window.jsInterface.getDeviceType(); var oskHeight = window.jsInterface.getKeyboardHeight(); var oskWidth = 0; + var fragmentToggle = 0; window.addEventListener('load', init, false); function init() { @@ -39,6 +40,18 @@ kmw.addEventListener('keyboardloaded', setIsChiral); kmw.addEventListener('keyboardchange', setIsChiral); kmw.addEventListener('mm.modelchange', onModelChange); + + notifyHost('pageLoaded'); + } + + function notifyHost(event, params) { + // TODO: Update all other host notifications to use notifyHost instead of directly setting window.location.hash + window.setTimeout(function() { + // We use a timeout so that the navigation doesn't cause the calling function to abort after the call + fragmentToggle = (fragmentToggle + 1) % 100; + params = params ? '+'+params : ''; + window.location.hash = event+'-'+fragmentToggle+params; + }, 10); } // Update the KMW banner height @@ -211,7 +224,6 @@ kmw['setActiveElement'](ta); } - var fragmentToggle = 0; function oskCreateKeyPreview(x,y,w,h,t) { fragmentToggle = (fragmentToggle + 1) % 100; var div = document.createElement('div'); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java index cb438e3dd8..3a5d26d555 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java @@ -62,6 +62,8 @@ final class KMKeyboard extends WebView { private String keyboardName; private String keyboardVersion; + protected ArrayList javascriptAfterLoad = new ArrayList(); + private static String currentKeyboard = null; private static String txtFont = ""; private static String oskFont = null; @@ -136,12 +138,16 @@ final class KMKeyboard extends WebView { public boolean onConsoleMessage(ConsoleMessage cm) { if (KMManager.isDebugMode()) { Log.d("KMEA", "Keyman JS Log: Line " + cm.lineNumber() + ", " + cm.sourceId() + ":" + cm.message()); + if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { + Log.e("KMEA", "Keyman JS Log: ERROR"); + } } // Send console errors to Firebase Analytics. // (Ignoring spurious message "No keyboard stubs exist = ...") // TODO: Analyze if this error warrants reverting to default keyboard // TODO: Fix base error rather than trying to ignore it "No keyboard stubs exist" + if ((cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) && (!cm.message().startsWith("No keyboard stubs exist"))) { sendKMWError(cm.lineNumber(), cm.sourceId(), cm.message()); Toast.makeText(context, "Fatal Error with " + currentKeyboard + @@ -151,6 +157,7 @@ final class KMKeyboard extends WebView { KMManager.KMDefault_LanguageID, KMManager.KMDefault_KeyboardName, KMManager.KMDefault_LanguageName, KMManager.KMDefault_KeyboardFont, null); } + return true; } }); @@ -167,10 +174,11 @@ final class KMKeyboard extends WebView { if (subKeysList != null) { showSubKeys(context); return; - } else if(suggestionJSON != null) { + } /* For future implementation + else if(suggestionJSON != null) { showSuggestionLongpress(context); return; - } + }*/ } @Override @@ -182,20 +190,55 @@ final class KMKeyboard extends WebView { public void loadKeyboard() { keyboardSet = false; + + if(keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP) + KMManager.InAppKeyboardLoaded = false; + else + KMManager.SystemKeyboardLoaded = false; + String htmlPath = "file://" + getContext().getDir("data", Context.MODE_PRIVATE) + "/" + KMManager.KMFilename_KeyboardHtml; loadUrl(htmlPath); setBackgroundColor(0); } + public void loadJavascript(String func) { + this.javascriptAfterLoad.add(func); + + if((keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && KMManager.InAppKeyboardLoaded) || + (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM && KMManager.SystemKeyboardLoaded)) { + + if(this.javascriptAfterLoad.size() == 1) + callJavascriptAfterLoad(); + } + } + + public void callJavascriptAfterLoad() { + if(this.javascriptAfterLoad.size() > 0) { + Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + if(javascriptAfterLoad.size() > 0) { + loadUrl("javascript:" + javascriptAfterLoad.get(0)); + javascriptAfterLoad.remove(0); + if (javascriptAfterLoad.size() > 0) { + callJavascriptAfterLoad(); + } + } + } + }, 1); + } + } + public void hideKeyboard() { - String jsString = "javascript:hideKeyboard()"; - loadUrl(jsString); + String jsString = "hideKeyboard()"; + loadJavascript(jsString); } public void executeHardwareKeystroke(int code, int shift, int lstates, int eventModifiers) { - String jsFormat = "javascript:executeHardwareKeystroke(%d,%d, %d, %d)"; + String jsFormat = "executeHardwareKeystroke(%d,%d, %d, %d)"; String jsString = String.format(jsFormat, code, shift, lstates, eventModifiers); - loadUrl(jsString); + loadJavascript(jsString); } @SuppressLint("ClickableViewAccessibility") @@ -225,8 +268,8 @@ final class KMKeyboard extends WebView { DisplayMetrics dms = context.getResources().getDisplayMetrics(); int kbWidth = (int) (dms.widthPixels / dms.density); // Ensure window is loaded for javascript functions - loadUrl(String.format( - "javascript:window.onload = function(){ setOskWidth(\"%d\");"+ + loadJavascript(String.format( + "window.onload = function(){ setOskWidth(\"%d\");"+ "setOskHeight(\"0\"); };", kbWidth)); if (ShouldShowHelpBubble) { ShouldShowHelpBubble = false; @@ -234,7 +277,7 @@ final class KMKeyboard extends WebView { handler.postDelayed(new Runnable() { @Override public void run() { - loadUrl("javascript:showHelpBubble()"); + loadJavascript("showHelpBubble()"); } }, 2000); } @@ -257,15 +300,15 @@ final class KMKeyboard extends WebView { dismissKeyPreview(0); dismissSubKeysWindow(); int bannerHeight = KMManager.getBannerHeight(context); - loadUrl(String.format("javascript:setBannerHeight(%d)", bannerHeight)); - loadUrl(String.format("javascript:setOskWidth(%d)", newConfig.screenWidthDp)); - loadUrl("javascript:setOskHeight(0)"); + loadJavascript(String.format("setBannerHeight(%d)", bannerHeight)); + loadJavascript(String.format("setOskWidth(%d)", newConfig.screenWidthDp)); + loadJavascript("setOskHeight(0)"); if (dismissHelpBubble()) { Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { - loadUrl("javascript:showHelpBubble()"); + loadJavascript("showHelpBubble()"); } }, 2000); } @@ -385,9 +428,9 @@ final class KMKeyboard extends WebView { keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex. languageName = languageName.replaceAll("\'", "\\\\'"); - String jsFormat = "javascript:setKeymanLanguage('%s','%s','%s','%s','%s', %s, %s, '%s')"; + String jsFormat = "setKeymanLanguage('%s','%s','%s','%s','%s', %s, %s, '%s')"; String jsString = String.format(jsFormat, keyboardName, keyboardID, languageName, languageID, keyboardPath, tFont, oFont, packageID); - loadUrl(jsString); + loadJavascript(jsString); this.packageID = packageID; this.keyboardID = keyboardID; @@ -401,7 +444,7 @@ final class KMKeyboard extends WebView { handler.postDelayed(new Runnable() { @Override public void run() { - loadUrl("javascript:showHelpBubble()"); + loadJavascript("showHelpBubble()"); } }, 2000); } @@ -486,9 +529,9 @@ final class KMKeyboard extends WebView { keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex. languageName = languageName.replaceAll("\'", "\\\\'"); - String jsFormat = "javascript:setKeymanLanguage('%s','%s','%s','%s','%s', %s, %s, '%s')"; + String jsFormat = "setKeymanLanguage('%s','%s','%s','%s','%s', %s, %s, '%s')"; String jsString = String.format(jsFormat, keyboardName, keyboardID, languageName, languageID, keyboardPath, tFont, oFont, packageID); - loadUrl(jsString); + loadJavascript(jsString); this.packageID = packageID; this.keyboardID = keyboardID; @@ -502,7 +545,7 @@ final class KMKeyboard extends WebView { handler.postDelayed(new Runnable() { @Override public void run() { - loadUrl("javascript:showHelpBubble()"); + loadJavascript("showHelpBubble()"); } }, 2000); } @@ -712,14 +755,14 @@ final class KMKeyboard extends WebView { public void onDismiss() { suggestionJSON = null; suggestionMenuWindow = null; - String jsString = "javascript:popupVisible(0)"; - loadUrl(jsString); + String jsString = "popupVisible(0)"; + loadJavascript(jsString); } }); suggestionMenuWindow.showAtLocation(KMKeyboard.this, Gravity.TOP | Gravity.LEFT, posX , posY); - String jsString = "javascript:popupVisible(1)"; - loadUrl(jsString); + String jsString = "popupVisible(1)"; + loadJavascript(jsString); return; } @@ -822,9 +865,9 @@ final class KMKeyboard extends WebView { int index = v.getId() - 1; String keyId = subkeyList.get(index).get("keyId"); String keyText = getSubkeyText(keyId, subkeyList.get(index).get("keyText")); - String jsFormat = "javascript:executePopupKey('%s','%s')"; + String jsFormat = "executePopupKey('%s','%s')"; String jsString = String.format(jsFormat, keyId, keyText); - loadUrl(jsString); + loadJavascript(jsString); } }); button.setClickable(false); @@ -909,8 +952,8 @@ final class KMKeyboard extends WebView { public void onDismiss() { subKeysList = null; subKeysWindow = null; - String jsString = "javascript:popupVisible(0)"; - loadUrl(jsString); + String jsString = "popupVisible(0)"; + loadJavascript(jsString); } }); @@ -933,8 +976,8 @@ final class KMKeyboard extends WebView { // And now to actually display it. subKeysWindow.showAtLocation(KMKeyboard.this, Gravity.TOP | Gravity.LEFT, posX, posY); - String jsString = "javascript:popupVisible(1)"; - loadUrl(jsString); + String jsString = "popupVisible(1)"; + loadJavascript(jsString); } // Attempt to get the subkey text. diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java index cadf858abf..bccc76f973 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java @@ -718,11 +718,11 @@ public final class KMManager { RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); if (InAppKeyboard != null && InAppKeyboardLoaded && !InAppKeyboardShouldIgnoreTextChange) { InAppKeyboard.setLayoutParams(params); - InAppKeyboard.loadUrl(String.format("javascript:enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); + InAppKeyboard.loadJavascript(String.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); } if (SystemKeyboard != null && SystemKeyboardLoaded && !SystemKeyboardShouldIgnoreTextChange) { SystemKeyboard.setLayoutParams(params); - SystemKeyboard.loadUrl(String.format("javascript:enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); + SystemKeyboard.loadJavascript(String.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); } return true; } @@ -733,13 +733,13 @@ public final class KMManager { currentLexicalModel = null; } - String url = String.format("javascript:deregisterModel('%s')", modelID); - if (InAppKeyboard != null && InAppKeyboardLoaded) { - InAppKeyboard.loadUrl(url); + String url = String.format("deregisterModel('%s')", modelID); + if (InAppKeyboard != null) { // && InAppKeyboardLoaded) { + InAppKeyboard.loadJavascript(url); } - if (SystemKeyboard != null && SystemKeyboardLoaded) { - SystemKeyboard.loadUrl(url); + if (SystemKeyboard != null) { // && SystemKeyboardLoaded) { + SystemKeyboard.loadJavascript(url); } return true; } @@ -1103,11 +1103,11 @@ public final class KMManager { public static void setNumericLayer(KeyboardType kbType) { if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (InAppKeyboard != null && InAppKeyboardLoaded && !InAppKeyboardShouldIgnoreTextChange) { - InAppKeyboard.loadUrl("javascript:setNumericLayer()"); + InAppKeyboard.loadJavascript("setNumericLayer()"); } } else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { if (SystemKeyboard != null && SystemKeyboardLoaded && !SystemKeyboardShouldIgnoreTextChange) { - SystemKeyboard.loadUrl("javascript:setNumericLayer()"); + SystemKeyboard.loadJavascript("setNumericLayer()"); } } } @@ -1121,14 +1121,14 @@ public final class KMManager { if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (InAppKeyboard != null && InAppKeyboardLoaded && !InAppKeyboardShouldIgnoreTextChange) { - InAppKeyboard.loadUrl(String.format("javascript:updateKMText('%s')", kmText)); + InAppKeyboard.loadJavascript(String.format("updateKMText('%s')", kmText)); result = true; } InAppKeyboardShouldIgnoreTextChange = false; } else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { if (SystemKeyboard != null && SystemKeyboardLoaded && !SystemKeyboardShouldIgnoreTextChange) { - SystemKeyboard.loadUrl(String.format("javascript:updateKMText('%s')", kmText)); + SystemKeyboard.loadJavascript(String.format("updateKMText('%s')", kmText)); result = true; } @@ -1142,7 +1142,7 @@ public final class KMManager { boolean result = false; if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (InAppKeyboard != null && InAppKeyboardLoaded && !InAppKeyboardShouldIgnoreSelectionChange) { - InAppKeyboard.loadUrl(String.format("javascript:updateKMSelectionRange(%d,%d)", selStart, selEnd)); + InAppKeyboard.loadJavascript(String.format("updateKMSelectionRange(%d,%d)", selStart, selEnd)); result = true; } @@ -1157,7 +1157,7 @@ public final class KMManager { } } - SystemKeyboard.loadUrl(String.format("javascript:updateKMSelectionRange(%d,%d)", selStart, selEnd)); + SystemKeyboard.loadJavascript(String.format("updateKMSelectionRange(%d,%d)", selStart, selEnd)); result = true; } @@ -1170,11 +1170,11 @@ public final class KMManager { public static void resetContext(KeyboardType kbType) { if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (InAppKeyboard != null && InAppKeyboardLoaded) { - InAppKeyboard.loadUrl("javascript:resetContext()"); + InAppKeyboard.loadJavascript("resetContext()"); } } else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { if (SystemKeyboard != null && SystemKeyboardLoaded) { - SystemKeyboard.loadUrl("javascript:resetContext()"); + SystemKeyboard.loadJavascript("resetContext()"); } } } @@ -1301,15 +1301,18 @@ public final class KMManager { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - if (url.endsWith(KMFilename_KeyboardHtml)) { - InAppKeyboardLoaded = false; - } } @Override public void onPageFinished(WebView view, String url) { + Log.d("KMEA", "onPageFinished: [inapp] " + url); + shouldOverrideUrlLoading(view, url); + } + + private void pageLoaded(WebView view, String url) { String langId = KMManager.KMKey_LanguageID; - if (url.endsWith(KMFilename_KeyboardHtml)) { + Log.d("KMEA", "pageLoaded: [inapp] " + url); + if (url.startsWith("file")) { //endsWith(KMFilename_KeyboardHtml)) { InAppKeyboardLoaded = true; if (!InAppKeyboard.keyboardSet) { @@ -1342,20 +1345,23 @@ public final class KMManager { public void run() { SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); if (prefs.getBoolean(KMManager.KMKey_ShouldShowHelpBubble, true)) { - InAppKeyboard.loadUrl("javascript:showHelpBubble()"); + InAppKeyboard.loadJavascript("showHelpBubble()"); } } }, 2000); + InAppKeyboard.callJavascriptAfterLoad(); + KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, KeyboardType.KEYBOARD_TYPE_INAPP, EventType.KEYBOARD_LOADED, null); } - - shouldOverrideUrlLoading(view, url); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.indexOf("hideKeyboard") >= 0) { + Log.d("KMEA", "shouldOverrideUrlLoading [inapp]: "+url); + if(url.indexOf("pageLoaded") >= 0) { + pageLoaded(view, url); + } else if (url.indexOf("hideKeyboard") >= 0) { if (KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) { InAppKeyboard.dismissHelpBubble(); KMTextView textView = (KMTextView) KMTextView.activeView; @@ -1490,17 +1496,19 @@ public final class KMManager { JSONParser parser = new JSONParser(); JSONObject obj = parser.getJSONObjectFromURIString(suggestionJSON); + /* // For future implementation InAppKeyboard.suggestionWindowPos = new double[]{x, y}; InAppKeyboard.suggestionJSON = suggestionJSON; try { Log.v("KMEA", "Suggestion display: " + obj.getString("displayAs")); Log.v("KMEA", "Suggestion's banner coords: " + x + ", " + y + ", " + width + ", " + height); - Log.v("KMEA", "Is a suggestion: " + isCustom); + Log.v("KMEA", "Is a suggestion: " + isCustom); // likely outdated now that tags exist. } catch (JSONException e) { //e.printStackTrace(); Log.v("KMEA", "JSON parsing error: " + e.getMessage()); } + */ } return false; } @@ -1515,15 +1523,18 @@ public final class KMManager { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - if (url.endsWith(KMFilename_KeyboardHtml)) { - SystemKeyboardLoaded = false; - } } @Override public void onPageFinished(WebView view, String url) { + Log.d("KMEA", "onPageFinished: [system] " + url); + shouldOverrideUrlLoading(view, url); + } + + private void pageLoaded(WebView view, String url) { String langId = KMManager.KMKey_LanguageID; - if (url.endsWith(KMFilename_KeyboardHtml)) { + Log.d("KMEA", "pageLoaded: [system] " + url); + if (url.startsWith("file:")) { SystemKeyboardLoaded = true; if (!SystemKeyboard.keyboardSet) { SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); @@ -1555,20 +1566,22 @@ public final class KMManager { public void run() { SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); if (prefs.getBoolean(KMManager.KMKey_ShouldShowHelpBubble, true)) { - SystemKeyboard.loadUrl("javascript:showHelpBubble()"); + SystemKeyboard.loadJavascript("showHelpBubble()"); } } }, 2000); KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, KeyboardType.KEYBOARD_TYPE_SYSTEM, EventType.KEYBOARD_LOADED, null); - } - shouldOverrideUrlLoading(view, url); + SystemKeyboard.callJavascriptAfterLoad(); + } } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.indexOf("hideKeyboard") >= 0) { + if(url.indexOf("pageLoaded") >= 0) { + pageLoaded(view, url); + } else if (url.indexOf("hideKeyboard") >= 0) { SystemKeyboard.dismissHelpBubble(); IMService.requestHideSelf(0); } else if (url.indexOf("globeKeyAction") >= 0) { @@ -1718,14 +1731,19 @@ public final class KMManager { JSONParser parser = new JSONParser(); JSONObject obj = parser.getJSONObjectFromURIString(suggestionJSON); + /* // For future implementation + SystemKeyboard.suggestionWindowPos = new double[]{x, y}; + SystemKeyboard.suggestionJSON = suggestionJSON; + try { Log.v("KMEA", "Suggestion display: " + obj.getString("displayAs")); Log.v("KMEA", "Suggestion's banner coords: " + x + ", " + y + ", " + width + ", " + height); - Log.v("KMEA", "Is a suggestion: " + isCustom); + Log.v("KMEA", "Is a suggestion: " + isCustom); // likely outdated now that tags exist. } catch (JSONException e) { //e.printStackTrace(); Log.v("KMEA", "JSON parsing error: " + e.getMessage()); } + */ } return false; diff --git a/android/history.md b/android/history.md index bdee99089a..f71df1d60e 100644 --- a/android/history.md +++ b/android/history.md @@ -1,10 +1,9 @@ # Keyman for Android -## 12.0 alpha +## 2019-07-29 12.0.2200 beta * Add new "Settings" menu (#1751) -* Update compile and target Android SDK version to 28 (#1654) -* Add feature to install lexical model packages (#1641, 1653) -* Started work on Keyman for Android 12. +* Add predictive text support (#1641, #1653) +* Minimum supported Android version is now 4.4 (KitKat) (#1905) ## 2019-06-28 11.0.2108 stable * Changes: diff --git a/common/predictive-text/package.json b/common/predictive-text/package.json index 83c32f5fe1..4695cf9a46 100644 --- a/common/predictive-text/package.json +++ b/common/predictive-text/package.json @@ -14,7 +14,7 @@ "url": "git+https://github.com/eddieantonio/keyman-lmlayer-prototype.git" }, "author": "Eddie Antonio Santos (http://eddieantonio.ca/)", - "license": "AGPL-3.0", + "license": "MIT", "bugs": { "url": "https://github.com/eddieantonio/keyman-lmlayer-prototype/issues" }, diff --git a/common/predictive-text/unit_tests/in_browser/CI.conf.js b/common/predictive-text/unit_tests/in_browser/CI.conf.js index 4732a552cf..797e93bbe8 100644 --- a/common/predictive-text/unit_tests/in_browser/CI.conf.js +++ b/common/predictive-text/unit_tests/in_browser/CI.conf.js @@ -73,12 +73,14 @@ module.exports = function(config) { }; var CURRENT_WIN_LAUNCHERS = { - bs_firefox_win: { - os: 'Windows', - os_version: '10', - browser: 'firefox', - browser_version: '62.0' - }, + // Currently, Firefox launcher is unstable; see https://github.com/karma-runner/karma-firefox-launcher/issues/93 + // (in particular "not maintained" commentary). + //bs_firefox_win: { + // os: 'Windows', + // os_version: '10', + // browser: 'firefox', + // browser_version: '62.0' + //}, bs_chrome_win: { os: 'Windows', os_version: '10', @@ -91,12 +93,13 @@ module.exports = function(config) { browser: 'ie', browser_version: '11.0' }, - bs_edge_win: { - os: 'Windows', - os_version: '10', - browser: 'edge', - browser_version: '17.0' - } + // On recent versions of Edge, launcher fails to start and/or stop Edge successfully + //bs_edge_win: { + // os: 'Windows', + // os_version: '10', + // browser: 'edge', + // browser_version: '17.0' + //} } var CURRENT_ANDROID_LAUNCHERS = { diff --git a/common/predictive-text/unit_tests/in_browser/browser-test.sh b/common/predictive-text/unit_tests/in_browser/browser-test.sh index 9266d5b12a..e56d3fe56c 100755 --- a/common/predictive-text/unit_tests/in_browser/browser-test.sh +++ b/common/predictive-text/unit_tests/in_browser/browser-test.sh @@ -32,7 +32,7 @@ get_browser_set_for_OS ( ) { if [ $os_id = "mac" ]; then BROWSERS="--browsers Firefox,Chrome,Safari" elif [ $os_id = "win" ]; then - BROWSERS="--browsers Firefox,Chrome,IE,Edge" + BROWSERS="--browsers Chrome,IE" else BROWSERS="--browsers Firefox,Chrome" fi diff --git a/common/predictive-text/unit_tests/in_browser/manual.conf.js b/common/predictive-text/unit_tests/in_browser/manual.conf.js index 417823fb56..6918d4a0e0 100644 --- a/common/predictive-text/unit_tests/in_browser/manual.conf.js +++ b/common/predictive-text/unit_tests/in_browser/manual.conf.js @@ -9,7 +9,8 @@ module.exports = function(config) { // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Firefox', 'IE', 'Chrome', 'Edge'], // Can be specified at run-time instead! + browsers: ['IE', 'Chrome'], // Can be specified at run-time instead! + // Note: disabled Firefox, Edge due to instability // Future note for us: https://www.npmjs.com/package/karma-browserstack-launcher // Concurrency level diff --git a/common/predictive-text/worker/worker-interfaces.ts b/common/predictive-text/worker/worker-interfaces.ts index 514784aa4f..a555db671e 100644 --- a/common/predictive-text/worker/worker-interfaces.ts +++ b/common/predictive-text/worker/worker-interfaces.ts @@ -135,8 +135,54 @@ interface LMLayerWorkerState { * The model implementation, within the Worker. */ interface WorkerInternalModel { + /** + * Processes `config` messages, configuring the newly-loaded model based on the host + * platform's capability restrictions. + * + * This allows the model to configure its suggestions according to what the platform + * allows the host to actually perform - for example, if post-caret deletions are not + * supported, no suggestions requiring this feature should be produced by the model. + * + * Returns a `Configuration` object detailing the capabilities the model plans to + * actually utilize, which must be as restrictive or more restrictive than those + * indicated within the provided `Capabilities` object. + * @param capabilities + */ configure(capabilities: Capabilities): Configuration; + + /** + * Generates predictive suggestions corresponding to the state of context after the proposed + * transform is applied to it. This transform may correspond to a 'correction' of a recent + * keystroke rather than one actually received. + * + * This method should NOT attempt to perform any form of correction; this is modeled within a + * separate component of the LMLayer predictive engine. That is, "th" + "e" should not be + * have "this" for a suggestion ("e" has been 'corrected' to "i"), while "there" would be + * a reasonable prediction. + * + * However, addition of diacritics to characters (which may transform the underlying char code + * when Unicode-normalized) is permitted. For example, "pur" + "e" may reasonably predict + * "purée", where "e" has been transformed to "é" as part of the suggestion. + * + * When both prediction and correction are permitted, said component (the `ModelCompositor`) will + * generally call this method once per 'likely' generated corrected state of the context, + * utilizing the results to compute an overall likelihood across all possible suggestions. + * @param transform A Transform corresponding to a recent input keystroke + * @param context A depiction of the context to which `transform` is applied. + * @returns A probability distribution (`Distribution`) on the resulting `Suggestion` + * space for use in determining the most optimal overall suggestions. + */ predict(transform: Transform, context: Context): Distribution; + + /** + * Performs a wordbreak operation given the current context state, returning whatever word + * or word fragment exists that starts before the caret but after the most recent whitespace + * preceding the caret. If no such text exists, the empty string is returned. + * + * This function is designed for use in generating display text for 'keep' `Suggestions` + * and display text for reverting any previously-applied `Suggestions`. + * @param context + */ wordbreak(context: Context): USVString; /** diff --git a/developer/js/build.sh b/developer/js/build.sh new file mode 100755 index 0000000000..0308dc00e3 --- /dev/null +++ b/developer/js/build.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# +# Compiles the developer tools, including the language model compilers. +# + +# Exit on command failure and when using unset variables: +set -eu + +# Include some helper functions from resources +. ../../resources/shellHelperFunctions.sh +EX_USAGE=64 + + +# Build the main script. +build () { + npm run build || fail "Could not build top-level JavaScript file." +} + +display_usage ( ) { + echo "Usage: $0 [-test]" + echo " $0 -help" + echo + echo " -help displays this screen and exits" + echo " -test runs unit tests after building" +} + +################################ Main script ################################ + +run_tests=0 + +# Process command-line arguments +while [[ $# -gt 0 ]] ; do + key="$1" + case $key in + -help|-h) + display_usage + exit + ;; + -test) + run_tests=1 + ;; + *) + echo "$0: invalid option: $key" + display_usage + exit $EX_USAGE + esac + shift # past the processed argument +done + +# Check if Node.JS/npm is installed. +type npm >/dev/null ||\ + fail "Build environment setup error detected! Please ensure Node.js is installed!" + +build || fail "Compilation failed." +echo "Typescript compilation successful." + +if (( run_tests )); then + npm test || fail "Tests failed" +fi diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 9789c00731..9d2af3cb20 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -123,12 +123,12 @@ 9A3B14D3229370B20052A11F /* InstalledLanguagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3B14D1229370B20052A11F /* InstalledLanguagesViewController.swift */; }; 9A4609972241B39B00B0BFD1 /* LexicalModelInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4609962241B39B00B0BFD1 /* LexicalModelInfoViewController.swift */; }; 9A4609992242047400B0BFD1 /* LexicalModelAPICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4609982242047400B0BFD1 /* LexicalModelAPICall.swift */; }; - 9A60764422893A4E003BCFBA /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A60764322893A4E003BCFBA /* SettingsViewController.swift */; }; - 9A7EEFA222DCF64F00877C22 /* LanguageSpecificViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7EEFA122DCF64F00877C22 /* LanguageSpecificViewController.swift */; }; 9A5091B122B418140094B99C /* KeyboardCommandStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */; }; 9A5091B222B418A90094B99C /* KeyboardPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62161F67CFB800268D03 /* KeyboardPickerViewController.swift */; }; 9A5091B322B418CA0094B99C /* KeyboardInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62141F67C8D500268D03 /* KeyboardInfoViewController.swift */; }; 9A5091B422B419B10094B99C /* LanguageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04C2A6C1F6B7D9A00BA42B6 /* LanguageViewController.swift */; }; + 9A60764422893A4E003BCFBA /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A60764322893A4E003BCFBA /* SettingsViewController.swift */; }; + 9A7EEFA222DCF64F00877C22 /* LanguageSpecificViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7EEFA122DCF64F00877C22 /* LanguageSpecificViewController.swift */; }; 9A862D2822B429FB005FE26D /* KeyboardNameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62121F67C31100268D03 /* KeyboardNameTableViewCell.swift */; }; 9A862D2922B42A0C005FE26D /* LanguageDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C092D8381F6A70C8005C5485 /* LanguageDetailViewController.swift */; }; 9A9CB08022416E5400231FB9 /* LexicalModelPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB07F22416E5400231FB9 /* LexicalModelPickerViewController.swift */; }; @@ -139,6 +139,11 @@ 9AD4F53D229F85AC007992D3 /* LanguageSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9AD4F53B229F85AC007992D3 /* LanguageSettingsViewController.xib */; }; 9AD4F53E22A8A25B007992D3 /* LanguageSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD4F53A229F85AC007992D3 /* LanguageSettingsViewController.swift */; }; 9AD4F53F22A8A286007992D3 /* LanguageSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9AD4F53B229F85AC007992D3 /* LanguageSettingsViewController.xib */; }; + 9ADC459A22E186F5004C78C6 /* LanguageSpecificViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7EEFA122DCF64F00877C22 /* LanguageSpecificViewController.swift */; }; + 9ADC459D22E1895D004C78C6 /* LanguageLMDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC459B22E1895D004C78C6 /* LanguageLMDetailViewController.swift */; }; + 9ADC459E22E1895D004C78C6 /* LanguageLMDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC459B22E1895D004C78C6 /* LanguageLMDetailViewController.swift */; }; + 9ADC459F22E1895D004C78C6 /* LanguageLMDetailViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9ADC459C22E1895D004C78C6 /* LanguageLMDetailViewController.xib */; }; + 9ADC45A022E1895D004C78C6 /* LanguageLMDetailViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9ADC459C22E1895D004C78C6 /* LanguageLMDetailViewController.xib */; }; C007C4651F9F52D8006461B9 /* LanguagesAPICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C007C4641F9F52D8006461B9 /* LanguagesAPICall.swift */; }; C024C9941FA6EB470060583B /* NotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C9931FA6EB470060583B /* NotificationName.swift */; }; C024C9961FA6EC650060583B /* NotificationCenter+Typed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C9951FA6EC650060583B /* NotificationCenter+Typed.swift */; }; @@ -335,8 +340,8 @@ 9A079E3F223B602B00581263 /* LexicalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModel.swift; sourceTree = ""; }; 9A079E42223B61AE00581263 /* FullLexicalModelID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullLexicalModelID.swift; sourceTree = ""; }; 9A082558227589360051EBB0 /* Formatter+ISODateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Formatter+ISODateExtension.swift"; sourceTree = ""; }; - 9A3B14D1229370B20052A11F /* InstalledLanguagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InstalledLanguagesViewController.swift; path = Settings/InstalledLanguagesViewController.swift; sourceTree = ""; }; 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Reachability.framework; path = ../../Carthage/Build/iOS/Reachability.framework; sourceTree = ""; }; + 9A3B14D1229370B20052A11F /* InstalledLanguagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InstalledLanguagesViewController.swift; path = Settings/InstalledLanguagesViewController.swift; sourceTree = ""; }; 9A4609962241B39B00B0BFD1 /* LexicalModelInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelInfoViewController.swift; sourceTree = ""; }; 9A4609982242047400B0BFD1 /* LexicalModelAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelAPICall.swift; sourceTree = ""; }; 9A60763C22892485003BCFBA /* Settings.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Settings.storyboard; path = Settings/Settings.storyboard; sourceTree = ""; }; @@ -348,6 +353,8 @@ 9A9CB0852241717C00231FB9 /* LexicalModelRepositoryDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelRepositoryDelegate.swift; sourceTree = ""; }; 9AD4F53A229F85AC007992D3 /* LanguageSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LanguageSettingsViewController.swift; path = Settings/LanguageSettingsViewController.swift; sourceTree = ""; }; 9AD4F53B229F85AC007992D3 /* LanguageSettingsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = LanguageSettingsViewController.xib; path = Settings/LanguageSettingsViewController.xib; sourceTree = ""; }; + 9ADC459B22E1895D004C78C6 /* LanguageLMDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageLMDetailViewController.swift; sourceTree = ""; }; + 9ADC459C22E1895D004C78C6 /* LanguageLMDetailViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LanguageLMDetailViewController.xib; sourceTree = ""; }; C007C4641F9F52D8006461B9 /* LanguagesAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagesAPICall.swift; sourceTree = ""; }; C024C9931FA6EB470060583B /* NotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationName.swift; sourceTree = ""; }; C024C9951FA6EC650060583B /* NotificationCenter+Typed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Typed.swift"; sourceTree = ""; }; @@ -801,6 +808,8 @@ C08C62161F67CFB800268D03 /* KeyboardPickerViewController.swift */, 9A9CB07F22416E5400231FB9 /* LexicalModelPickerViewController.swift */, C092D8381F6A70C8005C5485 /* LanguageDetailViewController.swift */, + 9ADC459B22E1895D004C78C6 /* LanguageLMDetailViewController.swift */, + 9ADC459C22E1895D004C78C6 /* LanguageLMDetailViewController.xib */, C04C2A6C1F6B7D9A00BA42B6 /* LanguageViewController.swift */, 9A7EEFA122DCF64F00877C22 /* LanguageSpecificViewController.swift */, 98C0B11817A22DB5007ECAFC /* PickerButtons */, @@ -1030,6 +1039,7 @@ buildActionMask = 2147483647; files = ( 9AD4F53F22A8A286007992D3 /* LanguageSettingsViewController.xib in Resources */, + 9ADC45A022E1895D004C78C6 /* LanguageLMDetailViewController.xib in Resources */, 9A0FCA0922D7C58B00D33F86 /* Keyman.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1039,6 +1049,7 @@ buildActionMask = 2147483647; files = ( C06D37601F82095200F61AE0 /* Keyman.bundle in Resources */, + 9ADC459F22E1895D004C78C6 /* LanguageLMDetailViewController.xib in Resources */, 9AD4F53D229F85AC007992D3 /* LanguageSettingsViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1145,6 +1156,7 @@ 9A31E2C8224AE87600D9A491 /* RegisteredFont.swift in Sources */, 9A5091B422B419B10094B99C /* LanguageViewController.swift in Sources */, 9A079E352236235700581263 /* Version.swift in Sources */, + 9ADC459E22E1895D004C78C6 /* LanguageLMDetailViewController.swift in Sources */, 9A079DDC2231A11000581263 /* Font.swift in Sources */, 9A079E49223F176100581263 /* String+Helpers.swift in Sources */, 9A079DE82236138800581263 /* HTTPDownloadRequest.swift in Sources */, @@ -1170,7 +1182,7 @@ 9A5091B222B418A90094B99C /* KeyboardPickerViewController.swift in Sources */, 9A31E2C2224AAD9A00D9A491 /* Notifications.swift in Sources */, 9A079DDD2231A11C00581263 /* InstallableKeyboard.swift in Sources */, - 9A7EEFA222DCF64F00877C22 /* LanguageSpecificViewController.swift in Sources */, + 9ADC459A22E186F5004C78C6 /* LanguageSpecificViewController.swift in Sources */, 9A079DD2223194B100581263 /* KeymanEngineTests.swift in Sources */, 9A079E41223B602B00581263 /* LexicalModel.swift in Sources */, 9A3B14C0228F59490052A11F /* SettingsViewController.swift in Sources */, @@ -1241,6 +1253,7 @@ C06D37381F81F5C400F61AE0 /* SubKeysView.swift in Sources */, C0A93A541F8B21240079948B /* Manager.swift in Sources */, C024C9981FA6F2340060583B /* NotificationObserver.swift in Sources */, + 9ADC459D22E1895D004C78C6 /* LanguageLMDetailViewController.swift in Sources */, C06D373E1F81F5C400F61AE0 /* KeyboardInfoViewController.swift in Sources */, C0B901AA1FA1AFC200764EB8 /* UserDefaults+Types.swift in Sources */, 9A079E372238680700581263 /* KMPLexicalModel.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift index 918759d2aa..956b33d5fd 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift @@ -179,8 +179,10 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate { open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // Necessary for existing infrastructure to resend info for the keyboard after reloading - // as system keyboard. - Manager.shared.shouldReloadKeyboard = true + // as system keyboard. Do NOT perform if in-app, as this unnecessarily resets the WebView. + if(Manager.shared.isSystemKeyboard) { + Manager.shared.shouldReloadKeyboard = true + } } open override func textDidChange(_ textInput: UITextInput?) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardNameTableViewCell.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardNameTableViewCell.swift index 5b71148716..1f671e2102 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardNameTableViewCell.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardNameTableViewCell.swift @@ -29,9 +29,9 @@ class KeyboardNameTableViewCell: UITableViewCell, UIAlertViewDelegate { } self.accessoryType = accessoryType - let isSelectedView = viewWithTag(cellSelectedTag) + let selectedView = viewWithTag(cellSelectedTag) if isSelected { - if isSelectedView == nil { + if selectedView == nil { let view = UIView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)) view.tag = cellSelectedTag view.backgroundColor = UIColor.green @@ -39,7 +39,7 @@ class KeyboardNameTableViewCell: UITableViewCell, UIAlertViewDelegate { addSubview(view) } } else { - isSelectedView?.removeFromSuperview() + selectedView?.removeFromSuperview() } } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift index 1aa7ffea90..5244297180 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift @@ -50,9 +50,11 @@ class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate { override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - log.info("didAppear: LanguageDetailViewController (actually willAppear)") + log.info("willAppear: LanguageDetailViewController") } + // MARK: - Table view data source UITableViewDataSource + override func numberOfSections(in tableView: UITableView) -> Int { return language.keyboards!.count } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.swift new file mode 100644 index 0000000000..3d68ab8bf0 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.swift @@ -0,0 +1,201 @@ +// +// LanguageLMDetailViewController.swift +// KeymanEngine +// +// Created by Randy Boring on 7/19/19. +// Copyright © 2019 SIL International. All rights reserved. +// + + +import UIKit + +private let toolbarButtonTag = 100 +private let toolbarLabelTag = 101 +private let toolbarActivityIndicatorTag = 102 + +class LanguageLMDetailViewController: UITableViewController, UIAlertViewDelegate { + private var userLexicalModels: [String: InstallableLexicalModel] = [:] + private var isUpdate = false // unused currently, may be used when we switch to HTTPDownloader + private let language: Language + public var lexicalModels: [LexicalModel]? = nil + + private var lexicalModelDownloadStartedObserver: NotificationObserver? + //NOTE: there is no need for a CompletedObserver, as our parent LexicalModelPickerViewController + // is registered for that and deals with it by popping us out to root. + private var lexicalModelDownloadFailedObserver: NotificationObserver? + + init(language: Language) { + self.language = language + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + super.loadView() + loadUserLexicalModels() + tableView.dataSource = self + } + + override func viewDidLoad() { + super.viewDidLoad() + lexicalModelDownloadStartedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadStarted, + observer: self, + function: LanguageLMDetailViewController.lexicalModelDownloadStarted) + lexicalModelDownloadFailedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadFailed, + observer: self, + function: LanguageLMDetailViewController.lexicalModelDownloadFailed) + log.info("viewDidLoad: LanguageLMDetailViewController (registered for lexicalModelDownloadStarted)") + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + log.info("willAppear: LanguageLMDetailViewController") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + log.info("didAppear: LanguageLMDetailViewController") + + navigationController?.setToolbarHidden(true, animated: true) + } + + // MARK: - Table view data source UITableViewDataSource + + override func numberOfSections(in tableView: UITableView) -> Int { + return lexicalModels!.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellIdentifier = "Cell" + if let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) { + return cell + } + + let cell = KeyboardNameTableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) + let selectionColor = UIView() + selectionColor.backgroundColor = UIColor(red: 204.0 / 255.0, green: 136.0 / 255.0, blue: 34.0 / 255.0, alpha: 1.0) + cell.selectedBackgroundView = selectionColor + return cell + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + let lexicalModel = lexicalModels![indexPath.section] + let cell = cell as! KeyboardNameTableViewCell + cell.indexPath = indexPath + cell.textLabel?.text = lexicalModel.name + if isAdded(languageID: language.id, lexicalModelID: lexicalModel.id) { + cell.accessoryType = .checkmark + cell.isUserInteractionEnabled = false + cell.textLabel?.isEnabled = false + cell.detailTextLabel?.isEnabled = false + } else { + cell.accessoryType = .none + cell.isUserInteractionEnabled = true + cell.textLabel?.isEnabled = true + cell.detailTextLabel?.isEnabled = true + } + + let kbState = Manager.shared.stateForLexicalModel(withID: lexicalModel.id) + cell.setKeyboardState(kbState, selected: false, defaultAccessoryType: cell.accessoryType) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.cellForRow(at: indexPath)?.isSelected = false + let lexicalModelIndex = indexPath.section + let lexicalModel = lexicalModels![lexicalModelIndex] + + let state = Manager.shared.stateForLexicalModel(withID: lexicalModel.id) + if state != .downloading { + if state == .needsDownload { + isUpdate = false + } else { + isUpdate = true + } + + let alertController = UIAlertController(title: "\(language.name): \(lexicalModel.name)", + message: "Would you like to download this dictionary?", + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: "Cancel", + style: UIAlertActionStyle.cancel, + handler: nil)) + alertController.addAction(UIAlertAction(title: "Download", + style: UIAlertActionStyle.default, + handler: {_ in self.downloadHandler(lexicalModelIndex)} )) + + self.present(alertController, animated: true, completion: nil) + } + } + + func downloadHandler(_ lexicalModelIndex: Int) { + Manager.shared.downloadLexicalModelPackage(string: (lexicalModels?[lexicalModelIndex].packageFilename)!) + } + + private func lexicalModelDownloadStarted() { + log.info("lexicalModelDownloadStarted: LanguageLMDetailViewController") + view.isUserInteractionEnabled = false + navigationItem.setHidesBackButton(true, animated: true) + + let toolbarFrame = navigationController!.toolbar.frame + let labelFrame = CGRect(origin: toolbarFrame.origin, + size: CGSize(width: toolbarFrame.width * 0.95, height: toolbarFrame.height * 0.7)) + let label = UILabel(frame: labelFrame) + label.backgroundColor = UIColor.clear + label.textColor = UIColor.white + label.textAlignment = .center + label.center = CGPoint(x: toolbarFrame.width * 0.5, y: toolbarFrame.height * 0.5) + label.text = "Downloading\u{2026}" + label.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, + .flexibleBottomMargin, .flexibleWidth, .flexibleHeight] + label.tag = toolbarLabelTag + + let indicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray) + indicatorView.center = CGPoint(x: toolbarFrame.width - indicatorView.frame.width, + y: toolbarFrame.height * 0.5) + indicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin, .flexibleBottomMargin] + indicatorView.tag = toolbarActivityIndicatorTag + indicatorView.startAnimating() + + navigationController?.toolbar.viewWithTag(toolbarButtonTag)?.removeFromSuperview() + navigationController?.toolbar.viewWithTag(toolbarLabelTag)?.removeFromSuperview() + navigationController?.toolbar.viewWithTag(toolbarActivityIndicatorTag)?.removeFromSuperview() + navigationController?.toolbar.addSubview(label) + navigationController?.toolbar.addSubview(indicatorView) + navigationController?.setToolbarHidden(false, animated: true) + } + + private func lexicalModelDownloadFailed() { + log.info("lexicalModelDownloadFailed: LanguageLMDetailViewController") + view.isUserInteractionEnabled = true + navigationItem.setHidesBackButton(false, animated: true) + } + + private func loadUserLexicalModels() { + guard let userLmList = Storage.active.userDefaults.userLexicalModels, !userLmList.isEmpty else { + userLexicalModels = [:] + return + } + + userLexicalModels = [:] + for lm in userLmList { + let dictKey = "\(lm.languageID)_\(lm.id)" + userLexicalModels[dictKey] = lm + } + } + + private func isAdded(languageID: String?, lexicalModelID: String?) -> Bool { + guard let languageID = languageID, let lexicalModelID = lexicalModelID else { + return false + } + return userLexicalModels["\(languageID)_\(lexicalModelID)"] != nil + } +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.xib b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.xib new file mode 100644 index 0000000000..d379dc8b53 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageLMDetailViewController.xib @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift index 812b36d1d0..1aff71e17d 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift @@ -101,7 +101,9 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { } else { log.info("didAppear: LanguageViewController") } - } + } + + // MARK: - Table view data source UITableViewDataSource override func numberOfSections(in tableView: UITableView) -> Int { return languages.count @@ -242,7 +244,7 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { func downloadHandler(_ keyboardIndex: Int) { let language = languages[selectedSection] let keyboard = language.keyboards![keyboardIndex] - Manager.shared.downloadKeyboard(withID: keyboard.id, languageID: language.id, isUpdate: isUpdate) + Manager.shared.downloadKeyboard(withID: keyboard.id, languageID: language.id, isUpdate: isUpdate) } private func keyboardDownloadStarted() { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift index 67e50e5a1a..3ddeb28165 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift @@ -24,7 +24,7 @@ class LexicalModelInfoViewController: UITableViewController, UIAlertViewDelegate infoArray = [[String: String]]() infoArray.append([ - "title": "Model version", + "title": "Dictionary version", "subtitle": lexicalModelVersion ]) @@ -35,7 +35,7 @@ class LexicalModelInfoViewController: UITableViewController, UIAlertViewDelegate ]) } infoArray.append([ - "title": "Uninstall model", + "title": "Uninstall dictionary", "subtitle": "" ]) } @@ -135,7 +135,7 @@ class LexicalModelInfoViewController: UITableViewController, UIAlertViewDelegate } private func showDeleteLexicalModel() { - let alertController = UIAlertController(title: title ?? "", message: "Would you like to delete this lexical model?", + let alertController = UIAlertController(title: title ?? "", message: "Would you like to delete this dictionary?", preferredStyle: UIAlertControllerStyle.alert) alertController.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift index b70a91c115..433929b307 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift @@ -15,7 +15,7 @@ private let toolbarActivityIndicatorTag = 102 class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelegate { private var userLexicalModels: [InstallableLexicalModel] = [InstallableLexicalModel]() - public var language: Language? = nil + public var language: Language! private var updateQueue: [InstallableLexicalModel]? private var _isDoneButtonEnabled = false private var isDidUpdateCheck = false @@ -24,17 +24,19 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega private var lexicalModelDownloadCompletedObserver: NotificationObserver? private var lexicalModelDownloadFailedObserver: NotificationObserver? + public init(_ language: Language) { + self.language = language + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() - if let langName = language?.name { - title = "\(langName) Lexical Models" - } else { - title = "Lexical Models" - } - if language == nil { - setIsDoneButtonEnabled(false) - } + title = "\(language.name) Dictionaries" isDidUpdateCheck = false updateQueue = nil if Manager.shared.canAddNewLexicalModels { @@ -137,7 +139,6 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega if Manager.shared.removeLexicalModel(at: globalIndex) { loadUserLexicalModels() } - setIsDoneButtonEnabled(true) } override func tableView(_ tableView: UITableView, @@ -146,9 +147,6 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } func showLexicalModelInfoView(with index: Int) { - if language == nil { - setIsDoneButtonEnabled(true) - } let lm = userLexicalModels[index] let version = lm.version @@ -178,7 +176,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega if Manager.shared.currentLexicalModelID == lm.fullID { cell.selectionStyle = .blue cell.isSelected = true - cell.accessoryType = .checkmark + cell.accessoryType = .detailDisclosureButton } else { cell.selectionStyle = .none cell.isSelected = false @@ -236,14 +234,14 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } updateQueue = nil let label = navigationController?.toolbar?.viewWithTag(toolbarLabelTag) as? UILabel - label?.text = "Lexical models successfully updated!" + label?.text = "Dictionaries successfully updated!" navigationController?.toolbar?.viewWithTag(toolbarActivityIndicatorTag)?.removeFromSuperview() Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(self.hideToolbarDelayed), userInfo: nil, repeats: false) } } else { let label = navigationController?.toolbar?.viewWithTag(toolbarLabelTag) as? UILabel - label?.text = "Lexical model successfully downloaded!" + label?.text = "Dictionary successfully downloaded!" navigationController?.toolbar?.viewWithTag(toolbarActivityIndicatorTag)?.removeFromSuperview() Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(self.hideToolbarDelayed), userInfo: nil, repeats: false) @@ -276,9 +274,9 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega let title: String if view == navigationController?.topViewController?.view { updateQueue = nil - title = "Lexical model Update Error" + title = "Dictionary Update Error" } else { - title = "Lexical model Download Error" + title = "Dictionary Download Error" } navigationController?.setToolbarHidden(true, animated: true) @@ -399,8 +397,8 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } private func scroll(toSelectedLexicalModel animated: Bool) { - let index = userLexicalModels.index { kb in - return Manager.shared.currentLexicalModelID == kb.fullID + let index = userLexicalModels.index { lm in + return Manager.shared.currentLexicalModelID == lm.fullID } if let index = index { @@ -428,13 +426,32 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } func showAddLexicalModel() { - let button: UIButton? = (navigationController?.toolbar?.viewWithTag(toolbarButtonTag) as? UIButton) - button?.isEnabled = false - let vc = LanguageViewController(Manager.shared.apiLexicalModelRepository) //may need to be different for models - navigationController?.pushViewController(vc, animated: true) - if language == nil { - setIsDoneButtonEnabled(true) + //get list of lexical models for this languageID and show it + func listCompletionHandler(lexicalModels: [LexicalModel]?, error: Error?) -> Void { + if let error = error { + log.info("Failed to fetch lexical model list for "+language.id+". error: "+(error as! String)) + DispatchQueue.main.async { + self.lexicalModelDownloadFailed(LexicalModelDownloadFailedNotification(lmOrLanguageID: self.language.id, error: error)) + } + } else if nil == lexicalModels { + log.info("No lexical models available for language \(language.id) (nil)") + } else if 0 == lexicalModels?.count { + log.info("No lexical models available for language \(language.id) (empty)") + } else { + log.info("Fetched lexical model list for "+language.id+".") + // show the list of lexical models (on the main thread) + DispatchQueue.main.async { + let button: UIButton? = (self.navigationController?.toolbar?.viewWithTag(toolbarButtonTag) as? UIButton) + button?.isEnabled = false + let vc = LanguageLMDetailViewController(language: self.language) + vc.lexicalModels = lexicalModels! + self.navigationController?.pushViewController(vc, animated: true) + } + } } + + Manager.shared.apiLexicalModelRepository.fetchList(languageID: language.id, completionHandler: listCompletionHandler) } + } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index b30d5acfdd..5118c0a3fd 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -17,7 +17,7 @@ typealias FetchKeyboardsBlock = ([String: Any]?) -> Void // MARK: - Constants -// Possible states that a keyboard can be in +// Possible states that a keyboard or lexical model can be in public enum KeyboardState { case needsDownload case needsUpdate @@ -26,15 +26,6 @@ public enum KeyboardState { case none } -// Possible states that a lexical model can be in -public enum LexicalModelState { - case needsDownload - case needsUpdate - case upToDate - case downloading - case none -} - public enum VibrationSupport { case none // Has no vibrator case basic // Has only the basic 0.4 sec long vibration @@ -313,6 +304,8 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat // If we have a lexical model for the keyboard's language, activate it. if let preferred_model = preferredLexicalModel(userDefaults, forLanguage: kb.languageID) { _ = Manager.shared.registerLexicalModel(preferred_model) + } else if let first_model = userDefaults.userLexicalModels?.first(where: { $0.languageID == kb.languageID }) { + _ = Manager.shared.registerLexicalModel(first_model) } return true @@ -482,30 +475,50 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat } /// Removes the lexical model at index from the lexical models list if it exists. - /// - Returns: The lexical model exists and was removed - public func removeLexicalModel(at index: Int) -> Bool { - let userData = Storage.active.userDefaults - + public func removeLexicalModelFromUserList(userDefs ud: UserDefaults, at index: Int) -> InstallableLexicalModel? { // If user defaults for lexical models list does not exist, do nothing. - guard var userLexicalModels = userData.userLexicalModels else { - return false + guard var userLexicalModels = ud.userLexicalModels else { + return nil } guard index < userLexicalModels.count else { - return false + return nil } let lm = userLexicalModels[index] + log.info("Removing lexical model with ID \(lm.id) and languageID \(lm.languageID) from user list of all models") userLexicalModels.remove(at: index) - userData.userLexicalModels = userLexicalModels - userData.set([Date()], forKey: Key.synchronizeSWLexicalModel) - userData.synchronize() - - log.info("Removing lexical model with ID \(lm.id) and languageID \(lm.languageID)") + ud.userLexicalModels = userLexicalModels + ud.set([Date()], forKey: Key.synchronizeSWLexicalModel) + ud.synchronize() + return lm +} + + /// Removes the lexical model at index from the lexical models list if it exists. + public func removeLexicalModelFromLanguagePreference(userDefs ud: UserDefaults, _ lm: InstallableLexicalModel) { + log.info("Removing lexical model with ID \(lm.id) and languageID \(lm.languageID) from per-language prefs") + ud.set(preferredLexicalModelID: nil, forKey: lm.languageID) + } + + /// Removes the lexical model at index from the lexical models list if it exists. + /// - Returns: The lexical model exists and was removed + public func removeLexicalModel(at index: Int) -> Bool { + let userData = Storage.active.userDefaults + guard let lm = removeLexicalModelFromUserList(userDefs: userData, at: index) else { + return false + } + removeLexicalModelFromLanguagePreference(userDefs: userData, lm) // Set a new lexical model if deleting the current one + let userLexicalModels = userData.userLexicalModels! //removeLexicalModelFromUserList fails above if this is not present + if lm.fullID == currentLexicalModelID { - _ = registerLexicalModel(userLexicalModels[0]) + if let first_lm = userLexicalModels.first(where: {$0.languageID == lm.languageID}) { + _ = registerLexicalModel(first_lm) + } else { + log.info("no more lexical models available for language \(lm.fullID)") + currentLexicalModelID = nil + } } if !userLexicalModels.contains(where: { $0.id == lm.id }) { @@ -1076,6 +1089,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat log.info("Failed to fetch lexical model list for "+languageID+". error: "+(error as! String)) self.downloadFailed(forLanguageID: languageID, error: error) //???forKeyboards } else if nil == lexicalModels { + //TODO: put up an alert instead log.info("No lexical models available for language \(languageID) (nil)") } else if 0 == lexicalModels?.count { log.info("No lexical models available for language \(languageID) (empty)") @@ -1276,7 +1290,8 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat } /// - Returns: The current state for a lexical model - public func stateForLexicalModel(withID lexicalModelID: String) -> LexicalModelState { + //TODO: rename KeyboardState to ResourceState? so it can be used with both keybaoards and lexical models without confusion + public func stateForLexicalModel(withID lexicalModelID: String) -> KeyboardState { if lexicalModelIdForCurrentRequest() == lexicalModelID { return .downloading } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift index 6786d52d1f..aa2f361577 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift @@ -9,7 +9,7 @@ import Foundation struct InstallableConstants { - static let defaultVersion = "0.1.0" + static let defaultVersion = "1.0" } /// Mainly differs from the API `LexicalModel` by having an associated language. diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift index e5c117a914..7f4009b88b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift @@ -12,7 +12,8 @@ public class LexicalModelKeymanPackage : KeymanPackage { public var models : [KMPLexicalModel]! public override func defaultInfoHtml() -> String { - var str = "Found lexical models in package:
" + let count = models.count + var str = "Found "+(count > 1 ? "\(count) dictionaries" : "dictionary")+" in package:
" for model in models { str += model.lexicalModelId! + "
" } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift index 0260f10e16..81e01bc95e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift @@ -20,7 +20,6 @@ class InstalledLanguagesViewController: UITableViewController, UIAlertViewDelega private var sectionIndexTitles: [String] = [] private var indices: [Int] = [] private var selectedSection = 0 - private var isUpdate = false private var installedLanguages: [String: Language] private var languages: [Language] = [] private let keyboardRepository: KeyboardRepository? @@ -165,49 +164,13 @@ class InstalledLanguagesViewController: UITableViewController, UIAlertViewDelega } cell.textLabel?.text = language.name - - if cell.accessoryType == .disclosureIndicator { - return - } - guard let cell = cell as? KeyboardNameTableViewCell else { - return - } - - let keyboardID = language.keyboards![0].id - - cell.accessoryType = .none - cell.isUserInteractionEnabled = true - cell.textLabel?.isEnabled = true - cell.detailTextLabel?.isEnabled = true - - let kbState = Manager.shared.stateForKeyboard(withID: keyboardID) - cell.setKeyboardState(kbState, selected: false, defaultAccessoryType: cell.accessoryType) } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { selectedSection = indexPath.section tableView.cellForRow(at: indexPath)?.isSelected = false - if tableView.cellForRow(at: indexPath)?.accessoryType == .disclosureIndicator { - let title = tableView.cellForRow(at: indexPath)?.textLabel?.text ?? "" - showLanguageSettingsView(title: title, languageIndex: indexPath.section) - return - } - - let language = languages[indexPath.section] - let keyboardIndex = 0; - let keyboard = language.keyboards![keyboardIndex] - - //TODO: launch real Language Settings controller here - let languageSettingsController = UIAlertController(title: "\(language.name): \(keyboard.name)", - message: "This should be Language Settings", - preferredStyle: UIAlertControllerStyle.alert) - languageSettingsController.addAction(UIAlertAction(title: "Cancel", - style: UIAlertActionStyle.cancel, - handler: nil)) -// languageSettingsController.addAction(UIAlertAction(title: "Download", -// style: UIAlertActionStyle.default, -// handler: {_ in self.downloadHandler(keyboardIndex)} )) - self.present(languageSettingsController, animated: true, completion: nil) + let title = tableView.cellForRow(at: indexPath)?.textLabel?.text ?? "" + showLanguageSettingsView(title: title, languageIndex: indexPath.section) } override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { @@ -230,7 +193,7 @@ class InstalledLanguagesViewController: UITableViewController, UIAlertViewDelega func downloadHandler(_ keyboardIndex: Int) { let language = languages[selectedSection] let keyboard = language.keyboards![keyboardIndex] - Manager.shared.downloadKeyboard(withID: keyboard.id, languageID: language.id, isUpdate: isUpdate) + Manager.shared.downloadKeyboard(withID: keyboard.id, languageID: language.id, isUpdate: false) } private func restoreNavigation() { @@ -336,7 +299,7 @@ class InstalledLanguagesViewController: UITableViewController, UIAlertViewDelega log.info("lexicalModelDownloadFailed: InstalledLanguagesViewController") restoreNavigation() - let title = "Lexical Model Download Error" + let title = "Dictionary Download Error" navigationController?.setToolbarHidden(true, animated: true) let alertController = UIAlertController(title: title, message: "", diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift index 1cf3b6f30c..8b7f2d9e91 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift @@ -153,9 +153,9 @@ class LanguageSettingsViewController: UITableViewController { var title: String switch (section) { case 0: - title = "keyboards" + title = "Keyboards" case 1: - title = "language settings" + title = "Language settings" default: title = "unknown header" } @@ -199,19 +199,19 @@ class LanguageSettingsViewController: UITableViewController { case 1: cell.textLabel?.text = "Enable corrections" case 2: - cell.textLabel?.text = "Model" + cell.textLabel?.text = "Dictionaries" cell.accessoryType = .disclosureIndicator if let modelCt = language.lexicalModels?.count { switch modelCt { case 0: - cell.detailTextLabel?.text = "no models installed" + cell.detailTextLabel?.text = "no dictionaries installed" case 1: - cell.detailTextLabel?.text = "one model installed" + cell.detailTextLabel?.text = "\(language.lexicalModels![0].name)" default: - cell.detailTextLabel?.text = "\(modelCt) models installed" + cell.detailTextLabel?.text = "\(modelCt) dictionaries installed" } } else { - cell.detailTextLabel?.text = "no models installed" + cell.detailTextLabel?.text = "no dictionaries installed" } case 3: // future cell.textLabel?.text = "Manage dictionary" @@ -297,7 +297,7 @@ class LanguageSettingsViewController: UITableViewController { func showLexicalModelsView() { //LanguageLexicalModelPickerViewController? (should show just the models for this language) - let lmListView = LexicalModelPickerViewController() + let lmListView = LexicalModelPickerViewController(self.language) lmListView.language = self.language navigationController?.pushViewController(lmListView, animated: true) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift index 57da4fea48..4377a8bedc 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift @@ -16,7 +16,7 @@ open class SettingsViewController: UITableViewController { super.viewWillAppear(animated) loadUserLanguages() - log.info("didAppear: SettingsViewController (actually willAppear)") + log.info("willAppear: SettingsViewController") } override open func viewDidLoad() { @@ -256,7 +256,13 @@ open class SettingsViewController: UITableViewController { } // MARK: - language access - - private func installed2API(_ installedList: [InstallableLexicalModel]) -> [LexicalModel] { + + /** returns an array of LexicalModel created from an array of InstallableLexicalModel + * @param installedList: The InstallableLexicalModel are probably already installed + * The returned LexicalModels are not complete (usually we go the other way round) + * but sufficient for future API calls + */ + public static func installed2API(_ installedList: [InstallableLexicalModel]) -> [LexicalModel] { var returnList = [LexicalModel]() for ilm in installedList { returnList.append(LexicalModel(id: ilm.id, name: ilm.name, license: "", version: ilm.version, languages: [], authorName: "", fileSize: 0, filename: "no filename", sourcePath: nil, authorEmail: nil, description: nil, packageFileSize: 0, packageFilename: "", packageIncludes: nil, isDefault: false, lastModified: nil, minKeymanVersion: nil)) @@ -283,7 +289,7 @@ open class SettingsViewController: UITableViewController { } let userDefaults : UserDefaults = Storage.active.userDefaults let lmListInstalled: [InstallableLexicalModel] = userDefaults.userLexicalModelsForLanguage(languageID: l) ?? [] - let lmList = installed2API(lmListInstalled) + let lmList = SettingsViewController.installed2API(lmListInstalled) keyboardLanguages[l] = Language(name: k.languageName, id: k.languageID, keyboards: kbds, lexicalModels: lmList, font: nil, oskFont: nil) } // there shouldn't be any lexical models for languages that don't have a keyboard installed diff --git a/web/package-lock.json b/web/package-lock.json index 027a9b9997..20668cdba4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -73,13 +73,13 @@ "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=", "dev": true }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "integrity": "sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs=", "dev": true }, "async": { @@ -94,7 +94,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=", "dev": true }, "backo2": { @@ -367,7 +367,7 @@ "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=", "dev": true, "requires": { "color-name": "^1.1.1" @@ -424,7 +424,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", "dev": true }, "cookie": { @@ -484,7 +484,7 @@ "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "integrity": "sha1-38lARACtHI/gI+faHfHBR8S0RN8=", "dev": true, "requires": { "type-detect": "^4.0.0" @@ -872,7 +872,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -1217,7 +1217,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true } } @@ -1242,7 +1242,7 @@ "karma-chrome-launcher": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", - "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "integrity": "sha1-zxudBxNswY/iOTJ9JGVMPbw2is8=", "dev": true, "requires": { "fs-access": "^1.0.0", @@ -1252,7 +1252,7 @@ "karma-edge-launcher": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/karma-edge-launcher/-/karma-edge-launcher-0.4.2.tgz", - "integrity": "sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==", + "integrity": "sha1-PZUpsJsTyQnF887uEtAOf5qYmz0=", "dev": true, "requires": { "edge-launcher": "1.2.2" @@ -1261,7 +1261,7 @@ "karma-firefox-launcher": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz", - "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==", + "integrity": "sha1-LEcDBFLwRTHrfRPU/HZpYwu5Mzk=", "dev": true }, "karma-fixture": { @@ -1328,7 +1328,7 @@ "ansi-styles": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -1374,7 +1374,7 @@ "karma-teamcity-reporter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/karma-teamcity-reporter/-/karma-teamcity-reporter-1.1.0.tgz", - "integrity": "sha512-Ca1uhHGtNqUuzsnW3I+QykNuS/jF9vdxnIrkbLCVJRunCc6yWJq+ai1UobQT13j0e3JVUOf0mKo3QHZ6A6mG9Q==", + "integrity": "sha1-eYD33JHmqwhH4SKt4Ls0x8b0KBY=", "dev": true }, "linkify-it": { @@ -1405,7 +1405,7 @@ "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "integrity": "sha1-V0Dhxdbw39pK2TI7UzIQfva0xAo=", "dev": true, "requires": { "chalk": "^2.0.1" @@ -1414,7 +1414,7 @@ "ansi-styles": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -1548,7 +1548,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -1863,7 +1863,7 @@ "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -1954,7 +1954,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=", "dev": true }, "safer-buffer": { @@ -2198,7 +2198,7 @@ "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "dev": true, "requires": { "safe-buffer": "~5.1.0" @@ -2258,7 +2258,7 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", "dev": true, "requires": { "os-tmpdir": "~1.0.2" @@ -2328,7 +2328,7 @@ "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", "dev": true }, "universalify": { @@ -2418,7 +2418,7 @@ "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", "dev": true, "requires": { "isexe": "^2.0.0" @@ -2482,7 +2482,7 @@ "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", "dev": true, "requires": { "async-limiter": "~1.0.0",